diff --git a/.rubocop.yml b/.rubocop.yml index 2fda0b03119adccf18fe46b3186db1d978e930af..83ed6c3867825f1e8853335e396d0eeb17c5795e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -728,7 +728,7 @@ Metrics/ParameterLists: # A complexity metric geared towards measuring complexity for a human reader. Metrics/PerceivedComplexity: Enabled: true - Max: 17 + Max: 18 #################### Lint ################################ diff --git a/CHANGELOG b/CHANGELOG index 382318a203cc5829f90bbf265d510f79be2a4bfb..89572b9e1565279ef0210bbc8941167e87562882 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,50 +1,132 @@ Please view this file on the master branch, on stable branches it's out of date. +v 8.8.0 (unreleased) + v 8.7.0 (unreleased) - - All service classes (those residing in app/services) are now instrumented (Yorick Peterse) + - The number of InfluxDB points stored per UDP packet can now be configured + - Fix error when cross-project label reference used with non-existent project + - Transactions for /internal/allowed now have an "action" tag set + - Method instrumentation now uses Module#prepend instead of aliasing methods + - Repository.clean_old_archives is now instrumented + - Add support for environment variables on a job level in CI configuration file + - SQL query counts are now tracked per transaction + - The Projects::HousekeepingService class has extra instrumentation + - All service classes (those residing in app/services) are now instrumented + - Developers can now add custom tags to transactions + - Loading of an issue's referenced merge requests and related branches is now done asynchronously - Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea) + - Add support to cherry-pick any commit into any branch in the web interface (Minqi Pan) + - Project switcher uses new dropdown styling - Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea) + - Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles) + - Restrict user profiles when public visibility level is restricted. + - Add ability set due date to issues, sort and filter issues by due date (Mehmet Beydogan) - All images in discussions and wikis now link to their source files !3464 (Connor Shea). - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu) - - Improved Markdown rendering performance !3389 (Yorick Peterse) + - Add setting for customizing the list of trusted proxies !3524 + - Allow projects to be transfered to a lower visibility level group + - Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524 + - Improved Markdown rendering performance !3389 + - Make shared runners text in box configurable - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu) + - API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling) - Expose project badges in project settings + - Make /profile/keys/new redirect to /profile/keys for back-compat. !3717 - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) + - Add support for `after_script`, requires Runner 1.2 (Kamil Trzciński) - Expose label description in API (Mariusz Jachimowicz) - - Allow back dating on issues when created through the API + - API: Ability to update a group (Robert Schilling) + - API: Ability to move issues (Robert Schilling) - Fix Error 500 after renaming a project path (Stan Hu) + - Fix a bug whith trailing slash in teamcity_url (Charles May) + - Allow back dating on issues when created or updated through the API + - Allow back dating on issue notes when created through the API + - Propose license template when creating a new LICENSE file + - API: Expose /licenses and /licenses/:key - Fix avatar stretching by providing a cropping feature - API: Expose `subscribed` for issues and merge requests (Robert Schilling) - Allow SAML to handle external users based on user's information !3530 + - Allow Omniauth providers to be marked as `external` !3657 - Add endpoints to archive or unarchive a project !3372 + - Fix a bug whith trailing slash in bamboo_url - Add links to CI setup documentation from project settings and builds pages + - Display project members page to all members - Handle nil descriptions in Slack issue messages (Stan Hu) + - Add automated repository integrity checks - API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling) + - API: Ability to star and unstar a project (Robert Schilling) - Add default scope to projects to exclude projects pending deletion + - Allow to close merge requests which source projects(forks) are deleted. - Ensure empty recipients are rejected in BuildsEmailService + - Use rugged to change HEAD in Project#change_head (P.S.V.R) - API: Ability to filter milestones by state `active` and `closed` (Robert Schilling) - API: Fix milestone filtering by `iid` (Robert Schilling) + - Make before_script and after_script overridable on per-job (Kamil Trzciński) - API: Delete notes of issues, snippets, and merge requests (Robert Schilling) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Better errors handling when creating milestones inside groups + - Fix high CPU usage when PostReceive receives refs/merge-requests/<id> - Hide `Create a group` help block when creating a new project in a group - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) + - Allow issues and merge requests to be assigned to the author !2765 + - Make Ci::Commit to group only similar builds and make it stateful (ref, tag) - Gracefully handle notes on deleted commits in merge requests (Stan Hu) + - Decouple membership and notifications - Fix creation of merge requests for orphaned branches (Stan Hu) - API: Ability to retrieve a single tag (Robert Schilling) + - While signing up, don't persist the user password across form redisplays - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) - Fix admin/projects when using visibility levels on search (PotHix) - Build status notifications - API: Expose user location (Robert Schilling) + - API: Do not leak group existence via return code (Robert Schilling) - ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591 - Update number of Todos in the sidebar when it's marked as "Done". !3600 + - Sanitize branch names created for confidential issues - API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling) - API: User can leave a project through the API when not master or owner. !3613 + - Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu) + - Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld) + - Improved markdown forms + - Diff design updates (colors, button styles, etc) + - Copying and pasting a diff no longer pastes the line numbers or +/- + - Add null check to formData when updating profile content to fix Firefox bug + - Disable spellcheck and autocorrect for username field in admin page + - Delete tags using Rugged for performance reasons (Robert Schilling) + - Add Slack notifications when Wiki is edited (Sebastian Klier) + - Diffs load at the correct point when linking from from number + - Selected diff rows highlight + - Fix emoji categories in the emoji picker + - API: Properly display annotated tags for GET /projects/:id/repository/tags (Robert Schilling) + - Add encrypted credentials for imported projects and migrate old ones + - Properly format all merge request references with ! rather than # !3740 (Ben Bodenmiller) + - Author and participants are displayed first on users autocompletion + - Show number sign on external issue reference text (Florent Baldino) + - Updated print style for issues + - Use GitHub Issue/PR number as iid to keep references + - Import GitHub labels + - Import GitHub milestones + - Fix emoji catgories in the emoji picker + - Execute system web hooks on push to the project + - Allow enable/disable push events for system hooks + - Fix GitHub project's link in the import page when provider has a custom URL + - Add RAW build trace output and button on build page + - Add incremental build trace update into CI API + +v 8.6.7 + - Fix persistent XSS vulnerability in `commit_person_link` helper + - Fix persistent XSS vulnerability in Label and Milestone dropdowns + - Fix vulnerability that made it possible to enumerate private projects belonging to group v 8.6.6 + - Expire the exists cache before deletion to ensure project dir actually exists (Stan Hu). !3413 + - Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654 + - Fix revoking of authorized OAuth applications (Connor Shea). !3690 - Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk) + - Issuable header is consistent between issues and merge requests + - Improved spacing in issuable header on mobile v 8.6.5 - Fix importing from GitHub Enterprise. !3529 @@ -174,6 +256,9 @@ v 8.6.0 - Trigger a todo for mentions on commits page - Let project owners and admins soft delete issues and merge requests +v 8.5.11 + - Fix persistent XSS vulnerability in `commit_person_link` helper + v 8.5.10 - Fix a 2FA authentication spoofing vulnerability. @@ -244,7 +329,7 @@ v 8.5.1 v 8.5.0 - Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu) - - Cache various Repository methods to improve performance (Yorick Peterse) + - Cache various Repository methods to improve performance - Fix duplicated branch creation/deletion Webhooks/service notifications when using Web UI (Stan Hu) - Ensure rake tasks that don't need a DB connection can be run without one - Update New Relic gem to 3.14.1.311 (Stan Hu) @@ -321,6 +406,9 @@ v 8.5.0 - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Add Todos +v 8.4.9 + - Fix persistent XSS vulnerability in `commit_person_link` helper + v 8.4.8 - Fix a 2FA authentication spoofing vulnerability. @@ -443,6 +531,9 @@ v 8.4.0 - Add IP check against DNSBLs at account sign-up - Added cache:key to .gitlab-ci.yml allowing to fine tune the caching +v 8.3.8 + - Fix persistent XSS vulnerability in `commit_person_link` helper + v 8.3.7 - Fix a 2FA authentication spoofing vulnerability. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f26a5d7eaf434a6aa1bc9042a1aabc923798197..24cd5864530bb098e23015df0f7f22a4a6264c75 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -323,6 +323,7 @@ request is as follows: [shell command guidelines](doc/development/shell_commands.md) 1. If your code creates new files on disk please read the [shared files guidelines](doc/development/shared_files.md). +1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/). The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. This is the best time to submit an MR and get diff --git a/Gemfile b/Gemfile index 258b5612cd59e75cd3ea41cc0c4967a0f0ac6a56..67cc3f34b8c7d3077c894bab5fe85d09858286be 100644 --- a/Gemfile +++ b/Gemfile @@ -190,6 +190,9 @@ gem 'babosa', '~> 1.0.2' # Sanitizes SVG input gem "loofah", "~> 2.0.3" +# Working with license +gem 'licensee', '~> 8.0.0' + # Protect against bruteforcing gem "rack-attack", '~> 4.3.1' @@ -285,9 +288,9 @@ group :development, :test do gem 'teaspoon', '~> 1.1.0' gem 'teaspoon-jasmine', '~> 2.2.0' - gem 'spring', '~> 1.6.4' + gem 'spring', '~> 1.7.0' gem 'spring-commands-rspec', '~> 1.0.4' - gem 'spring-commands-spinach', '~> 1.0.0' + gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'rubocop', '~> 0.38.0', require: false @@ -315,7 +318,7 @@ end gem "newrelic_rpm", '~> 3.14' -gem 'octokit', '~> 3.8.0' +gem 'octokit', '~> 4.3.0' gem "mail_room", "~> 0.6.1" diff --git a/Gemfile.lock b/Gemfile.lock index 9da44a46583d6c6dc384ea0b8681ab48ad5791d8..b00d7b35c845612f05bc5f2c3bd8e1ee66839b1a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -452,6 +452,8 @@ GEM addressable (~> 2.3) letter_opener (1.1.2) launchy (~> 2.2) + licensee (8.0.0) + rugged (>= 0.24b) listen (3.0.5) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) @@ -485,8 +487,8 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (~> 1.2) - octokit (3.8.0) - sawyer (~> 0.6.0, >= 0.5.3) + octokit (4.3.0) + sawyer (~> 0.7.0, >= 0.5.3) omniauth (1.3.1) hashie (>= 1.2, < 4) rack (>= 1.0, < 3) @@ -712,8 +714,8 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - sawyer (0.6.0) - addressable (~> 2.3.5) + sawyer (0.7.0) + addressable (>= 2.3.5, < 2.5) faraday (~> 0.8, < 0.10) scss_lint (0.47.1) rake (>= 0.9, < 11) @@ -769,10 +771,10 @@ GEM spinach (>= 0.4) spinach-rerun-reporter (0.0.2) spinach (~> 0.8) - spring (1.6.4) + spring (1.7.1) spring-commands-rspec (1.0.4) spring (>= 0.9.1) - spring-commands-spinach (1.0.0) + spring-commands-spinach (1.1.0) spring (>= 0.9.1) spring-commands-teaspoon (0.0.2) spring (>= 0.9.1) @@ -957,6 +959,7 @@ DEPENDENCIES jquery-ui-rails (~> 5.0.0) kaminari (~> 0.16.3) letter_opener (~> 1.1.2) + licensee (~> 8.0.0) loofah (~> 2.0.3) mail_room (~> 0.6.1) method_source (~> 0.8) @@ -968,7 +971,7 @@ DEPENDENCIES newrelic_rpm (~> 3.14) nokogiri (~> 1.6.7, >= 1.6.7.2) oauth2 (~> 1.0.0) - octokit (~> 3.8.0) + octokit (~> 4.3.0) omniauth (~> 1.3.1) omniauth-auth0 (~> 1.4.1) omniauth-azure-oauth2 (~> 0.0.6) @@ -1030,9 +1033,9 @@ DEPENDENCIES slack-notifier (~> 1.2.0) spinach-rails (~> 0.2.1) spinach-rerun-reporter (~> 0.0.2) - spring (~> 1.6.4) + spring (~> 1.7.0) spring-commands-rspec (~> 1.0.4) - spring-commands-spinach (~> 1.0.0) + spring-commands-spinach (~> 1.1.0) spring-commands-teaspoon (~> 0.0.2) sprockets (~> 3.6.0) state_machines-activerecord (~> 0.3.0) diff --git a/PROCESS.md b/PROCESS.md index cad45d23df9fe2808e77845b3ea526db75a342d8..e34f59c6bce1c5cd881359ad4cccd3f66cfa0604 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -105,6 +105,25 @@ sensitive as to how you word things. Use Emoji to express your feelings (heart, star, smile, etc.). Some good tips about giving feedback to merge requests is in the [Thoughtbot code review guide]. +## Feature Freeze + +5 working days before the 22nd the stable branches for the upcoming release will +be frozen for major changes. Merge requests may still be merged into master +during this period. By freezing the stable branches prior to a release there's +no need to worry about last minute merge requests potentially breaking a lot of +things. + +What is considered to be a major change is determined on a case by case basis as +this definition depends very much on the context of changes. For example, a 5 +line change might have a big impact on the entire application. Ultimately the +decision will be made by those reviewing a merge request and the release +manager. + +During the feature freeze all merge requests that are meant to go into the next +release should have the correct milestone assigned _and_ have the label +~"Pick into Stable" set. Merge requests without a milestone and this label will +not be merged into any stable branches. + ## Copy & paste responses ### Improperly formatted issue diff --git a/VERSION b/VERSION index 91ab1f99daf48de9fb27d5b76b81571cf2ec2ff4..d5a967c393326859dd7cc33c619e2da19a26bbc8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.7.0-pre +8.8.0-pre diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index f3ed9a66715fe40588441ec74caadb2fcb52738a..dd1bbb37551e805b43c4e0700a04a498169edc2a 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -5,6 +5,7 @@ group_projects_path: "/api/:version/groups/:id/projects.json" projects_path: "/api/:version/projects.json" labels_path: "/api/:version/projects/:id/labels" + license_path: "/api/:version/licenses/:key" group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) @@ -92,6 +93,16 @@ ).done (projects) -> callback(projects) + # Return text for a specific license + licenseText: (key, data, callback) -> + url = Api.buildUrl(Api.license_path).replace(':key', key) + + $.ajax( + url: url + data: data + ).done (license) -> + callback(license) + buildUrl: (url) -> url = gon.relative_url_root + url if gon.relative_url_root? return url.replace(':version', gon.api_version) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index f01c67e9474fa066dc7a3b004c880ce3c8dc328e..5bac8eef1cb61ef087f266e1d4f044c54fe344e7 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -22,7 +22,17 @@ #= require cal-heatmap #= require turbolinks #= require autosave -#= require bootstrap +#= require bootstrap/affix +#= require bootstrap/alert +#= require bootstrap/button +#= require bootstrap/collapse +#= require bootstrap/dropdown +#= require bootstrap/modal +#= require bootstrap/scrollspy +#= require bootstrap/tab +#= require bootstrap/transition +#= require bootstrap/tooltip +#= require bootstrap/popover #= require select2 #= require raphael #= require g.raphael @@ -41,6 +51,7 @@ #= require shortcuts_issuable #= require shortcuts_network #= require jquery.nicescroll +#= require date.format #= require_tree . #= require fuzzaldrin-plus #= require cropper @@ -163,7 +174,7 @@ $ -> $('.trigger-submit').on 'change', -> $(@).parents('form').submit() - $('abbr.timeago, .js-timeago').timeago() + gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true) # Flash if (flash = $(".flash-container")).length > 0 diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee index 6e29d374267098f5ddb685ca8a42812d06aa6f33..3cb96bacaa741f2c8a5d62496a0f7f8d2dab78ad 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js.coffee +++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee @@ -29,7 +29,11 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> e.preventDefault() $form = $(e.target).closest('form') - $form.find('input[type=submit], button[type=submit]').disable() + $submit_button = $form.find('input[type=submit], button[type=submit]') + + return if $submit_button.attr('disabled') + + $submit_button.disable() $form.submit() # If the user tabs to a submit button on a `js-quick-submit` form, display a diff --git a/app/assets/javascripts/blob/blob_license_selector.js.coffee b/app/assets/javascripts/blob/blob_license_selector.js.coffee new file mode 100644 index 0000000000000000000000000000000000000000..e17eaa75dc11274c455593bd77e0887de39165d8 --- /dev/null +++ b/app/assets/javascripts/blob/blob_license_selector.js.coffee @@ -0,0 +1,30 @@ +class @BlobLicenseSelector + licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i + + constructor: (editor) -> + @$licenseSelector = $('.js-license-selector') + $fileNameInput = $('#file_name') + + initialFileNameValue = if $fileNameInput.length + $fileNameInput.val() + else if $('.editor-file-name').length + $('.editor-file-name').text().trim() + + @toggleLicenseSelector(initialFileNameValue) + + if $fileNameInput + $fileNameInput.on 'keyup blur', (e) => + @toggleLicenseSelector($(e.target).val()) + + $('select.license-select').on 'change', (e) -> + data = + project: $(this).data('project') + fullname: $(this).data('fullname') + Api.licenseText $(this).val(), data, (license) -> + editor.setValue(license.content, -1) + + toggleLicenseSelector: (fileName) => + if @licenseRegex.test(fileName) + @$licenseSelector.show() + else + @$licenseSelector.hide() diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index 390e41ed8d4ca56ee8bfb9b2ad16e724e2cbe7e9..eea9aa972ee33715cf217579640065095300462b 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -1,44 +1,39 @@ class @EditBlob - constructor: (assets_path, mode)-> - ace.config.set "modePath", assets_path + '/ace' + constructor: (assets_path, ace_mode = null) -> + ace.config.set "modePath", "#{assets_path}/ace" ace.config.loadModule "ace/ext/searchbox" - if mode - ace_mode = mode - editor = ace.edit("editor") - editor.focus() - @editor = editor - - if ace_mode - editor.getSession().setMode "ace/mode/" + ace_mode + @editor = ace.edit("editor") + @editor.focus() + @editor.getSession().setMode "ace/mode/#{ace_mode}" if ace_mode # Before a form submission, move the content from the Ace editor into the # submitted textarea - $('form').submit -> - $("#file-content").val(editor.getValue()) + $('form').submit => + $("#file-content").val(@editor.getValue()) + + @initModePanesAndLinks() + new BlobLicenseSelector(@editor) - editModePanes = $(".js-edit-mode-pane") - editModeLinks = $(".js-edit-mode a") - editModeLinks.click (event) -> - event.preventDefault() - currentLink = $(this) - paneId = currentLink.attr("href") - currentPane = editModePanes.filter(paneId) - editModeLinks.parent().removeClass "active hover" - currentLink.parent().addClass "active hover" - editModePanes.hide() - if paneId is "#preview" - currentPane.fadeIn 200 - $.post currentLink.data("preview-url"), - content: editor.getValue() - , (response) -> - currentPane.empty().append response - currentPane.syntaxHighlight() - return + initModePanesAndLinks: -> + @$editModePanes = $(".js-edit-mode-pane") + @$editModeLinks = $(".js-edit-mode a") + @$editModeLinks.click @editModeLinkClickHandler - else - currentPane.fadeIn 200 - editor.focus() - return + editModeLinkClickHandler: (event) => + event.preventDefault() + currentLink = $(event.target) + paneId = currentLink.attr("href") + currentPane = @$editModePanes.filter(paneId) + @$editModeLinks.parent().removeClass "active hover" + currentLink.parent().addClass "active hover" + @$editModePanes.hide() + currentPane.fadeIn 200 + if paneId is "#preview" + $.post currentLink.data("preview-url"), + content: @editor.getValue() + , (response) -> + currentPane.empty().append response + currentPane.syntaxHighlight() - editor: -> - return @editor + else + @editor.focus() diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee deleted file mode 100644 index 68c5e5195e396b5ef3a38ce187be52535037133c..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/blob/new_blob.js.coffee +++ /dev/null @@ -1,20 +0,0 @@ -class @NewBlob - constructor: (assets_path, mode)-> - ace.config.set "modePath", assets_path + '/ace' - ace.config.loadModule "ace/ext/searchbox" - if mode - ace_mode = mode - editor = ace.edit("editor") - editor.focus() - @editor = editor - - if ace_mode - editor.getSession().setMode "ace/mode/" + ace_mode - - # Before a form submission, move the content from the Ace editor into the - # submitted textarea - $('form').submit -> - $("#file-content").val(editor.getValue()) - - editor: -> - return @editor diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 70fd6f50e9c7e86d2b9ed347689273d2ba68e0e0..2fdb7562515e248528b9259efdec966ab7d24a7d 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -17,6 +17,7 @@ class Dispatcher switch page when 'projects:issues:index' Issues.init() + Issuable.init() shortcut_handler = new ShortcutsNavigation() when 'projects:issues:show' new Issue() @@ -28,26 +29,26 @@ class Dispatcher new Todos() when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() - new DropzoneInput($('.milestone-form')) + new GLForm($('.milestone-form')) when 'groups:milestones:new' new ZenMode() when 'projects:compare:show' new Diff() when 'projects:issues:new','projects:issues:edit' shortcut_handler = new ShortcutsNavigation() - new DropzoneInput($('.issue-form')) + new GLForm($('.issue-form')) new IssuableForm($('.issue-form')) when 'projects:merge_requests:new', 'projects:merge_requests:edit' new Diff() shortcut_handler = new ShortcutsNavigation() - new DropzoneInput($('.merge-request-form')) + new GLForm($('.merge-request-form')) new IssuableForm($('.merge-request-form')) when 'projects:tags:new' new ZenMode() - new DropzoneInput($('.tag-form')) + new GLForm($('.tag-form')) when 'projects:releases:edit' new ZenMode() - new DropzoneInput($('.release-form')) + new GLForm($('.release-form')) when 'projects:merge_requests:show' new Diff() shortcut_handler = new ShortcutsIssuable(true) @@ -57,7 +58,7 @@ class Dispatcher new ZenMode() when 'projects:merge_requests:index' shortcut_handler = new ShortcutsNavigation() - MergeRequests.init() + Issuable.init() when 'dashboard:activity' new Activities() when 'dashboard:projects:starred' @@ -137,7 +138,7 @@ class Dispatcher new Wikis() shortcut_handler = new ShortcutsNavigation() new ZenMode() - new DropzoneInput($('.wiki-form')) + new GLForm($('.wiki-form')) when 'snippets' shortcut_handler = new ShortcutsNavigation() new ZenMode() if path[2] == 'show' diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index b502131a99d07e82cd54a571aaf1705d0fb6ade0..e2194589b38b68325ecbc546f6913db907654dd7 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -15,11 +15,13 @@ class @DropzoneInput project_uploads_path = window.project_uploads_path or null max_file_size = gon.max_file_size or 10 - form_textarea = $(form).find("textarea.markdown-area") + form_textarea = $(form).find(".js-gfm-input") form_textarea.wrap "<div class=\"div-dropzone\"></div>" form_textarea.on 'paste', (event) => handlePaste(event) + $mdArea = $(form_textarea).closest('.md-area') + $(form).setupMarkdownPreview() form_dropzone = $(form).find('.div-dropzone') @@ -49,17 +51,17 @@ class @DropzoneInput $(".div-dropzone-alert").alert "close" dragover: -> - form_textarea.addClass "div-dropzone-focus" + $mdArea.addClass 'is-dropzone-hover' form.find(".div-dropzone-hover").css "opacity", 0.7 return dragleave: -> - form_textarea.removeClass "div-dropzone-focus" + $mdArea.removeClass 'is-dropzone-hover' form.find(".div-dropzone-hover").css "opacity", 0 return drop: -> - form_textarea.removeClass "div-dropzone-focus" + $mdArea.removeClass 'is-dropzone-hover' form.find(".div-dropzone-hover").css "opacity", 0 form_textarea.focus() return diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee new file mode 100644 index 0000000000000000000000000000000000000000..a4304786cbb6de3f6f8bb6aa47e7b013e91ddef4 --- /dev/null +++ b/app/assets/javascripts/due_date_select.js.coffee @@ -0,0 +1,64 @@ +class @DueDateSelect + constructor: -> + $loading = $('.js-issuable-update .due_date') + .find('.block-loading') + .hide() + + $('.js-due-date-select').each (i, dropdown) -> + $dropdown = $(dropdown) + $dropdownParent = $dropdown.closest('.dropdown') + $datePicker = $dropdownParent.find('.js-due-date-calendar') + $block = $dropdown.closest('.block') + $selectbox = $dropdown.closest('.selectbox') + $value = $block.find('.value') + $sidebarValue = $('.js-due-date-sidebar-value', $block) + + fieldName = $dropdown.data('field-name') + abilityName = $dropdown.data('ability-name') + issueUpdateURL = $dropdown.data('issue-update') + + $dropdown.glDropdown( + hidden: -> + $selectbox.hide() + $value.removeAttr('style') + ) + + addDueDate = -> + # Create the post date + value = $("input[name='#{fieldName}']").val() + date = new Date value.replace(new RegExp('-', 'g'), ',') + mediumDate = $.datepicker.formatDate 'M d, yy', date + + data = {} + data[abilityName] = {} + data[abilityName].due_date = value + + $.ajax( + type: 'PUT' + url: issueUpdateURL + data: data + beforeSend: -> + $loading.fadeIn() + $dropdown.trigger('loading.gl.dropdown') + $selectbox.hide() + $value.removeAttr('style') + + $value.html(mediumDate) + $sidebarValue.html(mediumDate) + ).done (data) -> + $dropdown.trigger('loaded.gl.dropdown') + $dropdown.dropdown('toggle') + $loading.fadeOut() + + $datePicker.datepicker( + dateFormat: 'yy-mm-dd', + defaultDate: $("input[name='#{fieldName}']").val() + altField: "input[name='#{fieldName}']" + onSelect: -> + addDueDate() + ) + + $(document) + .off 'click', '.ui-datepicker-header a' + .on 'click', '.ui-datepicker-header a', (e) -> + e.stopImmediatePropagation() diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 4718bcf7a1eac6eaa0d62b39ff1e863810714a16..61e3f811e73039e5ee8a4dc6d5cf4a8ef279c8ab 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -2,6 +2,8 @@ window.GitLab ?= {} GitLab.GfmAutoComplete = + dataLoading: false + dataSource: '' # Emoji @@ -17,17 +19,41 @@ GitLab.GfmAutoComplete = template: '<li><small>${id}</small> ${title}</li>' # Add GFM auto-completion to all input fields, that accept GFM input. - setup: -> - input = $('.js-gfm-input') + setup: (wrap) -> + @input = $('.js-gfm-input') + + # destroy previous instances + @destroyAtWho() + + # set up instances + @setupAtWho() + + if @dataSource + if !@dataLoading + @dataLoading = true + # We should wait until initializations are done + # and only trigger the last .setup since + # The previous .dataSource belongs to the previous issuable + # and the last one will have the **proper** .dataSource property + # TODO: Make this a singleton and turn off events when moving to another page + setTimeout( => + fetch = @fetchData(@dataSource) + fetch.done (data) => + @dataLoading = false + @loadData(data) + , 1000) + + + setupAtWho: -> # Emoji - input.atwho + @input.atwho at: ':' displayTpl: @Emoji.template insertTpl: ':${name}:' # Team Members - input.atwho + @input.atwho at: '@' displayTpl: @Members.template insertTpl: '${atwho-at}${username}' @@ -42,7 +68,7 @@ GitLab.GfmAutoComplete = title: sanitize(title) search: sanitize("#{m.username} #{m.name}") - input.atwho + @input.atwho at: '#' alias: 'issues' searchKey: 'search' @@ -55,7 +81,7 @@ GitLab.GfmAutoComplete = title: sanitize(i.title) search: "#{i.iid} #{i.title}" - input.atwho + @input.atwho at: '!' alias: 'mergerequests' searchKey: 'search' @@ -68,13 +94,18 @@ GitLab.GfmAutoComplete = title: sanitize(m.title) search: "#{m.iid} #{m.title}" - if @dataSource - $.getJSON(@dataSource).done (data) -> - # load members - input.atwho 'load', '@', data.members - # load issues - input.atwho 'load', 'issues', data.issues - # load merge requests - input.atwho 'load', 'mergerequests', data.mergerequests - # load emojis - input.atwho 'load', ':', data.emojis + destroyAtWho: -> + @input.atwho('destroy') + + fetchData: (dataSource) -> + $.getJSON(dataSource) + + loadData: (data) -> + # load members + @input.atwho 'load', '@', data.members + # load issues + @input.atwho 'load', 'issues', data.issues + # load merge requests + @input.atwho 'load', 'mergerequests', data.mergerequests + # load emojis + @input.atwho 'load', ':', data.emojis diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index ee1d0fad28910531be2e57b1f5e27b528bb842af..e5204f9dee970ab7c60ba3f8c500173e2fae3fc0 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -32,10 +32,8 @@ class GitLabDropdownFilter else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS $inputContainer.removeClass HAS_VALUE_CLASS - if keyCode is 13 and @input.val() isnt "" - if @options.enterCallback - @options.enterCallback() - return + if keyCode is 13 + return false clearTimeout timeout timeout = setTimeout => @@ -122,7 +120,9 @@ class GitLabDropdown FILTER_INPUT = '.dropdown-input .dropdown-input-field' constructor: (@el, @options) -> - @dropdown = $(@el).parent() + self = @ + selector = $(@el).data "target" + @dropdown = if selector? then $(selector) else $(@el).parent() # Set Defaults { @@ -130,7 +130,6 @@ class GitLabDropdown @filterInput = @getElement(FILTER_INPUT) @highlight = false @filterInputBlur = true - @enterCallback = true } = @options self = @ @@ -155,6 +154,9 @@ class GitLabDropdown @fullData = data @parseData @fullData + + if @options.filterable + @filterInput.trigger 'keyup' } # Init filterable @@ -176,9 +178,6 @@ class GitLabDropdown callback: (data) => currentIndex = -1 @parseData data - enterCallback: => - if @enterCallback - @selectRowAtIndex 0 # Event listeners @@ -387,13 +386,13 @@ class GitLabDropdown else selectedObject else - if !value? - field.remove() - - if not @options.multiSelect + if not @options.multiSelect or el.hasClass('dropdown-clear-active') @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @dropdown.parent().find("input[name='#{fieldName}']").remove() + if !value? + field.remove() + # Toggle active class for the tick mark el.addClass ACTIVE_CLASS diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee new file mode 100644 index 0000000000000000000000000000000000000000..d540cc4dc467f1802055bc5a32d06267acd4181d --- /dev/null +++ b/app/assets/javascripts/gl_form.js.coffee @@ -0,0 +1,51 @@ +class @GLForm + constructor: (@form) -> + @textarea = @form.find('textarea.js-gfm-input') + + # Before we start, we should clean up any previous data for this form + @destroy() + + # Setup the form + @setupForm() + + @form.data 'gl-form', @ + + destroy: -> + # Clean form listeners + @clearEventListeners() + @form.data 'gl-form', null + + setupForm: -> + isNewForm = @form.is(':not(.gfm-form)') + + @form.removeClass 'js-new-note-form' + + if isNewForm + @form.find('.div-dropzone').remove() + @form.addClass('gfm-form') + disableButtonIfEmptyField @form.find('.js-note-text'), @form.find('.js-comment-button') + + # remove notify commit author checkbox for non-commit notes + GitLab.GfmAutoComplete.setup() + new DropzoneInput(@form) + + autosize(@textarea) + + # form and textarea event listeners + @addEventListeners() + + # hide discard button + @form.find('.js-note-discard').hide() + + @form.show() + + clearEventListeners: -> + @textarea.off 'focus' + @textarea.off 'blur' + + addEventListeners: -> + @textarea.on 'focus', -> + $(@).closest('.md-area').addClass 'is-focused' + + @textarea.on 'blur', -> + $(@).closest('.md-area').removeClass 'is-focused' diff --git a/app/assets/javascripts/importer_status.js.coffee b/app/assets/javascripts/importer_status.js.coffee index be8d225e73b94ecd0b9a40c7a263461aae60055e..b0edc8956498d17e5b2733e88021b4b3136d3e8e 100644 --- a/app/assets/javascripts/importer_status.js.coffee +++ b/app/assets/javascripts/importer_status.js.coffee @@ -4,18 +4,33 @@ class @ImporterStatus this.setAutoUpdate() initStatusPage: -> - $(".js-add-to-import").click (event) => - new_namespace = null - tr = $(event.currentTarget).closest("tr") - id = tr.attr("id").replace("repo_", "") - if tr.find(".import-target input").length > 0 - new_namespace = tr.find(".import-target input").prop("value") - tr.find(".import-target").empty().append(new_namespace + "/" + tr.find(".import-target").data("project_name")) - $.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script' - - $(".js-import-all").click (event) => - $(".js-add-to-import").each -> - $(this).click() + $('.js-add-to-import') + .off 'click' + .on 'click', (e) => + new_namespace = null + $btn = $(e.currentTarget) + $tr = $btn.closest('tr') + id = $tr.attr('id').replace('repo_', '') + if $tr.find('.import-target input').length > 0 + new_namespace = $tr.find('.import-target input').prop('value') + $tr.find('.import-target').empty().append("#{new_namespace} / #{$tr.find('.import-target').data('project_name')}") + + $btn + .disable() + .addClass 'is-loading' + + $.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script' + + $('.js-import-all') + .off 'click' + .on 'click', (e) -> + $btn = $(@) + $btn + .disable() + .addClass 'is-loading' + + $('.js-add-to-import').each -> + $(this).trigger('click') setAutoUpdate: -> setInterval (=> diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee new file mode 100644 index 0000000000000000000000000000000000000000..afffed63ac57ec9c916111e7d9853040cbf18d46 --- /dev/null +++ b/app/assets/javascripts/issuable.js.coffee @@ -0,0 +1,84 @@ +@Issuable = + init: -> + Issuable.initTemplates() + Issuable.initSearch() + + initTemplates: -> + Issuable.labelRow = _.template( + '<% _.each(labels, function(label){ %> + <span class="label-row"> + <a href="#"><span class="label color-label has-tooltip" style="background-color: <%= label.color %>; color: <%= label.text_color %>" title="<%= _.escape(label.description) %>" data-container="body"><%= _.escape(label.title) %></span></a> + </span> + <% }); %>' + ) + + initSearch: -> + @timer = null + $('#issue_search') + .off 'keyup' + .on 'keyup', -> + clearTimeout(@timer) + @timer = setTimeout( -> + Issuable.filterResults $('#issue_search_form') + , 500) + + toggleLabelFilters: -> + $filteredLabels = $('.filtered-labels') + if $filteredLabels.find('.label-row').length > 0 + $filteredLabels.removeClass('hidden') + else + $filteredLabels.addClass('hidden') + + filterResults: (form) => + formData = form.serialize() + + $('.issues-holder, .merge-requests-holder').css('opacity', '0.5') + formAction = form.attr('action') + issuesUrl = formAction + issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}") + issuesUrl += formData + $.ajax + type: 'GET' + url: formAction + data: formData + complete: -> + $('.issues-holder, .merge-requests-holder').css('opacity', '1.0') + success: (data) -> + $('.issues-holder, .merge-requests-holder').html(data.html) + # Change url so if user reload a page - search results are saved + history.replaceState {page: issuesUrl}, document.title, issuesUrl + Issuable.reload() + Issuable.updateStateFilters() + $filteredLabels = $('.filtered-labels') + + if typeof Issuable.labelRow is 'function' + $filteredLabels.html(Issuable.labelRow(data)) + + Issuable.toggleLabelFilters() + + dataType: "json" + + reload: -> + if Issues.created + Issues.initChecks() + + $('#filter_issue_search').val($('#issue_search').val()) + + updateStateFilters: -> + stateFilters = $('.issues-state-filters') + newParams = {} + paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search'] + + for paramKey in paramKeys + newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or '' + + if stateFilters.length + stateFilters.find('a').each -> + initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]') + labelNameValues = gl.utils.getParameterValues('label_name[]') + if labelNameValues + labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&') + newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}" + else + newUrl = gl.utils.mergeUrlParams(newParams, initialUrl) + $(this).attr 'href', newUrl diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index 2f19513a831fd994c5a2091733054f871be8dc2d..3a439b94c599a4bcbd1b3304bfec69e21708158b 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -9,21 +9,29 @@ class @IssuableContext $(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> $(this).submit() - $(document).off("click", ".edit-link").on "click",".edit-link", (e) -> - $block = $(@).parents('.block') - $selectbox = $block.find('.selectbox') - if $selectbox.is(':visible') - $selectbox.hide() - $block.find('.value').show() - else - $selectbox.show() - $block.find('.value').hide() - - if $selectbox.is(':visible') - setTimeout (-> - $block.find('.dropdown-menu-toggle').trigger 'click' - ), 0 - + $(document) + .off 'click', '.dropdown-content a' + .on 'click', '.dropdown-content a', (e) -> + e.preventDefault() + + $(document) + .off 'click', '.edit-link' + .on 'click', '.edit-link', (e) -> + e.preventDefault() + + $block = $(@).parents('.block') + $selectbox = $block.find('.selectbox') + if $selectbox.is(':visible') + $selectbox.hide() + $block.find('.value').show() + else + $selectbox.show() + $block.find('.value').hide() + + if $selectbox.is(':visible') + setTimeout -> + $block.find('.dropdown-menu-toggle').trigger 'click' + , 0 $(".right-sidebar").niceScroll() diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 946d83b7bdd271e1e32d315120793b3662bdb110..c7d74a12f9929b5e58228df96cc9f0c0eb318ea9 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -10,6 +10,9 @@ class @Issue @initTaskList() @initIssueBtnEventListeners() + @initMergeRequests() + @initRelatedBranches() + initTaskList: -> $('.detail-page-description .js-task-list-container').taskList('enable') $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList @@ -69,3 +72,23 @@ class @Issue type: 'PATCH' url: $('form.js-issuable-update').attr('action') data: patchData + + initMergeRequests: -> + $container = $('#merge-requests') + + $.getJSON($container.data('url')) + .error -> + new Flash('Failed to load referenced merge requests', 'alert') + .success (data) -> + if 'html' of data + $container.html(data.html) + + initRelatedBranches: -> + $container = $('#related-branches') + + $.getJSON($container.data('url')) + .error -> + new Flash('Failed to load related branches', 'alert') + .success (data) -> + if 'html' of data + $container.html(data.html) diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 0d9f2094c2a6d56d0c245de319222f4883eff822..3330e6c68aded1a471a45bbb9be51315e39db664 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -1,6 +1,6 @@ @Issues = init: -> - Issues.initSearch() + Issues.created = true Issues.initChecks() $("body").on "ajax:success", ".close_issue, .reopen_issue", -> @@ -15,10 +15,6 @@ else $(this).html totalIssues - 1 - reload: -> - Issues.initChecks() - $('#filter_issue_search').val($('#issue_search').val()) - initChecks: -> $(".check_all_issues").click -> $(".selected_issue").prop("checked", @checked) @@ -26,51 +22,6 @@ $(".selected_issue").bind "change", Issues.checkChanged - # Update state filters if present in page - updateStateFilters: -> - stateFilters = $('.issues-state-filters') - newParams = {} - paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search'] - - for paramKey in paramKeys - newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or '' - - if stateFilters.length - stateFilters.find('a').each -> - initialUrl = $(this).attr 'href' - $(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl) - - # Make sure we trigger ajax request only after user stop typing - initSearch: -> - @timer = null - $("#issue_search").keyup -> - clearTimeout(@timer) - @timer = setTimeout( -> - Issues.filterResults $("#issue_search_form") - , 500) - - filterResults: (form) => - $('.issues-holder, .merge-requests-holder').css("opacity", '0.5') - formAction = form.attr('action') - formData = form.serialize() - issuesUrl = formAction - issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}") - issuesUrl += formData - - $.ajax - type: "GET" - url: formAction - data: formData - complete: -> - $('.issues-holder, .merge-requests-holder').css("opacity", '1.0') - success: (data) -> - $('.issues-holder, .merge-requests-holder').html(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: issuesUrl}, document.title, issuesUrl - Issues.reload() - Issues.updateStateFilters() - dataType: "json" - checkChanged: -> checked_issues = $(".selected_issue:checked") if checked_issues.length > 0 diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index d1fe116397a6b53e278ab5c625107099a856ba1c..021ade73d445b5102439012395fa739801d5fffb 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -6,7 +6,7 @@ class @LabelsSelect labelUrl = $dropdown.data('labels') issueUpdateURL = $dropdown.data('issueUpdate') selectedLabel = $dropdown.data('selected') - if selectedLabel? + if selectedLabel? and not $dropdown.hasClass 'js-multiselect' selectedLabel = selectedLabel.split(',') newLabelField = $('#new_label_name') newColorField = $('#new_label_color') @@ -16,6 +16,7 @@ class @LabelsSelect abilityName = $dropdown.data('ability-name') $selectbox = $dropdown.closest('.selectbox') $block = $selectbox.closest('.block') + $form = $dropdown.closest('form') $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span') $value = $block.find('.value') $loading = $block.find('.block-loading').fadeOut() @@ -33,13 +34,13 @@ class @LabelsSelect if issueUpdateURL labelHTMLTemplate = _.template( '<% _.each(labels, function(label){ %> - <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>"> - <span class="label color-label" style="background-color: <%= label.color %>;"> - <%= label.title %> + <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= _.escape(label.title) %>"> + <span class="label has-tooltip color-label" title="<%= _.escape(label.description) %>" style="background-color: <%= label.color %>;"> + <%= _.escape(label.title) %> </span> </a> <% }); %>' - ); + ) labelNoneHTMLTemplate = _.template('<div class="light">None</div>') if newLabelField.length and $dropdown.hasClass 'js-extra-options' @@ -165,11 +166,13 @@ class @LabelsSelect .html(template) $sidebarCollapsedValue.text(labelCount) + $('.has-tooltip', $value).tooltip(container: 'body') + $value .find('a') .each((i) -> setTimeout(=> - glAnimate($(@), 'pulse') + gl.animate.animate($(@), 'pulse') ,200 * i ) ) @@ -198,18 +201,23 @@ class @LabelsSelect callback data renderRow: (label) -> - selectedClass = '' - if $selectbox.find("input[type='hidden']\ - [name='#{$dropdown.data('field-name')}']\ - [value='#{label.id}']").length - selectedClass = 'is-active' + removesAll = label.id is 0 or not label.id? + + selectedClass = [] + if $form.find("input[type='hidden']\ + [name='#{$dropdown.data('fieldName')}']\ + [value='#{this.id(label)}']").length + selectedClass.push 'is-active' + + if $dropdown.hasClass('js-multiselect') and removesAll + selectedClass.push 'dropdown-clear-active' color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else "" "<li> - <a href='#' class='#{selectedClass}'> + <a href='#' class='#{selectedClass.join(' ')}'> #{color} - #{label.title} + #{_.escape(label.title)} </a> </li>" filterable: true @@ -217,37 +225,56 @@ class @LabelsSelect fields: ['title'] selectable: true - toggleLabel: (selected) -> - if selected and selected.title isnt 'Any Label' - selected.title + toggleLabel: (selected, el) -> + selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active') + + if selected and selected.title? + if selected_labels.length > 1 + "#{selected.title} +#{selected_labels.length - 1} more" + else + selected.title + else if not selected and selected_labels.length isnt 0 + if selected_labels.length > 1 + "#{$(selected_labels[0]).text()} +#{selected_labels.length - 1} more" + else if selected_labels.length is 1 + $(selected_labels).text() else defaultLabel fieldName: $dropdown.data('field-name') id: (label) -> - if label.isAny? - '' - else if $dropdown.hasClass "js-filter-submit" + if $dropdown.hasClass("js-filter-submit") and not label.isAny? label.title else label.id hidden: -> + page = $('body').data 'page' + isIssueIndex = page is 'projects:issues:index' + isMRIndex = page is 'projects:merge_requests:index' + $selectbox.hide() # display:block overrides the hide-collapse rule $value.removeAttr('style') if $dropdown.hasClass 'js-multiselect' - saveLabelData() + if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + selectedLabels = $dropdown + .closest('form') + .find("input:hidden[name='#{$dropdown.data('fieldName')}']") + Issuable.filterResults $dropdown.closest('form') + else if $dropdown.hasClass('js-filter-submit') + $dropdown.closest('form').submit() + else + saveLabelData() multiSelect: $dropdown.hasClass 'js-multiselect' clicked: (label) -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is page is 'projects:merge_requests:index' - + isMRIndex = page is 'projects:merge_requests:index' if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - selectedLabel = label.title - - Issues.filterResults $dropdown.closest('form') + if not $dropdown.hasClass 'js-multiselect' + selectedLabel = label.title + Issuable.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() else diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/animate.js.coffee index 8f892b5a2b9cc4abc0398cd7ccc33aeaefa63f1b..ec3b44d61263374ce7e5d8b5f01535cd0afbc3bd 100644 --- a/app/assets/javascripts/lib/animate.js.coffee +++ b/app/assets/javascripts/lib/animate.js.coffee @@ -1,13 +1,39 @@ ((w) -> + if not w.gl? then w.gl = {} + if not gl.animate? then gl.animate = {} - w.glAnimate = ($el, animation, done) -> + gl.animate.animate = ($el, animation, options, done) -> + if options?.cssStart? + $el.css(options.cssStart) $el - .removeClass() + .removeClass(animation + ' animated') .addClass(animation + ' animated') .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> - $(this).removeClass() + $(this).removeClass(animation + ' animated') + if done? + done() + if options?.cssEnd? + $el.css(options.cssEnd) return return - return + gl.animate.animateEach = ($els, animation, time, options, done) -> + dfd = $.Deferred() + if not $els.length + dfd.resolve() + $els.each((i) -> + setTimeout(=> + $this = $(@) + gl.animate.animate($this, animation, options, => + if i is $els.length - 1 + dfd.resolve() + if done? + done() + ) + ,time * i + ) + return + ) + return dfd.promise() + return ) window \ No newline at end of file diff --git a/app/assets/javascripts/lib/datetime_utility.js.coffee b/app/assets/javascripts/lib/datetime_utility.js.coffee new file mode 100644 index 0000000000000000000000000000000000000000..ad1d1c704819130185f350661ee8b7e2ce1002be --- /dev/null +++ b/app/assets/javascripts/lib/datetime_utility.js.coffee @@ -0,0 +1,17 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + w.gl.utils.formatDate = (datetime) -> + dateFormat(datetime, 'mmm d, yyyy h:MMtt Z') + + w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) -> + $timeagoEls.each( -> + $el = $(@) + $el.attr('title', gl.utils.formatDate($el.attr('datetime'))) + ) + + $timeagoEls.timeago() if setTimeago + +) window diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee index abd556e0b4e07eb20f77d242cc98a459267f74d1..6a00932c028ff23577a13f51c89efe348b7d3491 100644 --- a/app/assets/javascripts/lib/url_utility.js.coffee +++ b/app/assets/javascripts/lib/url_utility.js.coffee @@ -3,16 +3,20 @@ w.gl ?= {} w.gl.utils ?= {} - w.gl.utils.getUrlParameter = (sParam) -> + # Returns an array containing the value(s) of the + # of the key passed as an argument + w.gl.utils.getParameterValues = (sParam) -> sPageURL = decodeURIComponent(window.location.search.substring(1)) sURLVariables = sPageURL.split('&') sParameterName = undefined + values = [] i = 0 while i < sURLVariables.length sParameterName = sURLVariables[i].split('=') if sParameterName[0] is sParam - return if sParameterName[1] is undefined then true else sParameterName[1] + values.push(sParameterName[1]) i++ + values # # # @param {Object} params - url keys and value to merge @@ -28,4 +32,12 @@ newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" newUrl + # removes parameter query string from url. returns the modified url + w.gl.utils.removeParamQueryString = (url, param) -> + url = decodeURIComponent(url) + urlVariables = url.split('&') + ( + variables for variables in urlVariables when variables.indexOf(param) is -1 + ).join('&') + ) window diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 9946249adbfed147a954d6f295ebfd5c46211897..e8613cab72b039196fd4fb09f44a0cff40d49e08 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -85,8 +85,10 @@ class @MergeRequestTabs scrollToElement: (container) -> if window.location.hash - $el = $("div#{container} #{window.location.hash}") - $('body').scrollTo($el.offset().top) if $el.length + navBarHeight = $('.navbar-gitlab').outerHeight() + + $el = $("#{container} #{window.location.hash}") + $.scrollTo("#{container} #{window.location.hash}", offset: -navBarHeight) if $el.length # Activate a tab based on the current action activateTab: (action) -> @@ -142,7 +144,7 @@ class @MergeRequestTabs url: "#{source}.json" success: (data) => document.querySelector("div#commits").innerHTML = data.html - $('.js-timeago').timeago() + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')) @commitsLoaded = true @scrollToElement("#commits") @@ -152,12 +154,39 @@ class @MergeRequestTabs @_get url: "#{source}.json" + @_location.search success: (data) => - document.querySelector("div#diffs").innerHTML = data.html - $('.js-timeago').timeago() - $('div#diffs .js-syntax-highlight').syntaxHighlight() + $('#diffs').html data.html + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) + $('#diffs .js-syntax-highlight').syntaxHighlight() @expandViewContainer() if @diffViewType() is 'parallel' @diffsLoaded = true @scrollToElement("#diffs") + @highlighSelectedLine() + + $(document) + .off 'click', '.diff-line-num a' + .on 'click', '.diff-line-num a', (e) => + e.preventDefault() + window.location.hash = $(e.currentTarget).attr 'href' + @highlighSelectedLine() + @scrollToElement("#diffs") + + highlighSelectedLine: -> + $('.hll').removeClass 'hll' + locationHash = window.location.hash + + if locationHash isnt '' + hashClassString = ".#{locationHash.replace('#', '')}" + $diffLine = $(locationHash) + + if $diffLine.is ':not(tr)' + $diffLine = $("td#{locationHash}, td#{hashClassString}") + else + $diffLine = $('td', $diffLine) + + if $diffLine.length + $diffLine.addClass 'hll' + diffLineTop = $diffLine.offset().top + navBarHeight = $('.navbar-gitlab').outerHeight() loadBuilds: (source) -> return if @buildsLoaded @@ -166,7 +195,7 @@ class @MergeRequestTabs url: "#{source}.json" success: (data) => document.querySelector("div#builds").innerHTML = data.html - $('.js-timeago').timeago() + gl.utils.localTimeAgo($('.js-timeago', 'div#builds')) @buildsLoaded = true @scrollToElement("#builds") diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee deleted file mode 100644 index b3c73ffce5d1c0f111b7acdf0db2a82a1edde403..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/merge_requests.js.coffee +++ /dev/null @@ -1,35 +0,0 @@ -# -# * Filter merge requests -# -@MergeRequests = - init: -> - MergeRequests.initSearch() - - # Make sure we trigger ajax request only after user stop typing - initSearch: -> - @timer = null - $("#issue_search").keyup -> - clearTimeout(@timer) - @timer = setTimeout(MergeRequests.filterResults, 500) - - filterResults: => - form = $("#issue_search_form") - search = $("#issue_search").val() - $('.merge-requests-holder').css("opacity", '0.5') - issues_url = form.attr('action') + '?' + form.serialize() - - $.ajax - type: "GET" - url: form.attr('action') - data: form.serialize() - complete: -> - $('.merge-requests-holder').css("opacity", '1.0') - success: (data) -> - $('.merge-requests-holder').html(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: issues_url}, document.title, issues_url - MergeRequests.reload() - dataType: "json" - - reload: -> - $('#filter_issue_search').val($('#issue_search').val()) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 6bd4e885a036cf5bdbb1a19a86e813baa187f2cb..345a0e447af333a705d2c957d027daa93567e90b 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -24,7 +24,7 @@ class @MilestoneSelect if issueUpdateURL milestoneLinkTemplate = _.template( - '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= title %></a>' + '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= _.escape(title) %></a>' ) milestoneLinkNoneTemplate = '<div class="light">None</div>' @@ -71,7 +71,7 @@ class @MilestoneSelect defaultLabel fieldName: $dropdown.data('field-name') text: (milestone) -> - milestone.title + _.escape(milestone.title) id: (milestone) -> if !useId milestone.name @@ -97,7 +97,7 @@ class @MilestoneSelect selectedMilestone = selected.name else selectedMilestone = '' - Issues.filterResults $dropdown.closest('form') + Issuable.filterResults $dropdown.closest('form') else if $dropdown.hasClass('js-filter-submit') $dropdown.closest('form').submit() else diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 86e3b860fcb90b9e3a931c50cc90c39fc1c84108..82e210fed7d200a7ee39d1c4f25bef48ff5202db 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -75,6 +75,9 @@ class @Notes # when issue status changes, we need to refresh data $(document).on "issuable:change", @refresh + # when a key is clicked on the notes + $(document).on "keydown", ".js-note-text", @keydownNoteText + cleanBinding: -> $(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-discussion-note-form" @@ -92,10 +95,19 @@ class @Notes $(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-close" $(document).off "click", ".js-note-discard" + $(document).off "keydown", ".js-note-text" $('.note .js-task-list-container').taskList('disable') $(document).off 'tasklist:changed', '.note .js-task-list-container' + keydownNoteText: (e) -> + $this = $(this) + if $this.val() is '' and e.which is 38 #aka the up key + myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last") + if myLastNote.length + myLastNoteEditBtn = myLastNote.find('.js-note-edit') + myLastNoteEditBtn.trigger('click', [true, myLastNote]) + initRefresh: -> clearInterval(Notes.interval) Notes.interval = setInterval => @@ -163,9 +175,15 @@ class @Notes else if @isNewNote(note) @note_ids.push(note.id) - $('ul.main-notes-list') + $notesList = $('ul.main-notes-list') + + $notesList .append(note.html) .syntaxHighlight() + + # Update datetime format on the recent note + gl.utils.localTimeAgo($notesList.find("#note_#{note.id} .js-timeago"), false) + @initTaskList() @updateNotesCount(1) @@ -217,6 +235,8 @@ class @Notes # append new note to all matching discussions discussionContainer.append note_html + gl.utils.localTimeAgo($('.js-timeago', note_html), false) + @updateNotesCount(1) ### @@ -275,32 +295,10 @@ class @Notes show the form ### setupNoteForm: (form) -> - disableButtonIfEmptyField form.find(".js-note-text"), form.find(".js-comment-button") - form.removeClass "js-new-note-form" - form.find('.div-dropzone').remove() - - # hide discard button - form.find('.js-note-discard').hide() - - # setup preview buttons - previewButton = form.find(".js-md-preview-button") + new GLForm form textarea = form.find(".js-note-text") - textarea.on "input", -> - if $(this).val().trim() isnt "" - previewButton.removeClass("turn-off").addClass "turn-on" - else - previewButton.removeClass("turn-on").addClass "turn-off" - - textarea.on 'focus', -> - $(this).closest('.md-area').addClass 'is-focused' - - textarea.on 'blur', -> - $(this).closest('.md-area').removeClass 'is-focused' - - autosize(textarea) - new Autosave textarea, [ "Note" form.find("#note_commit_id").val() @@ -309,11 +307,6 @@ class @Notes form.find("#note_noteable_id").val() ] - # remove notify commit author checkbox for non-commit notes - GitLab.GfmAutoComplete.setup() - new DropzoneInput(form) - form.show() - ### Called in response to the new note form being submitted @@ -345,7 +338,9 @@ class @Notes updateNote: (_xhr, note, _status) => # Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html) - $('.js-timeago', $html).timeago() + + gl.utils.localTimeAgo($('.js-timeago', $html)) + $html.syntaxHighlight() $html.find('.js-task-list-container').taskList('enable') @@ -360,39 +355,38 @@ class @Notes Adds a hidden div with the original content of the note to fill the edit note form with if the user cancels ### - showEditForm: (e) -> + showEditForm: (e, scrollTo, myLastNote) -> e.preventDefault() note = $(this).closest(".note") note.addClass "is-editting" form = note.find(".note-edit-form") - isNewForm = form.is(':not(.gfm-form)') - if isNewForm - form.addClass('gfm-form') + form.addClass('current-note-edit-form') # Show the attachment delete link note.find(".js-note-attachment-delete").show() - # Setup markdown form - if isNewForm - GitLab.GfmAutoComplete.setup() - new DropzoneInput(form) - - textarea = form.find("textarea") - textarea.focus() - - if isNewForm - autosize(textarea) - - # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). - # The textarea has the correct value, Chrome just won't show it unless we - # modify it, so let's clear it and re-set it! - value = textarea.val() - textarea.val "" - textarea.val value - - if isNewForm - disableButtonIfEmptyField textarea, form.find(".js-comment-button") + done = ($noteText) -> + # Neat little trick to put the cursor at the end + noteTextVal = $noteText.val() + $noteText.val('').val(noteTextVal); + + new GLForm form + if scrollTo? and myLastNote? + # scroll to the bottom + # so the open of the last element doesn't make a jump + $('html, body').scrollTop($(document).height()); + $('html, body').animate({ + scrollTop: myLastNote.offset().top - 150 + }, 500, -> + $noteText = form.find(".js-note-text") + $noteText.focus() + done($noteText) + ); + else + $noteText = form.find('.js-note-text') + $noteText.focus() + done($noteText) ### Called in response to clicking the edit note link @@ -549,6 +543,9 @@ class @Notes removeDiscussionNoteForm: (form)-> row = form.closest("tr") + glForm = form.data 'gl-form' + glForm.destroy() + form.find(".js-note-text").data("autosave").reset() # show the reply button (will only work for replies) @@ -560,7 +557,6 @@ class @Notes # only remove the form form.remove() - cancelDiscussionForm: (e) => e.preventDefault() form = $(e.target).closest(".js-discussion-note-form") diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index ae87c6c4e40bba1dc50e6e1bba288e7504fac0fd..26a12423521b95691e72f17b54719117a012cba6 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -18,8 +18,11 @@ class @Profile $(this).find('.btn-save').enable() $(this).find('.loading-gif').hide() - $('.update-notifications').on 'ajax:complete', -> - $(this).find('.btn-save').enable() + $('.update-notifications').on 'ajax:success', (e, data) -> + if data.saved + new Flash("Notification settings saved", "notice") + else + new Flash("Failed to save new settings", "alert") @bindEvents() @@ -42,9 +45,10 @@ class @Profile saveForm: -> self = @ - formData = new FormData(@form[0]) - formData.append('user[avatar]', @avatarGlCrop.getBlob(), 'avatar.png') + + avatarBlob = @avatarGlCrop.getBlob() + formData.append('user[avatar]', avatarBlob, 'avatar.png') if avatarBlob? $.ajax url: @form.attr('action') diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 87d313ed67c271e7549758c765535fc0ea607b18..07be85a32a5cfa43b813626512de3ab8646cf77c 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -37,19 +37,20 @@ class @Project $('.update-notification').on 'click', (e) -> e.preventDefault() notification_level = $(@).data 'notification-level' - $('#notification_level').val(notification_level) + label = $(@).data 'notification-title' + $('#notification_setting_level').val(notification_level) $('#notification-form').submit() - label = null - switch notification_level - when 0 then label = ' Disabled ' - when 1 then label = ' Participating ' - when 2 then label = ' Watching ' - when 3 then label = ' Global ' - when 4 then label = ' On Mention ' $('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>") $(@).parents('ul').find('li.active').removeClass 'active' $(@).parent().addClass 'active' + $('#notification-form').on 'ajax:success', (e, data) -> + if data.saved + new Flash("Notification settings saved", "notice") + else + new Flash("Failed to save new settings", "alert") + + @projectSelectDropdown() projectSelectDropdown: -> diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee index be8ab9b428dcc6e95baace8b712d96c8ec9dea06..704bd8dee536a4ccacf927be4bb67c482ac35720 100644 --- a/app/assets/javascripts/project_select.js.coffee +++ b/app/assets/javascripts/project_select.js.coffee @@ -1,5 +1,37 @@ class @ProjectSelect constructor: -> + $('.js-projects-dropdown-toggle').each (i, dropdown) -> + $dropdown = $(dropdown) + + $dropdown.glDropdown( + filterable: true + filterRemote: true + search: + fields: ['name_with_namespace'] + data: (term, callback) -> + finalCallback = (projects) -> + callback projects + + if @includeGroups + projectsCallback = (projects) -> + groupsCallback = (groups) -> + data = groups.concat(projects) + finalCallback(data) + + Api.groups term, false, groupsCallback + else + projectsCallback = finalCallback + + if @groupId + Api.groupProjects @groupId, term, projectsCallback + else + Api.projects term, @orderBy, projectsCallback + url: (project) -> + project.web_url + text: (project) -> + project.name_with_namespace + ) + $('.ajax-project-select').each (i, select) -> @groupId = $(select).data('group-id') @includeGroups = $(select).data('include-groups') diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index e4b7a3172ecd432340a863c1b0ab66880240a3e2..1a430f3aa47437ce1b50760ab27bc903896f4f79 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -2,7 +2,7 @@ class @Subscription constructor: (container) -> $container = $(container) @url = $container.attr('data-url') - @subscribe_button = $container.find('.subscribe-button') + @subscribe_button = $container.find('.js-subscribe-button') @subscription_status = $container.find('.subscription-status') @subscribe_button.unbind('click').click(@toggleSubscription) diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee index 886da72e261d6db89282ebc1d5e0248c9528a27e..10e698d6a54ee702466cbb31fd7a2ae39f226989 100644 --- a/app/assets/javascripts/todos.js.coffee +++ b/app/assets/javascripts/todos.js.coffee @@ -1,5 +1,11 @@ class @Todos - constructor: (@name) -> + constructor: (opts = {}) -> + { + @el = $('.js-todos-options') + } = opts + + @perPage = @el.data('perPage') + @clearListeners() @initBtnListeners() @@ -26,6 +32,7 @@ class @Todos dataType: 'json' data: '_method': 'delete' success: (data) => + @redirectIfNeeded data.count @clearDone $this.closest('li') @updateBadges data @@ -57,8 +64,44 @@ class @Todos $('.todos-pending .badge, .todos-pending-count').text data.count $('.todos-done .badge').text data.done_count + getTotalPages: -> + @el.data('totalPages') + + getCurrentPage: -> + @el.data('currentPage') + + getTodosPerPage: -> + @el.data('perPage') + + redirectIfNeeded: (total) -> + currPages = @getTotalPages() + currPage = @getCurrentPage() + + # Refresh if no remaining Todos + if not total + location.reload() + return + + # Do nothing if no pagination + return if not currPages + + newPages = Math.ceil(total / @getTodosPerPage()) + url = location.href # Includes query strings + + # If new total of pages is different than we have now + if newPages isnt currPages + # Redirect to previous page if there's one available + if currPages > 1 and currPage is currPages + pageParams = + page: currPages - 1 + url = gl.utils.mergeUrlParams(pageParams, url) + + Turbolinks.visit(url) + goToTodoUrl: (e)-> todoLink = $(this).data('url') + return unless todoLink + if e.metaKey e.preventDefault() window.open(todoLink,'_blank') diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index eee9b6e690e304dc87f225e7fb48d09c78a48daa..b80b1b861ccca05379b7a6864c0a8b79f33cae0b 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -12,6 +12,7 @@ class @UsersSelect showNullUser = $dropdown.data('null-user') showAnyUser = $dropdown.data('any-user') firstUser = $dropdown.data('first-user') + @authorId = $dropdown.data('author-id') selectedId = $dropdown.data('selected') defaultLabel = $dropdown.data('default-label') issueURL = $dropdown.data('issueUpdate') @@ -157,7 +158,7 @@ class @UsersSelect if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) selectedId = user.id - Issues.filterResults $dropdown.closest('form') + Issuable.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() else @@ -207,6 +208,7 @@ class @UsersSelect @projectId = $(select).data('project-id') @groupId = $(select).data('group-id') @showCurrentUser = $(select).data('current-user') + @authorId = $(select).data('author-id') showNullUser = $(select).data('null-user') showAnyUser = $(select).data('any-user') showEmailUser = $(select).data('email-user') @@ -312,6 +314,7 @@ class @UsersSelect project_id: @projectId group_id: @groupId current_user: @showCurrentUser + author_id: @authorId dataType: "json" ).done (users) -> callback(users) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index ba6c7930cdcd9e47d4be70977f2ad82f2985073b..fd395041c3dcb34ec4baaf4a696b5f825424842b 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -248,7 +248,7 @@ .dropdown-title { position: relative; - padding: 0 25px 15px; + padding: 0 25px 10px; margin: 0 10px 10px; font-weight: 600; line-height: 1; @@ -278,7 +278,7 @@ right: 5px; width: 20px; height: 20px; - top: -1px; + top: -3px; } .dropdown-menu-back { @@ -358,6 +358,13 @@ border-top: 1px solid $dropdown-divider-color; } +.dropdown-due-date-footer { + padding-top: 0; + margin-left: 10px; + margin-right: 10px; + border-top: 0; +} + .dropdown-footer-list { font-size: 14px; @@ -395,3 +402,122 @@ height: 15px; border-radius: $border-radius-base; } + +.dropdown-menu-due-date { + .dropdown-content { + max-height: 230px; + } + + .ui-widget { + table { + margin: 0; + } + + &.ui-datepicker-inline { + padding: 0 10px; + border: 0; + width: 100%; + } + + .ui-datepicker-header { + padding: 0 8px 10px; + border: 0; + + .ui-icon { + background: none; + font-size: 20px; + text-indent: 0; + + &:before { + display: block; + position: relative; + top: -2px; + color: $dropdown-title-btn-color; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + } + } + + .ui-state-active, + .ui-state-hover { + color: $md-link-color; + background-color: $calendar-hover-bg; + } + + .ui-datepicker-prev, + .ui-datepicker-next { + top: 0; + height: 15px; + cursor: pointer; + + &:hover { + background-color: transparent; + border: 0; + + .ui-icon:before { + color: $md-link-color; + } + } + } + + .ui-datepicker-prev { + left: 0; + + .ui-icon:before { + content: '\f104'; + text-align: left; + } + } + + .ui-datepicker-next { + right: 0; + + .ui-icon:before { + content: '\f105'; + text-align: right; + } + } + + td { + padding: 0; + border: 1px solid $calendar-border-color; + + &:first-child { + border-left: 0; + } + + &:last-child { + border-right: 0; + } + + a { + line-height: 17px; + border: 0; + border-radius: 0; + } + } + + .ui-datepicker-title { + color: $gl-gray; + font-size: 15px; + line-height: 1; + font-weight: normal; + } + } + + th { + padding: 2px 0; + color: $calendar-header-color; + font-weight: normal; + text-transform: lowercase; + border-top: 1px solid $calendar-border-color; + } + + .ui-datepicker-unselectable { + background-color: $calendar-unselectable-bg; + } +} diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index 5ae0520fd7b6d9eabe815bef2bfbc80be70b6227..f4d35c4b4b1fe26f4a816541cb98be145bdfa618 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -1,24 +1,6 @@ /** * Styles that apply to all GFM related forms. */ -.issue-form, .merge-request-form, .wiki-form { - .description { - height: 16em; - border-top-left-radius: 0; - } -} - -.wiki-form { - .description { - height: 26em; - } -} - -.milestone-form { - .description { - height: 14em; - } -} .gfm-commit, .gfm-commit_range { font-family: $monospace_font; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index b3397d160165ceb4069d4a512416642bd0ab9d1d..c303380764b0ec38716eb4056ccf42cea6f8a384 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -69,13 +69,18 @@ header { } .header-content { + position: relative; height: $header-height; - padding-right: 20px; + padding-right: 40px; @media (min-width: $screen-sm-min) { padding-right: 0; } + .dropdown-menu { + margin-top: -5px; + } + .title { margin: 0; font-size: 19px; @@ -117,6 +122,10 @@ header { } } + .project-item-select-holder { + display: inline; + } + .impersonation i { color: $red-normal; } diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 7f7b7c806e71ada444f63d658933777cbb4361f7..8bfc0d583c57f901832dda50e8df72a1fa3fecd1 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -5,7 +5,7 @@ */ .status-box { - + /* Extra small devices (phones, less than 768px) */ /* No media query since this is the default in Bootstrap */ padding: 5px 11px; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index c8f86d60e3bdfcb5addcaf0c18f87518495b1c36..704fa1ff800d79126be2670a9ac822477a52eed0 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -1,22 +1,15 @@ .div-dropzone-wrapper { .div-dropzone { position: relative; - margin-bottom: -5px; - - .div-dropzone-focus { - border-color: #66afe9 !important; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6) !important; - outline: 0 !important; - } .div-dropzone-hover { position: absolute; top: 50%; left: 50%; - margin-top: -0.5em; - margin-left: -0.6em; + margin-top: -11.5px; + margin-left: -15px; opacity: 0; - font-size: 50px; + font-size: 30px; transition: opacity 200ms ease-in-out; pointer-events: none; } @@ -97,3 +90,12 @@ box-shadow: none; width: 100%; } + +.md { + &.md-preview-holder { + code { + white-space: pre-wrap; + word-break: break-all; + } + } +} diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 66180f38a4fb36364e40e9f571ade01916940a6d..7eb451c124ef991a1f7c8332d313f924fe4230a2 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -70,13 +70,6 @@ display: none; } - .issue-details { - .creator, - .page-title .btn-close { - display: none; - } - } - %ul.notes .note-role, .note-actions { display: none; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 94f5a12ff6af4d0905ec107c30edada4b045ba91..192d53b048aa1b8d69d54ff040769ec58c77539b 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -58,12 +58,12 @@ .nav-search { display: inline-block; - width: 50%; + width: 100%; padding: 11px 0; /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-sm-min) { - width: 100%; + @media (min-width: $screen-sm-min) { + width: 50%; } } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index b91f2f6f898dfc7ef5e7fd265cb8918c2a303e14..f0ec250de2b863a426f844df87782d3b4a67afbb 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -39,8 +39,7 @@ .diff-file { border: 1px solid $border-color; border-bottom: none; - margin-left: 0; - margin-right: 0; + margin: 0; } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index dd42db1840f35497578ad092008a0e827b182548..96bab7880c2156b2d21efa82c0993ff1f071579a 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -43,7 +43,6 @@ @import "bootstrap/modals"; @import "bootstrap/tooltip"; @import "bootstrap/popovers"; -@import "bootstrap/carousel"; // Utility classes .clearfix { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 7b2aada5a0d84805d02d8fc4dcfd76a08af4f088..0a5b4b8834c8605ff4cfdbf70455819eb97a9928 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -250,14 +250,6 @@ a > code { * Textareas intended for GFM * */ -.js-gfm-input { - font-family: $monospace_font; - color: $gl-text-color; -} - -.md-preview { -} - .strikethrough { text-decoration: line-through; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 1ebbd9b0e57092f55c47ce31084093f7c1ae2194..30ca27ab1048c5a922523a27fc0bb71cad086222 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -28,6 +28,7 @@ $gl-link-color: #3084bb; $gl-dark-link-color: #333; $gl-placeholder-color: #8f8f8f; $gl-icon-color: $gl-placeholder-color; +$gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-header-color: $gl-title-color; @@ -149,6 +150,7 @@ $light-grey-header: #faf9f9; */ $gl-primary: $blue-normal; $gl-success: $green-normal; +$gl-success-focus: rgba($gl-success, .4); $gl-info: $blue-normal; $gl-warning: $orange-normal; $gl-danger: $red-normal; @@ -239,3 +241,8 @@ $note-form-border-color: #e5e5e5; $note-toolbar-color: #959494; $zen-control-hover-color: #111; + +$calendar-header-color: #b8b8b8; +$calendar-hover-bg: #ecf3fe; +$calendar-border-color: rgba(#000, .1); +$calendar-unselectable-bg: #faf9f9; diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 47673944896f2e9d49877781ba88a8645915c677..77a73dc379b43b253781c7b2232c5c8b193c2ba0 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #557; + border-color: darken(#557, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080); } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 806401c21ae603b7c11273dd4e96263c5933e5a0..28253d4ccb4c756b45b0aee6db9e7c32539fd052 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #49483e; + border-color: darken(#49483e, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080); } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 6a809d4dfd26658060e0acb83f6e60e8658bd33f..c62bd021aefda15de6fe2baea4d02b965b97e294 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #174652; + border-color: darken(#174652, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46); } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index c482a1258f7d45f77e3db5831a357027e548848e..524cfaf90c309c43d0aac388acbaf80fc87984b3 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #ddd8c5; + border-color: darken(#ddd8c5, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4); } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 28331f59754f2684291473e392dd3d9fc67eb9af..1ff6ad75e07a39c17bd23e7083a2a1cd24259d8d 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #f8eec7; + border-color: darken(#f8eec7, 15%); + } + .diff-line-num { &.old { background-color: $line-number-old; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 6453c91d955069866bea7c0873c9f3fbc26e8306..c8c6bbde0846a9c8491e514cb7efcff25579259b 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -75,6 +75,11 @@ li.commit { } } + .item-title { + display: inline-block; + max-width: 70%; + } + .commit-row-description { font-size: 14px; border-left: 1px solid #eee; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 5917f08972054fb3178750b7363409fc0ffd667d..751a5ab4d92e7eb8927a7d6606c6dc5c7c4c1f60 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -1,5 +1,5 @@ .detail-page-header { - padding: 11px 0; + padding: $gl-padding-top 0; border-bottom: 1px solid $border-color; color: #5c5d5e; font-size: 16px; @@ -16,11 +16,6 @@ .issue_created_ago, .author_link { white-space: nowrap; } - - .issue-meta { - display: inline-block; - line-height: 20px; - } } .detail-page-description { @@ -41,4 +36,10 @@ } } } + + .wiki { + code { + white-space: pre-wrap; + } + } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index d0855f66911d127a1a39eeadd6620f586e0db203..e7c8198ba455ae9ae144fd6eabc48640b540961a 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -34,6 +34,7 @@ background: #fff; color: #333; border-radius: 0 0 3px 3px; + -webkit-overflow-scrolling: auto; .unfold { cursor: pointer; @@ -67,8 +68,26 @@ line-height: $code_line_height; font-size: $code_font_size; + &.noteable_line { + position: relative; + + &.old { + &:before { + content: '-'; + position: absolute; + } + } + + &.new { + &:before { + content: '+'; + position: absolute; + } + } + } + span { - white-space: pre; + white-space: pre-wrap; } } } @@ -317,7 +336,7 @@ } .diff-file .line_content { - white-space: pre; + white-space: pre-wrap; } .diff-wrap-lines .line_content { @@ -391,3 +410,23 @@ margin-bottom: 0; } } + +.file-holder { + .diff-line-num:not(.js-unfold-bottom) { + a { + &:before { + content: attr(data-linenumber); + } + } + } +} + +.discussion { + .diff-content { + .diff-line-num { + &:before { + content: attr(data-linenumber); + } + } + } +} diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 0f0592a0ab8bcc57416619b7e654319bcab11945..8981f070a20337d9184f73b2a4d37c4cbd6015ee 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -26,6 +26,10 @@ line-height: 42px; padding-top: 7px; padding-bottom: 7px; + + .pull-right { + height: 20px; + } } .editor-ref { @@ -53,4 +57,9 @@ .select2 { float: right; } + + .encoding-selector, + .license-selector { + display: inline-block; + } } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index c66efe978cda14584d7591f84fe507300e5c7cd6..6fe57c737b39eef27352a32a9e4274d4add2df2b 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -41,8 +41,17 @@ word-wrap: break-word; .md { - color: #7f8fa4; + color: $gl-grayish-blue; font-size: $gl-font-size; + + .label { + color: $gl-text-color; + font-size: inherit; + } + + iframe.twitter-share-button { + vertical-align: bottom; + } } pre { diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 604f1700cf840543786e51c956b68d272be2966e..ee95bdf488e3842fe4c05446506ba2a4ac565420 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -59,8 +59,10 @@ position: relative; overflow-y: auto; padding: 15px; + .form-actions { margin: -$gl-padding+1; + margin-top: 15px; } } diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss index 6a99cd9cb9450ab475f2a33ea7ca0444f498adf3..84cc35239f94ceed3198faa50359d8b0282fa028 100644 --- a/app/assets/stylesheets/pages/import.scss +++ b/app/assets/stylesheets/pages/import.scss @@ -16,3 +16,24 @@ i.icon-gitorious-big { width: 18px; height: 18px; } + +.import-jobs-from-col, +.import-jobs-to-col { + width: 40%; +} + +.import-jobs-status-col { + width: 20%; +} + +.btn-import { + .loading-icon { + display: none; + } + + &.is-loading { + .loading-icon { + display: inline-block; + } + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 88c1b614c740efaa21b60e12ea6d899860b5f32c..1d6190c8f18621ccde71c5bf5621f983c6fdcf6f 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -128,6 +128,7 @@ top: 58px; bottom: 0; right: 0; + z-index: 10; transition: width .3s; background: $gray-light; padding: 10px 20px; @@ -173,12 +174,6 @@ } } - .subscribe-button { - span { - margin-top: 0; - } - } - &.right-sidebar-collapsed { /* Extra small devices (phones, less than 768px) */ display: none; @@ -247,7 +242,7 @@ } } - .btn { + .issuable-pager { background: $gray-normal; border: 1px solid $border-gray-normal; &:hover { @@ -256,13 +251,19 @@ } } - a:not(.btn) { + a:not(.issuable-pager) { &:hover { color: $md-link-color; text-decoration: none; } } + .dropdown-content { + a:hover { + color: inherit; + } + } + .dropdown-menu-toggle { width: 100%; padding-top: 6px; @@ -273,10 +274,6 @@ } } -.btn-default.gutter-toggle { - margin-top: 4px; -} - .detail-page-description { small { color: $gray-darkest; @@ -316,3 +313,56 @@ color: #8c8c8c; } } + +.issuable-form-padding-top { + @media (min-width: $screen-sm-min) { + padding-top: 7px; + } +} + +.issuable-status-box { + float: none; + display: inline-block; + margin-top: 0; + + @media (max-width: $screen-xs-max) { + position: absolute; + top: 0; + left: 0; + } +} + +.issuable-header { + position: relative; + padding-left: 45px; + padding-right: 45px; + line-height: 35px; + + @media (min-width: $screen-sm-min) { + float: left; + padding-left: 0; + padding-right: 0; + } +} + +.issuable-actions { + padding-top: 10px; + + @media (min-width: $screen-sm-min) { + float: right; + padding-top: 0; + } +} + +.issuable-gutter-toggle { + @media (max-width: $screen-sm-max) { + position: absolute; + top: 0; + right: 0; + } +} + +.issuable-meta { + display: inline-block; + line-height: 18px; +} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 6a1d28590c20721746da2d363f7bee78a7d22d15..fc9db97132dd463b252b09c2ac0472ccac9c88a5 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -86,41 +86,9 @@ form.edit-issue { @media (max-width: $screen-xs-max) { .issue-btn-group { width: 100%; - margin-top: 5px; - - .btn-group { - width: 100%; - - ul { - width: 100%; - text-align: center; - } - } .btn { width: 100%; - - &:first-child:not(:last-child) { - - } - - &:not(:first-child):not(:last-child) { - margin-top: 10px; - } - - &:last-child:not(:first-child) { - margin-top: 10px; - } - } - } - - .issue { - &:hover .issue-actions { - display: none !important; - } - - .issue-updated-at { - display: none; } } } @@ -133,11 +101,3 @@ form.edit-issue { color: $gl-text-color; margin-left: 52px; } - -.editor-details { - display: block; - - @media (min-width: $screen-sm-min) { - display: inline-block; - } -} diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 3e0a3140be77a6d99f3be7e31fbc3b611ae1b467..e179bdf0048d1620d9f6803c4cf8c365ea942551 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -79,19 +79,30 @@ color: $white-light; } +@mixin labels-mobile { + @media (max-width: $screen-xs-min) { + display: block; + width: 100%; + margin-left: 0; + padding: 10px 0; + } +} + + .manage-labels-list { - .prepend-left-10 { + .prepend-left-10, .prepend-description-left { display: inline-block; width: 40%; vertical-align: middle; - @media (max-width: $screen-xs-min) { - display: block; - width: 100%; - margin-left: 0; - padding: 10px 0; - } + @include labels-mobile; + } + + .prepend-description-left { + width: 57%; + + @include labels-mobile; } .pull-info-right { @@ -106,7 +117,7 @@ padding: 6px; color: $gl-text-color; - &.subscribe-button { + &.label-subscribe-button { padding-left: 0; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index b79335eab9155bb336eca8d3e2b2c7fa64b27fac..4ef548ffbe7c132c1e4c11d823a62c084a781f53 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -142,6 +142,7 @@ overflow: hidden; font-size: 90%; margin: 0 3px; + word-break: break-all; } .mr-list { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 4d4d508396d7b1ad429950e9d5d4a2bb37f04bac..7fa13e66b436661bacc013d0c304f316b1fae91f 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -40,7 +40,9 @@ } .note-textarea { + display: block; padding: 10px 0; + color: $gl-gray; font-family: $regular_font; border: 0; @@ -60,23 +62,34 @@ padding: $gl-padding-top $gl-padding; border: 1px solid $note-form-border-color; border-radius: $border-radius-base; + transition: border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; &.is-focused { - border-color: $focus-border-color; - box-shadow: 0 0 2px rgba(#000, .2), - 0 0 4px rgba($focus-border-color, .4); + @extend .form-control:focus; .comment-toolbar, .nav-links { border-color: $focus-border-color; } } + + &.is-dropzone-hover { + border-color: $gl-success; + box-shadow: 0 0 2px $black-transparent, + 0 0 4px $gl-success-focus; + + .comment-toolbar, + .nav-links { + border-color: $gl-success; + } + } } } .discussion-form { padding: $gl-padding-top $gl-padding; - background-color: #fff; + background-color: $white-light; } .note-edit-form { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 7295fe51121e5346bd9d6479286a1be742eac40a..82f78e8d7966af0c19d694950c72a527f7dd1950 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -81,10 +81,8 @@ ul.notes { @include md-typography; // On diffs code should wrap nicely and not overflow - pre { - code { - white-space: pre; - } + code { + white-space: pre-wrap; } // Reset ul style types since we're nested inside a ul already @@ -112,6 +110,10 @@ ul.notes { margin: 10px 0; } } + + a { + word-break: break-all; + } } .note-header { @@ -145,19 +147,27 @@ ul.notes { background: $background-color; color: $text-color; } + &.notes_line2 { text-align: center; padding: 10px 0; border-left: 1px solid #ddd !important; } + &.notes_content { - background-color: #fff; + background-color: $background-color; border-width: 1px 0; padding: 0; vertical-align: top; + white-space: normal; + &.parallel { border-width: 1px; } + + .notes { + background-color: $white-light; + } } } } @@ -180,6 +190,12 @@ ul.notes { color: $notes-light-color; } +.discussion-headline-light { + a { + color: $gl-link-color; + } +} + /** * Actions for Discussions/Notes */ @@ -191,6 +207,17 @@ ul.notes { color: $notes-action-color; } +.discussion-actions { + @media (max-width: $screen-sm-max) { + float: none; + margin-left: 0; + + .note-action-button { + margin-left: 0; + } + } +} + .note-action-button, .discussion-action-button { display: inline-block; @@ -258,8 +285,7 @@ ul.notes { .diff-file tr.line_holder { @mixin show-add-diff-note { - filter: alpha(opacity=100); - opacity: 1.0; + display: inline-block; } .add-diff-note { @@ -269,17 +295,12 @@ ul.notes { padding: 4px; font-size: 16px; color: $gl-link-color; - margin-left: -60px; + margin-left: -56px; position: absolute; z-index: 10; width: 32px; - - transition: all 0.2s ease; - // "hide" it by default - opacity: 0.0; - filter: alpha(opacity=0); - + display: none; &:hover { background: $gl-info; color: #fff; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index e83fa9e3d52e2f576c0cf54d132a24b12ade7162..75f78569e3c17df7f519c03b581509aa31f8c30e 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -34,6 +34,11 @@ color: #7f8fa4; font-size: $gl-font-size; + .label { + color: $gl-text-color; + font-size: inherit; + } + p { color: #5c5d5e; } diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index 1be0551ad3be5a0f0504ae730420ff19a8e6a02a..a30b64925729a620cded078f4ff983fd566b2740 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -1,17 +1,37 @@ -/* Generic print styles */ -header, nav, nav.main-nav, nav.navbar-collapse, nav.navbar-collapse.collapse {display: none!important;} -.profiler-results {display: none;} - -/* Styles targeted specifically at printing files */ -.tree-ref-holder, .tree-holder .breadcrumb, .blob-commit-info {display: none;} -.file-title {display: none;} -.file-holder {border: none;} - .wiki h1, .wiki h2, .wiki h3, .wiki h4, .wiki h5, .wiki h6 {margin-top: 17px; } .wiki h1 {font-size: 30px;} .wiki h2 {font-size: 22px;} .wiki h3 {font-size: 18px; font-weight: bold; } -.sidebar-wrapper { display: none; } -.nav { display: none; } -.btn { display: none; } +header, +nav, +nav.main-nav, +nav.navbar-collapse, +nav.navbar-collapse.collapse, +.profiler-results, +.tree-ref-holder, +.tree-holder .breadcrumb, +.blob-commit-info, +.file-title, +.file-holder, +.sidebar-wrapper, +.nav, +.btn, +ul.notes-form, +.merge-request-ci-status .ci-status-link:after, +.issuable-gutter-toggle, +.gutter-toggle, +.issuable-details .content-block-small, +.edit-link, +.note-action-button { + display: none!important; +} + +.page-gutter { + padding-top: 0; + padding-left: 0; +} + +.right-sidebar { + top: 0; +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index f010436bd36d64f7d2992f7bf2317537b6972224..ec22548ddeb145f36e8b06a5850860fa26d6a355 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -19,6 +19,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to admin_runners_path end + def clear_repository_check_states + RepositoryCheck::ClearWorker.perform_async + + redirect_to( + admin_application_settings_path, + notice: 'Started asynchronous removal of all repository check states.' + ) + end + private def set_application_setting @@ -66,6 +75,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :admin_notification_email, :user_oauth_applications, :shared_runners_enabled, + :shared_runners_text, :max_artifacts_size, :metrics_enabled, :metrics_host, @@ -82,6 +92,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :akismet_enabled, :akismet_api_key, :email_author_in_body, + :repository_checks_enabled, + :metrics_packet_size, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index 0bd19c49d8f1f9a1e616ca9113049a47af614592..93c4894ea0f5791d023c5d3f08eae9480abb8632 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -39,6 +39,6 @@ class Admin::HooksController < Admin::ApplicationController end def hook_params - params.require(:hook).permit(:url, :enable_ssl_verification) + params.require(:hook).permit(:url, :enable_ssl_verification, :push_events, :tag_push_events) end end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index c6b3105544ad84a42a1218081f722e78597f30fc..87986fdf8b13625397bfd40bfac4130e9c6c17db 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,5 +1,5 @@ class Admin::ProjectsController < Admin::ApplicationController - before_action :project, only: [:show, :transfer] + before_action :project, only: [:show, :transfer, :repository_check] before_action :group, only: [:show, :transfer] def index @@ -8,6 +8,7 @@ class Admin::ProjectsController < Admin::ApplicationController @projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? + @projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present? @projects = @projects.non_archived unless params[:with_archived].present? @projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.sort(@sort = params[:sort]) @@ -30,6 +31,15 @@ class Admin::ProjectsController < Admin::ApplicationController redirect_to admin_namespace_project_path(@project.namespace, @project) end + def repository_check + RepositoryCheck::SingleRepositoryWorker.perform_async(@project.id) + + redirect_to( + admin_namespace_project_path(@project.namespace, @project), + notice: 'Repository check was triggered.' + ) + end + protected def project diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 97d53acde941ee1cf27788a7a736d244047352da..1c53b0b21a3695edbccbc15fec82d7e116fb347e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,6 +3,7 @@ require 'fogbugz' class ApplicationController < ActionController::Base include Gitlab::CurrentSettings + include Gitlab::GonHelper include GitlabRoutingHelper include PageLayoutHelper @@ -13,7 +14,7 @@ class ApplicationController < ActionController::Base before_action :check_password_expiration before_action :check_2fa_requirement before_action :ldap_security_check - before_action :sentry_user_context + before_action :sentry_context before_action :default_headers before_action :add_gon_variables before_action :configure_permitted_parameters, if: :devise_controller? @@ -40,13 +41,15 @@ class ApplicationController < ActionController::Base protected - def sentry_user_context - if Rails.env.production? && current_application_settings.sentry_enabled && current_user - Raven.user_context( - id: current_user.id, - email: current_user.email, - username: current_user.username, - ) + def sentry_context + if Rails.env.production? && current_application_settings.sentry_enabled + if current_user + Raven.user_context( + id: current_user.id, + email: current_user.email, + username: current_user.username, + ) + end Raven.tags_context(program: sentry_program_context) end @@ -158,20 +161,6 @@ class ApplicationController < ActionController::Base end end - def add_gon_variables - gon.api_version = API::API.version - gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s - gon.default_issues_tracker = Project.new.default_issue_tracker.to_param - gon.max_file_size = current_application_settings.max_attachment_size - gon.relative_url_root = Gitlab.config.gitlab.relative_url_root - gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class - - if current_user - gon.current_user_id = current_user.id - gon.api_token = current_user.private_token - end - end - def validate_user_service_ticket! return unless signed_in? && session[:service_tickets] diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 81ba58ce49c4168b3425e36b24462a27a565a5b5..eb0abc80ab43e34d8f3d4812e2e80de34ec5747d 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -12,8 +12,15 @@ class AutocompleteController < ApplicationController if params[:search].blank? # Include current user if available to filter by "Me" if params[:current_user] && current_user - @users = [*@users, current_user].uniq + @users = [*@users, current_user] end + + if params[:author_id].present? + author = User.find_by_id(params[:author_id]) + @users = [author, *@users] if author + end + + @users.uniq! end render json: @users, only: [:name, :username, :id], methods: [:avatar_url] diff --git a/app/controllers/groups/notification_settings_controller.rb b/app/controllers/groups/notification_settings_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..de13b16ccf210cb65f1d9dbd10be9cc360cd880b --- /dev/null +++ b/app/controllers/groups/notification_settings_controller.rb @@ -0,0 +1,16 @@ +class Groups::NotificationSettingsController < Groups::ApplicationController + before_action :authenticate_user! + + def update + notification_setting = current_user.notification_settings_for(group) + saved = notification_setting.update_attributes(notification_setting_params) + + render json: { saved: saved } + end + + private + + def notification_setting_params + params.require(:notification_setting).permit(:level) + end +end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index c1adc999567019ee6d0ab7aa945cbbeb0d445ed9..ee4fcc4e360e895f79f84a25fe52cb504e2a875a 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -40,6 +40,7 @@ class GroupsController < Groups::ApplicationController @last_push = current_user.recent_push if current_user @projects = @projects.includes(:namespace) + @projects = @projects.sorted_by_activity @projects = filter_projects(@projects) @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]) if params[:filter_projects].blank? diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 550506154736cfa7c6de7ca7324b6b4aa975bc3d..9b5c43b17e250b03d14da3e7a98d5d189d1b1dbb 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -51,6 +51,7 @@ class HelpController < ApplicationController end def ui + @user = User.new(id: 0, name: 'John Doe', username: '@johndoe') end private diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index d1e4ac10f6ca92031f104413f29f0a546cf2d031..c6bdd0602c156f916b14bdf03d429f44af4350c3 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,9 +1,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include Gitlab::CurrentSettings + include Gitlab::GonHelper include PageLayoutHelper before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! + before_action :add_gon_variables layout 'profile' diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index b88c080352bc1958e0824ce3a4c62de6ea2fc9b0..a12549d6bcb7dcb9549ccb2d4ed831e0504db579 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -10,6 +10,11 @@ class Profiles::KeysController < Profiles::ApplicationController @key = current_user.keys.find(params[:id]) end + # Back-compat: We need to support this URL since git-annex webapp points to it + def new + redirect_to profile_keys_path + end + def create @key = current_user.keys.new(key_params) diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 1fd1d6882dfee07654367a6ad2272a073e319397..18ee55c839a649f11c787829119e0c637d40351c 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -1,39 +1,18 @@ class Profiles::NotificationsController < Profiles::ApplicationController def show @user = current_user - @notification = current_user.notification - @project_members = current_user.project_members - @group_members = current_user.group_members + @group_notifications = current_user.notification_settings.for_groups + @project_notifications = current_user.notification_settings.for_projects end def update - type = params[:notification_type] - - @saved = if type == 'global' - current_user.update_attributes(user_params) - elsif type == 'group' - group_member = current_user.group_members.find(params[:notification_id]) - group_member.notification_level = params[:notification_level] - group_member.save - else - project_member = current_user.project_members.find(params[:notification_id]) - project_member.notification_level = params[:notification_level] - project_member.save - end - - respond_to do |format| - format.html do - if @saved - flash[:notice] = "Notification settings saved" - else - flash[:alert] = "Failed to save new settings" - end - - redirect_back_or_default(default: profile_notifications_path) - end - - format.js + if current_user.update_attributes(user_params) + flash[:notice] = "Notification settings saved" + else + flash[:alert] = "Failed to save new settings" end + + redirect_back_or_default(default: profile_notifications_path) end def user_params diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index f159e169f6d03224472a64ba7f59575b8b0b2eb4..b8b9e78427d5f8f924e76ae21065a7aa406dc725 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -1,7 +1,7 @@ class Projects::BuildsController < Projects::ApplicationController before_action :build, except: [:index, :cancel_all] before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry] - before_action :authorize_update_build!, except: [:index, :show, :status] + before_action :authorize_update_build!, except: [:index, :show, :status, :raw] layout 'project' def index @@ -62,6 +62,14 @@ class Projects::BuildsController < Projects::ApplicationController notice: "Build has been sucessfully erased!" end + def raw + if @build.has_trace? + send_file @build.path_to_trace, type: 'text/plain; charset=utf-8', disposition: 'inline' + else + render_404 + end + end + private def build diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 576fa3cedb2b60bcf6f4ef0d1dfc67ee79861716..a202cb38692cd9eb0a5cb239cddbe82c8bc5db7c 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -12,7 +12,7 @@ class Projects::CommitController < Projects::ApplicationController before_action :authorize_read_commit_status!, only: [:builds] before_action :commit before_action :define_show_vars, only: [:show, :builds] - before_action :authorize_edit_tree!, only: [:revert] + before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] def show apply_diff_view_cookie! @@ -38,13 +38,13 @@ class Projects::CommitController < Projects::ApplicationController end def cancel_builds - ci_commit.builds.running_or_pending.each(&:cancel) + ci_builds.running_or_pending.each(&:cancel) redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) end def retry_builds - ci_commit.builds.latest.failed.each do |build| + ci_builds.latest.failed.each do |build| if build.retryable? Ci::Build.retry(build) end @@ -60,27 +60,32 @@ class Projects::CommitController < Projects::ApplicationController end def revert - assign_revert_commit_vars + assign_change_commit_vars(@commit.revert_branch_name) return render_404 if @target_branch.blank? - create_commit(Commits::RevertService, success_notice: "The #{revert_type_title} has been successfully reverted.", - success_path: successful_revert_path, failure_path: failed_revert_path) + create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.", + success_path: successful_change_path, failure_path: failed_change_path) end + + def cherry_pick + assign_change_commit_vars(@commit.cherry_pick_branch_name) + + return render_404 if @target_branch.blank? - private - - def revert_type_title - @commit.merged_merge_request ? 'merge request' : 'commit' + create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.", + success_path: successful_change_path, failure_path: failed_change_path) end - def successful_revert_path + private + + def successful_change_path return referenced_merge_request_url if @commit.merged_merge_request namespace_project_commits_url(@project.namespace, @project, @target_branch) end - def failed_revert_path + def failed_change_path return referenced_merge_request_url if @commit.merged_merge_request namespace_project_commit_url(@project.namespace, @project, params[:id]) @@ -94,8 +99,12 @@ class Projects::CommitController < Projects::ApplicationController @commit ||= @project.commit(params[:id]) end - def ci_commit - @ci_commit ||= project.ci_commit(commit.sha) + def ci_commits + @ci_commits ||= project.ci_commits.where(sha: commit.sha) + end + + def ci_builds + @ci_builds ||= Ci::Build.where(commit: ci_commits) end def define_show_vars @@ -108,17 +117,17 @@ class Projects::CommitController < Projects::ApplicationController @diff_refs = [commit.parent || commit, commit] @notes_count = commit.notes.count - @statuses = ci_commit.statuses if ci_commit + @statuses = CommitStatus.where(commit: ci_commits) + @builds = Ci::Build.where(commit: ci_commits) end - def assign_revert_commit_vars + def assign_change_commit_vars(mr_source_branch) @commit = project.commit(params[:id]) @target_branch = params[:target_branch] - @mr_source_branch = @commit.revert_branch_name + @mr_source_branch = mr_source_branch @mr_target_branch = @target_branch @commit_params = { commit: @commit, - revert_type_title: revert_type_title, create_merge_request: params[:create_merge_request].present? || different_project? } end diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 4159e53bfa9fde0aed75a4ede1adcff4442b37b6..606552fa85322b5486c8aa930fdf34c459125c3c 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -7,10 +7,12 @@ class Projects::GroupLinksController < Projects::ApplicationController end def create - link = project.project_group_links.new - link.group_id = params[:link_group_id] - link.group_access = params[:link_group_access] - link.save + group = Group.find(params[:link_group_id]) + return render_404 unless can?(current_user, :read_group, group) + + project.project_group_links.create( + group: group, group_access: params[:link_group_access] + ) redirect_to namespace_project_group_links_path(project.namespace, project) end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 6d649e72f84b4e9664f8f8547ce5352da5f320d8..b96ab91c17db96aa00c8c746a93238341ba1378b 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -3,7 +3,8 @@ class Projects::IssuesController < Projects::ApplicationController include IssuableActions before_action :module_enabled - before_action :issue, only: [:edit, :update, :show] + before_action :issue, + only: [:edit, :update, :show, :referenced_merge_requests, :related_branches] # Allow read any issue before_action :authorize_read_issue!, only: [:show] @@ -17,9 +18,6 @@ class Projects::IssuesController < Projects::ApplicationController # Allow issues bulk update before_action :authorize_admin_issues!, only: [:bulk_update] - # Cross-reference merge requests - before_action :closed_by_merge_requests, only: [:show] - respond_to :html def index @@ -35,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController end @issues = @issues.page(params[:page]) - @label = @project.labels.find_by(title: params[:label_name]) + @labels = @project.labels.where(title: params[:label_name]) respond_to do |format| format.html format.atom { render layout: false } format.json do render json: { - html: view_to_html_string("projects/issues/_issues") + html: view_to_html_string("projects/issues/_issues"), + labels: @labels.as_json(methods: :text_color) } end end @@ -62,11 +61,9 @@ class Projects::IssuesController < Projects::ApplicationController end def show - @note = @project.notes.new(noteable: @issue) - @notes = @issue.notes.nonawards.with_associations.fresh + @note = @project.notes.new(noteable: @issue) + @notes = @issue.notes.nonawards.with_associations.fresh @noteable = @issue - @merge_requests = @issue.referenced_merge_requests(current_user) - @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch) respond_to do |format| format.html @@ -118,15 +115,36 @@ class Projects::IssuesController < Projects::ApplicationController end end + def referenced_merge_requests + @merge_requests = @issue.referenced_merge_requests(current_user) + @closed_by_merge_requests = @issue.closed_by_merge_requests(current_user) + + respond_to do |format| + format.json do + render json: { + html: view_to_html_string('projects/issues/_merge_requests') + } + end + end + end + + def related_branches + @related_branches = @issue.related_branches(current_user) + + respond_to do |format| + format.json do + render json: { + html: view_to_html_string('projects/issues/_related_branches') + } + end + end + end + def bulk_update result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) end - def closed_by_merge_requests - @closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user) - end - protected def issue @@ -174,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController def issue_params params.require(:issue).permit( :title, :assignee_id, :position, :description, :confidential, - :milestone_id, :state_event, :task_num, label_ids: [] + :milestone_id, :due_date, :state_event, :task_num, label_ids: [] ) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3e0cfc6aa657388b747a7c1e9fd8ab1a8cbc6ee2..81003c34f594b279f0881b756ec4c506c1f95aca 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.preload(:target_project) - @label = @project.labels.find_by(title: params[:label_name]) + @labels = @project.labels.where(title: params[:label_name]) respond_to do |format| format.html format.json do render json: { - html: view_to_html_string("projects/merge_requests/_merge_requests") + html: view_to_html_string("projects/merge_requests/_merge_requests"), + labels: @labels.as_json(methods: :text_color) } end end @@ -320,6 +321,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_widget_vars @ci_commit = @merge_request.ci_commit + @ci_commits = [@ci_commit].compact closes_issues end diff --git a/app/controllers/projects/notification_settings_controller.rb b/app/controllers/projects/notification_settings_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..7d81cc03c73f6a91bc322ca5221654ed163a03cb --- /dev/null +++ b/app/controllers/projects/notification_settings_controller.rb @@ -0,0 +1,16 @@ +class Projects::NotificationSettingsController < Projects::ApplicationController + before_action :authenticate_user! + + def update + notification_setting = current_user.notification_settings_for(project) + saved = notification_setting.update_attributes(notification_setting_params) + + render json: { saved: saved } + end + + private + + def notification_setting_params + params.require(:notification_setting).permit(:level) + end +end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index e457db2f0b77a5afbff61280a125c1c80fdaac94..33b2625c0ac5b09ed4e9db3da0a908eba50d3b86 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,6 +1,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController # Authorize - before_action :authorize_admin_project_member!, except: :leave + before_action :authorize_admin_project_member!, except: [:leave, :index] def index @project_members = @project.project_members diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 5c7614cfbaf4557928e9de20bd431eea12aa0207..bb7a6b6a5ab76faefbbb91de60eaf6eded6799b0 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -11,7 +11,6 @@ class Projects::RepositoriesController < Projects::ApplicationController end def archive - RepositoryArchiveCacheWorker.perform_async headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format])) head :ok rescue => ex diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 8b2577aebe1e537dc368cac0598bfc1154650c30..739681f4085b8e4594c3e2f47183760340dd4cec 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -6,7 +6,7 @@ class Projects::ServicesController < Projects::ApplicationController :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :colorize_messages, :channels, :push_events, :issues_events, :merge_requests_events, :tag_push_events, - :note_events, :build_events, + :note_events, :build_events, :wiki_page_events, :notify_only_broken_builds, :add_pusher, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 9f3a4a6972128dfd0a512e880969b0ad2ec6421d..c02bc28acef46af91f3a653ab4e3d9c5fe92163f 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -44,7 +44,7 @@ class Projects::WikisController < Projects::ApplicationController return render('empty') unless can?(current_user, :create_wiki, @project) - if @page.update(content, format, message) + if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page) redirect_to( namespace_project_wiki_path(@project.namespace, @project, @page), notice: 'Wiki was successfully updated.' @@ -55,9 +55,9 @@ class Projects::WikisController < Projects::ApplicationController end def create - @page = WikiPage.new(@project_wiki) + @page = WikiPages::CreateService.new(@project, current_user, wiki_params).execute - if @page.create(wiki_params) + if @page.persisted? redirect_to( namespace_project_wiki_path(@project.namespace, @project, @page), notice: 'Wiki was successfully updated.' @@ -122,15 +122,4 @@ class Projects::WikisController < Projects::ApplicationController params[:wiki].slice(:title, :content, :format, :message) end - def content - params[:wiki][:content] - end - - def format - params[:wiki][:format] - end - - def message - params[:wiki][:message] - end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 9fb4370ef90426ee999a71af13138f820832fca3..1d4684ca22ecd00c6b44963407365173b9c7ea0f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -101,14 +101,18 @@ class ProjectsController < Projects::ApplicationController respond_to do |format| format.html do + if current_user + @membership = @project.team.find_member(current_user.id) + + if @membership + @notification_setting = current_user.notification_settings_for(@project) + end + end + if @project.repository_exists? if @project.empty_repo? render 'projects/empty' else - if current_user - @membership = @project.team.find_member(current_user.id) - end - render :show end else diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8e7956da48f132b7991bdd697ae96f896c0c8568..2ae180c8a124d175ad883ed9f49b148f9e0ecdf6 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,6 +1,7 @@ class UsersController < ApplicationController skip_before_action :authenticate_user! - before_action :set_user + before_action :user + before_action :authorize_read_user!, only: [:show] def show respond_to do |format| @@ -75,22 +76,26 @@ class UsersController < ApplicationController private - def set_user - @user = User.find_by_username!(params[:username]) + def authorize_read_user! + render_404 unless can?(current_user, :read_user, user) + end + + def user + @user ||= User.find_by_username!(params[:username]) end def contributed_projects - ContributedProjectsFinder.new(@user).execute(current_user) + ContributedProjectsFinder.new(user).execute(current_user) end def contributions_calendar @contributions_calendar ||= Gitlab::ContributionsCalendar. - new(contributed_projects, @user) + new(contributed_projects, user) end def load_events # Get user activity feed for projects common for both users - @events = @user.recent_events. + @events = user.recent_events. merge(projects_for_current_user). references(:project). with_associations. @@ -99,16 +104,16 @@ class UsersController < ApplicationController def load_projects @projects = - PersonalProjectsFinder.new(@user).execute(current_user) + PersonalProjectsFinder.new(user).execute(current_user) .page(params[:page]) end def load_contributed_projects - @contributed_projects = contributed_projects.joined(@user) + @contributed_projects = contributed_projects.joined(user) end def load_groups - @groups = JoinedGroupsFinder.new(@user).execute(current_user) + @groups = JoinedGroupsFinder.new(user).execute(current_user) end def projects_for_current_user diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f1df6832bf661eda41d1a811b1614b5901950184..93aa30b325571113191a117550ed8af0df113cd2 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -39,6 +39,7 @@ class IssuableFinder items = by_assignee(items) items = by_author(items) items = by_label(items) + items = by_due_date(items) sort(items) end @@ -117,7 +118,7 @@ class IssuableFinder end def filter_by_no_label? - labels? && params[:label_name] == Label::None.title + labels? && params[:label_name].include?(Label::None.title) end def labels @@ -271,18 +272,55 @@ class IssuableFinder items = items.without_label else items = items.with_label(label_names) - if projects items = items.where(labels: { project_id: projects }) end end end + # When filtering by multiple labels we may end up duplicating issues (if one + # has multiple labels). This ensures we only return unique issues. + items.distinct + end + + def by_due_date(items) + if due_date? + if filter_by_no_due_date? + items = items.without_due_date + elsif filter_by_overdue? + items = items.due_before(Date.today) + elsif filter_by_due_this_week? + items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week) + elsif filter_by_due_this_month? + items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month) + end + end + items end + def filter_by_no_due_date? + due_date? && params[:due_date] == Issue::NoDueDate.name + end + + def filter_by_overdue? + due_date? && params[:due_date] == Issue::Overdue.name + end + + def filter_by_due_this_week? + due_date? && params[:due_date] == Issue::DueThisWeek.name + end + + def filter_by_due_this_month? + due_date? && params[:due_date] == Issue::DueThisMonth.name + end + + def due_date? + params[:due_date].present? && klass.column_names.include?('due_date') + end + def label_names - params[:label_name].split(',') + params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name] end def current_user_related? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e6ceb21353209a7f44a5fbb3456b90399dba115f..3e0074da39420974435ad6958f9737d57e3228d3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -184,7 +184,7 @@ module ApplicationHelper element = content_tag :time, time.to_s, class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}", datetime: time.to_time.getutc.iso8601, - title: time.in_time_zone.to_s(:medium), + title: time.to_time.in_time_zone.to_s(:medium), data: { toggle: 'tooltip', placement: placement, container: 'body' } unless skip_js @@ -254,11 +254,11 @@ module ApplicationHelper def page_filter_path(options = {}) without = options.delete(:without) + add_label = options.delete(:label) exist_opts = { state: params[:state], scope: params[:scope], - label_name: params[:label_name], milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], author_id: params[:author_id], @@ -275,6 +275,13 @@ module ApplicationHelper path = request.path path << "?#{options.to_param}" + if add_label + if params[:label_name].present? and params[:label_name].respond_to?('any?') + params[:label_name].each do |label| + path << "&label_name[]=#{label}" + end + end + end path end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 60a0ff32c9cdd87a7c18b79f5b753d4d3737df53..914b0ef6042626e417ef267371a7de32aa8a11a3 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -15,6 +15,10 @@ module ApplicationSettingsHelper current_application_settings.sign_in_text end + def shared_runners_text + current_application_settings.shared_runners_text + end + def user_oauth_applications? current_application_settings.user_oauth_applications end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 9e59a295fc469fa39f01b68e585b69412af822b9..a4d7c425d0f359b192bc8e526b957926f2b82d23 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -173,4 +173,15 @@ module BlobHelper response.etag = @blob.id !stale end + + def licenses_for_select + return @licenses_for_select if defined?(@licenses_for_select) + + licenses = Licensee::License.all + + @licenses_for_select = { + Popular: licenses.select(&:featured).map { |license| [license.name, license.key] }, + Other: licenses.reject(&:featured).map { |license| [license.name, license.key] } + } + end end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8b1575d5e0c5175ecfbf7a81b559f3756743bdf6..417050b41327b2f2c74ec4f70c240f6fae0acf4c 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -4,14 +4,6 @@ module CiStatusHelper builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha) end - def ci_status_icon(ci_commit) - ci_icon_for_status(ci_commit.status) - end - - def ci_status_label(ci_commit) - ci_label_for_status(ci_commit.status) - end - def ci_status_with_icon(status, target = nil) content = ci_icon_for_status(status) + ' '.html_safe + ci_label_for_status(status) klass = "ci-status ci-#{status}" @@ -47,10 +39,13 @@ module CiStatusHelper end def render_ci_status(ci_commit, tooltip_placement: 'auto left') - link_to ci_status_icon(ci_commit), + # TODO: split this method into + # - render_commit_status + # - render_pipeline_status + link_to ci_icon_for_status(ci_commit.status), ci_status_path(ci_commit), class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", - title: "Build #{ci_status_label(ci_commit)}", + title: "Build #{ci_label_for_status(ci_commit.status)}", data: { toggle: 'tooltip', placement: tooltip_placement } end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 35ba543cef12f42d10d781e1eb91c27017d9f60c..b59c3982edd169c07b53de27e1228822e82f24f7 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -126,12 +126,10 @@ module CommitsHelper def revert_commit_link(commit, continue_to_path, btn_class: nil) return unless current_user - tooltip = "Revert this #{revert_commit_type(commit)} in a new merge request" + tooltip = "Revert this #{commit.change_type_title} in a new merge request" if can_collaborate_with_project? - content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do - link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}" - end + link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip" elsif can?(current_user, :fork_project, @project) continue_params = { to: continue_to_path, @@ -146,11 +144,24 @@ module CommitsHelper end end - def revert_commit_type(commit) - if commit.merged_merge_request - 'merge request' - else - 'commit' + def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil) + return unless current_user + + tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request" + + if can_collaborate_with_project? + link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip" + elsif can?(current_user, :fork_project, @project) + continue_params = { + to: continue_to_path, + notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.', + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_forks_path(@project.namespace, @project, + namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to 'Cherry-pick', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip end end @@ -183,7 +194,7 @@ module CommitsHelper options = { class: "commit-#{options[:source]}-link has-tooltip", - data: { 'original-title'.to_sym => sanitize(source_email) } + title: source_email } if user.nil? diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index ff32e8344997b16120f1d314172f99f4c889775a..6a3ec83b8c0195ae30f93d507dab9c923d8b3755 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -40,10 +40,11 @@ module DiffHelper (unfold) ? 'unfold js-unfold' : '' end - def diff_line_content(line) + def diff_line_content(line, line_type = nil) if line.blank? " ".html_safe else + line[0] = ' ' if %w[new old].include?(line_type) line end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index f3fddef01cb8c0e7226779ebaebe5f66edf21eda..f07eff3fb57004511b910839d1d5a47a53714800 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -25,6 +25,10 @@ module GitlabRoutingHelper namespace_project_commits_path(project.namespace, project, @ref || project.repository.root_ref) end + def project_pipelines_path(project, *args) + namespace_project_pipelines_path(project.namespace, project, *args) + end + def project_builds_path(project, *args) namespace_project_builds_path(project.namespace, project, *args) end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..109bc1a02d19bdbc8c8140aca3dc3e54d9af4bde --- /dev/null +++ b/app/helpers/import_helper.rb @@ -0,0 +1,18 @@ +module ImportHelper + def github_project_link(path_with_namespace) + link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' + end + + private + + def github_project_url(path_with_namespace) + "#{github_root_url}/#{path_with_namespace}" + end + + def github_root_url + return @github_url if defined?(@github_url) + + provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' } + @github_url = provider.fetch('url', 'https://github.com') if provider + end +end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index b14b8218d02a1077681bb6d8ef39a575b12221d4..3947421728604a976bc8c0dbb91ee13b641ea77f 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -16,6 +16,25 @@ module IssuablesHelper base_issuable_scope(issuable).where('iid > ?', issuable.iid).last end + def multi_label_name(current_labels, default_label) + # current_labels may be a string from before + if current_labels.is_a?(Array) + if current_labels.count > 1 + "#{current_labels[0]} +#{current_labels.count - 1} more" + else + current_labels[0] + end + elsif current_labels.is_a?(String) + if current_labels.nil? || current_labels.empty? + default_label + else + current_labels + end + else + default_label + end + end + def issuable_json_path(issuable) project = issuable.project @@ -55,6 +74,15 @@ module IssuablesHelper h(milestone_title.presence || default_label) end + def issuable_meta(issuable, project, text) + output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier" + output << " opened #{time_ago_with_tooltip(issuable.created_at)} by".html_safe + output << content_tag(:strong) do + author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs") + author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg") + end + end + private def sidebar_gutter_collapsed? diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 4cb8adcebad4ae6010cb7a5c323074b3728a2ffe..afe1e11a0da6ad5cb9f576cd309e63d9d6bf9fc0 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -131,7 +131,7 @@ module IssuesHelper class: "icon emoji-icon emoji-#{unicode}", title: name, data: data - else + else # Emoji icons displayed separately, used for the awards already given # to an issue or merge request. content_tag :img, "", @@ -172,6 +172,18 @@ module IssuesHelper end.to_h end + def due_date_options + options = [ + Issue::AnyDueDate, + Issue::NoDueDate, + Issue::DueThisWeek, + Issue::DueThisMonth, + Issue::Overdue + ] + + options_from_collection_for_select(options, 'name', 'title', params[:due_date]) + end + # Required for Banzai::Filter::IssueReferenceFilter module_function :url_for_issue end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 499c655d2bf21991eb9955aff39ca948665cee64..54ab9179efc00f121ad3d920b800392e39bc0ccc 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,48 +1,48 @@ module NotificationsHelper include IconsHelper - def notification_icon(notification) - if notification.disabled? - icon('volume-off', class: 'ns-mute') - elsif notification.participating? - icon('volume-down', class: 'ns-part') - elsif notification.watch? - icon('volume-up', class: 'ns-watch') - else - icon('circle-o', class: 'ns-default') + def notification_icon_class(level) + case level.to_sym + when :disabled + 'microphone-slash' + when :participating + 'volume-up' + when :watch + 'eye' + when :mention + 'at' + when :global + 'globe' end end - def notification_list_item(notification_level, user_membership) - case notification_level - when Notification::N_DISABLED - update_notification_link(Notification::N_DISABLED, user_membership, 'Disabled', 'microphone-slash') - when Notification::N_PARTICIPATING - update_notification_link(Notification::N_PARTICIPATING, user_membership, 'Participate', 'volume-up') - when Notification::N_WATCH - update_notification_link(Notification::N_WATCH, user_membership, 'Watch', 'eye') - when Notification::N_MENTION - update_notification_link(Notification::N_MENTION, user_membership, 'On mention', 'at') - when Notification::N_GLOBAL - update_notification_link(Notification::N_GLOBAL, user_membership, 'Global', 'globe') - else - # do nothing - end + def notification_icon(level, text = nil) + icon("#{notification_icon_class(level)} fw", text: text) end - def update_notification_link(notification_level, user_membership, title, icon) - content_tag(:li, class: active_level_for(user_membership, notification_level)) do - link_to '#', class: 'update-notification', data: { notification_level: notification_level } do - icon("#{icon} fw", text: title) - end + def notification_title(level) + case level.to_sym + when :participating + 'Participate' + when :mention + 'On mention' + else + level.to_s.titlecase end end - def notification_label(user_membership) - Notification.new(user_membership).to_s - end + def notification_list_item(level, setting) + title = notification_title(level) + + data = { + notification_level: level, + notification_title: title + } - def active_level_for(user_membership, level) - 'active' if user_membership.notification_level == level + content_tag(:li, class: ('active' if setting.level == level)) do + link_to '#', class: 'update-notification', data: data do + notification_icon(level, title) + end + end end end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 82f805fa44449d578dec07a98abbe7d28f179f82..e4e8b934bc8299d64d640e0b18411cb3d74f8871 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -84,6 +84,14 @@ module PageLayoutHelper end end + def nav(name = nil) + if name + @nav = name + else + @nav + end + end + def fluid_layout(enabled = false) if @fluid_layout.nil? @fluid_layout = (current_user && current_user.layout == "fluid") || enabled diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 4e4c6e301d509e8c975698e545c0b07980a71f77..ab85694da3f36b7c8c09fcb3b8790e86177dd1f1 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -52,7 +52,7 @@ module ProjectsHelper link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe else title = opts[:title].sub(":name", sanitize(author.name)) - link_to(author_html, user_path(author), class: "author_link has-tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe + link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe end end @@ -65,21 +65,14 @@ module ProjectsHelper link_to(simple_sanitize(owner.name), user_path(owner)) end - project_link = link_to project_path(project), { class: "project-item-select-holder" } do - link_output = simple_sanitize(project.name) + project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } - if current_user - link_output += project_select_tag :project_path, - class: "project-item-select js-projects-dropdown", - data: { include_groups: false, order_by: 'last_activity_at' } - end - - link_output + if current_user + project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) end - project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle" if current_user - full_title = namespace_link + ' / ' + project_link - full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name + full_title = "#{namespace_link} / #{project_link}".html_safe + full_title << ' · '.html_safe << link_to(simple_sanitize(name), url) if name full_title end @@ -151,6 +144,10 @@ module ProjectsHelper nav_tabs << :settings end + if can?(current_user, :read_project_member, project) + nav_tabs << :team + end + if can?(current_user, :read_issue, project) nav_tabs << :issues end @@ -223,40 +220,14 @@ module ProjectsHelper end end - def add_contribution_guide_path(project) - if project && !project.repository.contribution_guide - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch, - file_name: "CONTRIBUTING.md", - commit_message: "Add contribution guide" - ) - end - end - - def add_changelog_path(project) - if project && !project.repository.changelog - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch, - file_name: "CHANGELOG", - commit_message: "Add changelog" - ) - end - end - - def add_license_path(project) - if project && !project.repository.license - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch, - file_name: "LICENSE", - commit_message: "Add license" - ) - end + def add_special_file_path(project, file_name:, commit_message: nil) + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch || 'master', + file_name: file_name, + commit_message: commit_message || "Add #{file_name.downcase}" + ) end def contribution_guide_path(project) @@ -279,7 +250,7 @@ module ProjectsHelper end def license_path(project) - filename_path(project, :license) + filename_path(project, :license_blob) end def version_path(project) @@ -313,6 +284,13 @@ module ProjectsHelper namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md') end + def new_license_path + ref = @repository.root_ref if @repository + ref ||= 'master' + + namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE') + end + def last_push_event if current_user current_user.recent_push(@project.id) @@ -342,6 +320,12 @@ module ProjectsHelper @ref || @repository.try(:root_ref) end + def license_short_name(project) + license = Licensee::License.new(project.repository.license_key) + + license.nickname || license.name + end + private def filename_path(project, filename) diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 05386d790ca887ba4c465b5f0ebcd846d7245c35..e951a87a2127665f4686c061a369ad04d1c9d550 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -2,30 +2,29 @@ module SelectsHelper def users_select_tag(id, opts = {}) css_class = "ajax-users-select " css_class << "multiselect " if opts[:multiple] + css_class << "skip_ldap " if opts[:skip_ldap] css_class << (opts[:class] || '') value = opts[:selected] || '' - placeholder = opts[:placeholder] || 'Search for a user' - null_user = opts[:null_user] || false - any_user = opts[:any_user] || false - email_user = opts[:email_user] || false first_user = opts[:first_user] && current_user ? current_user.username : false - current_user = opts[:current_user] || false - project = opts[:project] || @project html = { class: css_class, data: { - placeholder: placeholder, - null_user: null_user, - any_user: any_user, - email_user: email_user, + placeholder: opts[:placeholder] || 'Search for a user', + null_user: opts[:null_user] || false, + any_user: opts[:any_user] || false, + email_user: opts[:email_user] || false, first_user: first_user, - current_user: current_user + current_user: opts[:current_user] || false, + "push-code-to-protected-branches" => opts[:push_code_to_protected_branches], + author_id: opts[:author_id] || '' } } unless opts[:scope] == :all + project = opts[:project] || @project + if project html['data-project-id'] = project.id elsif @group diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 2f2d2721d6df0f775c6287dd0917f05ef08f7680..630e10ea892b62448a733a850c3190fd3c0c943f 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -8,6 +8,8 @@ module SortingHelper sort_value_oldest_created => sort_title_oldest_created, sort_value_milestone_soon => sort_title_milestone_soon, sort_value_milestone_later => sort_title_milestone_later, + sort_value_due_date_soon => sort_title_due_date_soon, + sort_value_due_date_later => sort_title_due_date_later, sort_value_largest_repo => sort_title_largest_repo, sort_value_recently_signin => sort_title_recently_signin, sort_value_oldest_signin => sort_title_oldest_signin, @@ -50,6 +52,14 @@ module SortingHelper 'Milestone due later' end + def sort_title_due_date_soon + 'Due soon' + end + + def sort_title_due_date_later + 'Due later' + end + def sort_title_name 'Name' end @@ -98,6 +108,14 @@ module SortingHelper 'milestone_due_desc' end + def sort_value_due_date_soon + 'due_date_asc' + end + + def sort_value_due_date_later + 'due_date_desc' + end + def sort_value_name 'name_asc' end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 04e53fe7c6100baabf913968a5d2bc5192a6ffbe..96a836710096e1f45be7cdab92df90f336ef7427 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -110,4 +110,12 @@ module TabHelper 'active' end end + + def profile_tab_class + if controller.controller_path =~ /\Aprofiles/ + return 'active' + end + + 'active' if current_controller?('oauth/applications') + end end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index edc5686cf08e68e294d78deb58e9bee0f986dffd..2f066682180e7eadbdc263f765ae61b5060ed21e 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -20,6 +20,8 @@ module TodosHelper end def todo_target_path(todo) + return unless todo.target.present? + anchor = dom_id(todo.note) if todo.note.present? if todo.for_commit? diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 4920ca5af6e170c175b8419368379e38cc20979c..dbedf417fa5a1eda4998901954b0257c00dcd42f 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -66,7 +66,7 @@ module TreeHelper ref else project = tree_edit_project(project) - project.repository.next_patch_branch + project.repository.next_branch('patch') end end diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 55bb4f652705826da7ab7d75a7c30f4d192dcb19..9dd11d20ea6b1129a98b3841108a74d3308a04fa 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -56,7 +56,7 @@ module Emails { from: sender(sender_id), to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (##{@merge_request.iid})") + subject: subject("#{@merge_request.title} (#{@merge_request.to_reference})") } end end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index f9650df9a743726eef6719cba80b5536496c38a7..cdc40b81ee1b0757a7794c6ed32bd75014d33304 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -38,7 +38,7 @@ module Emails { from: sender(@note.author_id), to: recipient(recipient_id), - subject: subject("#{@note.noteable.title} (##{@note.noteable.iid})") + subject: subject("#{@note.noteable.title} (#{@note.noteable.to_reference})") } end diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb new file mode 100644 index 0000000000000000000000000000000000000000..21db2fe04a037fa19a238969034ac12e075ee18f --- /dev/null +++ b/app/mailers/repository_check_mailer.rb @@ -0,0 +1,14 @@ +class RepositoryCheckMailer < BaseMailer + def notify(failed_count) + if failed_count == 1 + @message = "One project failed its last repository check" + else + @message = "#{failed_count} projects failed their last repository check" + end + + mail( + to: User.admins.pluck(:email), + subject: "GitLab Admin | #{@message}" + ) + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index c0bf6def7c538994d1de9469cad800e794d5b60a..6103a2947e2a6fd60455e93d58f1e45ac2e39980 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -18,6 +18,7 @@ class Ability when Namespace then namespace_abilities(user, subject) when GroupMember then group_member_abilities(user, subject) when ProjectMember then project_member_abilities(user, subject) + when User then user_abilities else [] end.concat(global_abilities(user)) end @@ -35,6 +36,8 @@ class Ability anonymous_project_abilities(subject) when subject.is_a?(Group) || subject.respond_to?(:group) anonymous_group_abilities(subject) + when subject.is_a?(User) + anonymous_user_abilities else [] end @@ -81,17 +84,17 @@ class Ability end def anonymous_group_abilities(subject) + rules = [] + group = if subject.is_a?(Group) subject else subject.group end - if group && group.public? - [:read_group] - else - [] - end + rules << :read_group if group.public? + + rules end def anonymous_personal_snippet_abilities(snippet) @@ -110,9 +113,14 @@ class Ability end end + def anonymous_user_abilities + [:read_user] unless restricted_public_level? + end + def global_abilities(user) rules = [] rules << :create_group if user.can_create_group + rules << :read_users_list rules end @@ -163,7 +171,7 @@ class Ability @public_project_rules ||= project_guest_rules + [ :download_code, :fork_project, - :read_commit_status, + :read_commit_status ] end @@ -284,7 +292,6 @@ class Ability def group_abilities(user, group) rules = [] - rules << :read_group if can_read_group?(user, group) # Only group masters and group owners can create new projects @@ -456,6 +463,10 @@ class Ability rules end + def user_abilities + [:read_user] + end + def abilities @abilities ||= begin abilities = Six.new @@ -470,6 +481,10 @@ class Ability private + def restricted_public_level? + current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) + end + def named_abilities(name) [ :"read_#{name}", diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 052cd874733fb8352c475e2f1520cca505747cb7..36f88154232319605450d3b84beac6878c3e8210 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -153,7 +153,8 @@ class ApplicationSetting < ActiveRecord::Base require_two_factor_authentication: false, two_factor_grace_period: 48, recaptcha_enabled: false, - akismet_enabled: false + akismet_enabled: false, + repository_checks_enabled: true, ) end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 7d33838044b556846c754bbfdd50917fd138d20f..553cd44797193ef7a8d8238f2a53a30afb9f2bb1 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -37,8 +37,6 @@ module Ci class Build < CommitStatus - LAZY_ATTRIBUTES = ['trace'] - belongs_to :runner, class_name: 'Ci::Runner' belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' belongs_to :erased_by, class_name: 'User' @@ -50,25 +48,17 @@ module Ci scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } - scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader acts_as_taggable - # To prevent db load megabytes of data from trace - default_scope -> { select(Ci::Build.columns_without_lazy) } - before_destroy { project } - class << self - def columns_without_lazy - (column_names - LAZY_ATTRIBUTES).map do |column_name| - "#{table_name}.#{column_name}" - end - end + after_create :execute_hooks + class << self def last_month where('created_at > ?', Date.today - 1.month) end @@ -126,12 +116,16 @@ module Ci end def retried? - !self.commit.latest_statuses_for_ref(self.ref).include?(self) + !self.commit.statuses.latest.include?(self) + end + + def retry + Ci::Build.retry(self) end def depends_on_builds # Get builds of the same type - latest_builds = self.commit.builds.similar(self).latest + latest_builds = self.commit.builds.latest # Return builds from previous stages latest_builds.where('stage_idx < ?', stage_idx) @@ -230,12 +224,33 @@ module Ci end end + def trace_length + if raw_trace + raw_trace.length + else + 0 + end + end + def trace=(trace) + recreate_trace_dir + File.write(path_to_trace, trace) + end + + def recreate_trace_dir unless Dir.exists?(dir_to_trace) FileUtils.mkdir_p(dir_to_trace) end + end + private :recreate_trace_dir - File.write(path_to_trace, trace) + def append_trace(trace_part, offset) + recreate_trace_dir + + File.truncate(path_to_trace, offset) if File.exist?(path_to_trace) + File.open(path_to_trace, 'a') do |f| + f.write(trace_part) + end end def dir_to_trace @@ -365,11 +380,23 @@ module Ci self.update(erased_by: user, erased_at: Time.now) end - private - def yaml_variables + global_yaml_variables + job_yaml_variables + end + + def global_yaml_variables + if commit.config_processor + commit.config_processor.global_variables.map do |key, value| + { key: key, value: value, public: true } + end + else + [] + end + end + + def job_yaml_variables if commit.config_processor - commit.config_processor.variables.map do |key, value| + commit.config_processor.job_variables(name).map do |key, value| { key: key, value: value, public: true } end else diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index f4cf7034b14a3a37db58edba331cedc5bf88bb06..f2667e5476b7896d66fede910ddf1ad83f844914 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -19,21 +19,28 @@ module Ci class Commit < ActiveRecord::Base extend Ci::Model + include Statuseable belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id has_many :statuses, class_name: 'CommitStatus' has_many :builds, class_name: 'Ci::Build' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + delegate :stages, to: :statuses + validates_presence_of :sha + validates_presence_of :status validate :valid_commit_sha + # Invalidate object and save if when touched + after_touch :update_state + def self.truncate_sha(sha) sha[0...8] end - def to_param - sha + def self.stages + CommitStatus.where(commit: all).stages end def project_id @@ -68,15 +75,20 @@ module Ci nil end - def stage - running_or_pending = statuses.latest.running_or_pending.ordered - running_or_pending.first.try(:stage) + def branch? + !tag? + end + + def retryable? + builds.latest.any? do |build| + build.failed? && build.retryable? + end end - def create_builds(ref, tag, user, trigger_request = nil) + def create_builds(user, trigger_request = nil) return unless config_processor config_processor.stages.any? do |stage| - CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request, 'success').present? + CreateBuildsService.new(self).execute(stage, user, 'success', trigger_request).present? end end @@ -84,7 +96,7 @@ module Ci return unless config_processor # don't create other builds if this one is retried - latest_builds = builds.similar(build).latest + latest_builds = builds.latest return unless latest_builds.exists?(build.id) # get list of stages after this build @@ -92,88 +104,21 @@ module Ci next_stages.delete(build.stage) # get status for all prior builds - prior_builds = latest_builds.reject { |other_build| next_stages.include?(other_build.stage) } - status = Ci::Status.get_status(prior_builds) + prior_builds = latest_builds.where.not(stage: next_stages) + prior_status = prior_builds.status # create builds for next stages based next_stages.any? do |stage| - CreateBuildsService.new.execute(self, stage, build.ref, build.tag, build.user, build.trigger_request, status).present? + CreateBuildsService.new(self).execute(stage, build.user, prior_status, build.trigger_request).present? end end - def refs - statuses.order(:ref).pluck(:ref).uniq - end - - def latest_statuses - @latest_statuses ||= statuses.latest.to_a - end - - def latest_statuses_for_ref(ref) - latest_statuses.select { |status| status.ref == ref } - end - - def matrix_builds(build = nil) - matrix_builds = builds.latest.ordered - matrix_builds = matrix_builds.similar(build) if build - matrix_builds.to_a - end - def retried @retried ||= (statuses.order(id: :desc) - statuses.latest) end - def status - if yaml_errors.present? - return 'failed' - end - - @status ||= Ci::Status.get_status(latest_statuses) - end - - def pending? - status == 'pending' - end - - def running? - status == 'running' - end - - def success? - status == 'success' - end - - def failed? - status == 'failed' - end - - def canceled? - status == 'canceled' - end - - def active? - running? || pending? - end - - def complete? - canceled? || success? || failed? - end - - def duration - duration_array = statuses.map(&:duration).compact - duration_array.reduce(:+).to_i - end - - def started_at - @started_at ||= statuses.order('started_at ASC').first.try(:started_at) - end - - def finished_at - @finished_at ||= statuses.order('finished_at DESC').first.try(:finished_at) - end - def coverage - coverage_array = latest_statuses.map(&:coverage).compact + coverage_array = statuses.latest.map(&:coverage).compact if coverage_array.size >= 1 '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) end @@ -181,23 +126,29 @@ module Ci def config_processor return nil unless ci_yaml_file - @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace) - rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e - save_yaml_error(e.message) - nil - rescue - save_yaml_error("Undefined error") - nil + return @config_processor if defined?(@config_processor) + + @config_processor ||= begin + Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace) + rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e + save_yaml_error(e.message) + nil + rescue + save_yaml_error("Undefined error") + nil + end end def ci_yaml_file + return @ci_yaml_file if defined?(@ci_yaml_file) + @ci_yaml_file ||= begin blob = project.repository.blob_at(sha, '.gitlab-ci.yml') blob.load_all_data!(project.repository) blob.data + rescue + nil end - rescue - nil end def skip_ci? @@ -206,10 +157,23 @@ module Ci private + def update_state + statuses.reload + self.status = if yaml_errors.blank? + statuses.latest.status || 'skipped' + else + 'failed' + end + self.started_at = statuses.started_at + self.finished_at = statuses.finished_at + self.duration = statuses.latest.duration + save + end + def save_yaml_error(error) return if self.yaml_errors? self.yaml_errors = error - save + update_state end end end diff --git a/app/models/commit.rb b/app/models/commit.rb index d09876a07d92842a5d93896fbe80f3251f75b0f1..562c3ed15b211513743cc86dea3addfcf13a21cd 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -15,8 +15,8 @@ class Commit DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] # Commits above this size will not be rendered in HTML - DIFF_HARD_LIMIT_FILES = 1000 unless defined?(DIFF_HARD_LIMIT_FILES) - DIFF_HARD_LIMIT_LINES = 50000 unless defined?(DIFF_HARD_LIMIT_LINES) + DIFF_HARD_LIMIT_FILES = 1000 + DIFF_HARD_LIMIT_LINES = 50000 class << self def decorate(commits, project) @@ -150,13 +150,11 @@ class Commit end def hook_attrs(with_changed_files: false) - path_with_namespace = project.path_with_namespace - data = { id: id, message: safe_message, timestamp: committed_date.xmlschema, - url: "#{Gitlab.config.gitlab.url}/#{path_with_namespace}/commit/#{id}", + url: Gitlab::UrlBuilder.build(self), author: { name: author_name, email: author_email @@ -209,17 +207,22 @@ class Commit @raw.short_id(7) end - def ci_commit - project.ci_commit(sha) + def ci_commits + @ci_commits ||= project.ci_commits.where(sha: sha) end def status - ci_commit.try(:status) || :not_found + return @status if defined?(@status) + @status ||= ci_commits.status end def revert_branch_name "revert-#{short_id}" end + + def cherry_pick_branch_name + project.repository.next_branch("cherry-pick-#{short_id}", mild: true) + end def revert_description if merged_merge_request @@ -255,6 +258,10 @@ class Commit end.any? { |commit_ref| commit_ref.reverts_commit?(self) } end + def change_type_title + merged_merge_request ? 'merge request' : 'commit' + end + private def repo_changes diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 3377a85a55a7a103a451d44baf6060398d76223b..aa56314aa164a03886d55fa93dbc3e8cf27117fc 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -33,30 +33,23 @@ # class CommitStatus < ActiveRecord::Base + include Statuseable + self.table_name = 'ci_builds' belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - belongs_to :commit, class_name: 'Ci::Commit' + belongs_to :commit, class_name: 'Ci::Commit', touch: true belongs_to :user validates :commit, presence: true - validates :status, inclusion: { in: %w(pending running failed success canceled) } validates_presence_of :name alias_attribute :author, :user - scope :running, -> { where(status: 'running') } - scope :pending, -> { where(status: 'pending') } - scope :success, -> { where(status: 'success') } - scope :failed, -> { where(status: 'failed') } - scope :running_or_pending, -> { where(status: [:running, :pending]) } - scope :finished, -> { where(status: [:success, :failed, :canceled]) } - scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) } + scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) } scope :ordered, -> { order(:ref, :stage_idx, :name) } - scope :for_ref, ->(ref) { where(ref: ref) } - - AVAILABLE_STATUSES = ['pending', 'running', 'success', 'failed', 'canceled'] + scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } state_machine :status, initial: :pending do event :run do @@ -86,31 +79,24 @@ class CommitStatus < ActiveRecord::Base after_transition [:pending, :running] => :success do |commit_status| MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status) end - - state :pending, value: 'pending' - state :running, value: 'running' - state :failed, value: 'failed' - state :success, value: 'success' - state :canceled, value: 'canceled' end - delegate :sha, :short_sha, to: :commit, prefix: false + delegate :sha, :short_sha, to: :commit - # TODO: this should be removed with all references def before_sha - Gitlab::Git::BLANK_SHA + commit.before_sha || Gitlab::Git::BLANK_SHA end - def started? - !pending? && !canceled? && started_at + def self.stages + order_by = 'max(stage_idx)' + group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact end - def active? - running? || pending? - end - - def complete? - canceled? || success? || failed? + def self.stages_status + all.stages.inject({}) do |h, stage| + h[stage] = all.where(stage: stage).status + h + end end def ignored? @@ -118,11 +104,13 @@ class CommitStatus < ActiveRecord::Base end def duration - if started_at && finished_at - finished_at - started_at - elsif started_at - Time.now - started_at - end + duration = + if started_at && finished_at + finished_at - started_at + elsif started_at + Time.now - started_at + end + duration end def stuck? diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb index 51288094ef1fa02ac083e5e6e6fefced1f2bab73..5382dde67654b528847a58dde8cd806d593d0a49 100644 --- a/app/models/concerns/internal_id.rb +++ b/app/models/concerns/internal_id.rb @@ -7,11 +7,13 @@ module InternalId end def set_iid - records = project.send(self.class.name.tableize) - records = records.with_deleted if self.paranoid? - max_iid = records.maximum(:iid) + if iid.blank? + records = project.send(self.class.name.tableize) + records = records.with_deleted if self.paranoid? + max_iid = records.maximum(:iid) - self.iid = max_iid.to_i + 1 + self.iid = max_iid.to_i + 1 + end end def to_param diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index afa2ca039aef1cccfb97265a92c706d26199760d..d5166e814743693c82c5b638f7acccc057c121cf 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -37,7 +37,6 @@ module Issuable scope :closed, -> { with_state(:closed) } scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') } scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') } - scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) } scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :join_project, -> { joins(:project) } @@ -122,6 +121,14 @@ module Issuable joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC") end + + def with_label(title) + if title.is_a?(Array) && title.count > 1 + joins(:labels).where(labels: { title: title }).group('issues.id').having("count(distinct labels.title) = #{title.count}") + else + joins(:labels).where(labels: { title: title }) + end + end end def today? diff --git a/app/models/concerns/notifiable.rb b/app/models/concerns/notifiable.rb deleted file mode 100644 index d7dcd97911dd4825987855a38d5723e6550180d4..0000000000000000000000000000000000000000 --- a/app/models/concerns/notifiable.rb +++ /dev/null @@ -1,15 +0,0 @@ -# == Notifiable concern -# -# Contains notification functionality -# -module Notifiable - extend ActiveSupport::Concern - - included do - validates :notification_level, inclusion: { in: Notification.project_notification_levels }, presence: true - end - - def notification - @notification ||= Notification.new(self) - end -end diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb new file mode 100644 index 0000000000000000000000000000000000000000..8a293b7b76e981334236efaebfd84ed8f46dbbdc --- /dev/null +++ b/app/models/concerns/statuseable.rb @@ -0,0 +1,81 @@ +module Statuseable + extend ActiveSupport::Concern + + AVAILABLE_STATUSES = %w(pending running success failed canceled skipped) + + class_methods do + def status_sql + builds = all.select('count(*)').to_sql + success = all.success.select('count(*)').to_sql + ignored = all.ignored.select('count(*)').to_sql if all.respond_to?(:ignored) + ignored ||= '0' + pending = all.pending.select('count(*)').to_sql + running = all.running.select('count(*)').to_sql + canceled = all.canceled.select('count(*)').to_sql + skipped = all.skipped.select('count(*)').to_sql + + deduce_status = "(CASE + WHEN (#{builds})=0 THEN NULL + WHEN (#{builds})=(#{success})+(#{ignored}) THEN 'success' + WHEN (#{builds})=(#{pending}) THEN 'pending' + WHEN (#{builds})=(#{canceled}) THEN 'canceled' + WHEN (#{builds})=(#{skipped}) THEN 'skipped' + WHEN (#{running})+(#{pending})>0 THEN 'running' + ELSE 'failed' + END)" + + deduce_status + end + + def status + all.pluck(self.status_sql).first + end + + def duration + duration_array = all.map(&:duration).compact + duration_array.reduce(:+) + end + + def started_at + all.minimum(:started_at) + end + + def finished_at + all.maximum(:finished_at) + end + end + + included do + validates :status, inclusion: { in: AVAILABLE_STATUSES } + + state_machine :status, initial: :pending do + state :pending, value: 'pending' + state :running, value: 'running' + state :failed, value: 'failed' + state :success, value: 'success' + state :canceled, value: 'canceled' + state :skipped, value: 'skipped' + end + + scope :running, -> { where(status: 'running') } + scope :pending, -> { where(status: 'pending') } + scope :success, -> { where(status: 'success') } + scope :failed, -> { where(status: 'failed') } + scope :canceled, -> { where(status: 'canceled') } + scope :skipped, -> { where(status: 'skipped') } + scope :running_or_pending, -> { where(status: [:running, :pending]) } + scope :finished, -> { where(status: [:success, :failed, :canceled]) } + end + + def started? + !pending? && !canceled? && started_at + end + + def active? + running? || pending? + end + + def complete? + canceled? || success? || failed? + end +end diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index b8585d4e57742781cc6b73759f9b386eb7e8612a..b7894c99846040345641c5bd16ccb52441b943c5 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -37,4 +37,10 @@ class ExternalIssue def to_reference(_from_project = nil) id end + + def reference_link_text(from_project = nil) + return "##{id}" if /^\d+$/.match(id) + + id + end end diff --git a/app/models/group.rb b/app/models/group.rb index b332601c59b78bc70e1904aa644fb626034c05f0..1f8432e3320222e0904c5780fb963d0738680ab8 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -15,7 +15,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class Group < Namespace include Gitlab::ConfigHelper @@ -27,6 +26,7 @@ class Group < Namespace has_many :users, through: :group_members has_many :project_group_links, dependent: :destroy has_many :shared_projects, through: :project_group_links, source: :project + has_many :notification_settings, dependent: :destroy, as: :source validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :visibility_level_allowed_by_projects diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index fe923fafbe066dcc98d53939d9b317c37f9e553a..bc6e0f98c3c5ebb1bd24560f29463cbd88be1778 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -21,10 +21,9 @@ class ProjectHook < WebHook belongs_to :project - scope :push_hooks, -> { where(push_events: true) } - scope :tag_push_hooks, -> { where(tag_push_events: true) } scope :issue_hooks, -> { where(issues_events: true) } scope :note_hooks, -> { where(note_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) } scope :build_hooks, -> { where(build_events: true) } + scope :wiki_page_hooks, -> { where(wiki_page_events: true) } end diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index c147d8762a91cbbcc1f79e6831955de46d409cde..15dddcc2447f517894535810618dc219ac66c438 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -19,4 +19,7 @@ # class SystemHook < WebHook + def async_execute(data, hook_name) + Sidekiq::Client.enqueue(SystemHookWorker, id, data, hook_name) + end end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 7a13c3f0a3976304fe7f921ea6f0c6e43a7ce8be..3a2e4f546f72e4c7c7ebacd7e16e4213e7a93e44 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -30,6 +30,9 @@ class WebHook < ActiveRecord::Base default_value_for :build_events, false default_value_for :enable_ssl_verification, true + scope :push_hooks, -> { where(push_events: true) } + scope :tag_push_hooks, -> { where(tag_push_events: true) } + # HTTParty timeout default_timeout Gitlab.config.gitlab.webhook_timeout diff --git a/app/models/issue.rb b/app/models/issue.rb index e064b0f8b955bb0c52f9aa0d90e9cf68dd8894e7..ea1bfb776ee26fec7d58044373e916d9c7154dca 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -20,7 +20,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class Issue < ActiveRecord::Base include InternalId @@ -29,6 +28,13 @@ class Issue < ActiveRecord::Base include Sortable include Taskable + DueDateStruct = Struct.new(:title, :name).freeze + NoDueDate = DueDateStruct.new('No Due Date', '0').freeze + AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze + Overdue = DueDateStruct.new('Overdue', 'overdue').freeze + DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze + DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze + ActsAsTaggableOn.strict_case_match = true belongs_to :project @@ -40,6 +46,13 @@ class Issue < ActiveRecord::Base scope :open_for, ->(user) { opened.assigned_to(user) } scope :in_projects, ->(project_ids) { where(project_id: project_ids) } + scope :without_due_date, -> { where(due_date: nil) } + scope :due_before, ->(date) { where('issues.due_date < ?', date) } + scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) } + + scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } + scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } + state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -83,6 +96,15 @@ class Issue < ActiveRecord::Base @link_reference_pattern ||= super("issues", /(?<issue>\d+)/) end + def self.sort(method) + case method.to_s + when 'due_date_asc' then order_due_date_asc + when 'due_date_desc' then order_due_date_desc + else + super + end + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" @@ -104,10 +126,16 @@ class Issue < ActiveRecord::Base end end - def related_branches - project.repository.branch_names.select do |branch| - branch.end_with?("-#{iid}") + # All branches containing the current issue's ID, except for + # those with a merge request open referencing the current issue. + def related_branches(current_user) + branches_with_iid = project.repository.branch_names.select do |branch| + branch =~ /\A#{iid}-(?!\d+-stable)/i end + + branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch) + + branches_with_iid - branches_with_merge_request end # Reset issue events cache @@ -151,13 +179,21 @@ class Issue < ActiveRecord::Base end def to_branch_name - "#{title.parameterize}-#{iid}" + if self.confidential? + "#{iid}-confidential-issue" + else + "#{iid}-#{title.parameterize}" + end end def can_be_worked_on?(current_user) !self.closed? && !self.project.forked? && - self.related_branches.empty? && + self.related_branches(current_user).empty? && self.closed_by_merge_requests(current_user).empty? end + + def overdue? + due_date.try(:past?) || false + end end diff --git a/app/models/label.rb b/app/models/label.rb index 55c01cae76261939656170c600f719702f65b101..60bdce32952ed15791d076cb00dd02b3ce3e98e4 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -113,6 +113,10 @@ class Label < ActiveRecord::Base template end + def text_color + LabelsHelper::text_color_for_bg(self.color) + end + private def label_format_reference(format = :id) diff --git a/app/models/member.rb b/app/models/member.rb index ca08007b7eb9ef032f673589dd348a9aa2f43e87..60efafef211791648a2420a7988a359873f0ed58 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -19,7 +19,6 @@ class Member < ActiveRecord::Base include Sortable - include Notifiable include Gitlab::Access attr_accessor :raw_invite_token @@ -56,12 +55,15 @@ class Member < ActiveRecord::Base before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } after_create :send_invite, if: :invite? + after_create :create_notification_setting, unless: :invite? after_create :post_create_hook, unless: :invite? after_update :post_update_hook, unless: :invite? after_destroy :post_destroy_hook, unless: :invite? delegate :name, :username, :email, to: :user, prefix: true + default_value_for :notification_level, NotificationSetting.levels[:global] + class << self def find_by_invite_token(invite_token) invite_token = Devise.token_generator.digest(self, :invite_token, invite_token) @@ -160,6 +162,14 @@ class Member < ActiveRecord::Base send_invite end + def create_notification_setting + user.notification_settings.find_or_create_for(source) + end + + def notification_setting + @notification_setting ||= user.notification_settings_for(source) + end + private def send_invite diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 65d2ea00570db9e0a440ea872c382f67f0504cd2..9fb474a1a93d8fd54dc278d789cfa13485c1c8af 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -24,7 +24,6 @@ class GroupMember < Member # Make sure group member points only to group as it source default_value_for :source_type, SOURCE_TYPE - default_value_for :notification_level, Notification::N_GLOBAL validates_format_of :source_type, with: /\ANamespace\z/ default_scope { where(source_type: SOURCE_TYPE) } diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 560d1690e1432de9698378c9515eddccc60d81a9..07ddb02ae9dd6bba8da9b3ea4947d222ca60bc83 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -27,7 +27,6 @@ class ProjectMember < Member # Make sure project member points only to project as it source default_value_for :source_type, SOURCE_TYPE - default_value_for :notification_level, Notification::N_GLOBAL validates_format_of :source_type, with: /\AProject\z/ default_scope { where(source_type: SOURCE_TYPE) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index bf185cb5dd8bb52ba400bf2ab6c5168486bb70e6..97dbfba538346b49af182bcf3c8cb60b5e94fbee 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -27,9 +27,6 @@ # merge_commit_sha :string # -require Rails.root.join("app/models/commit") -require Rails.root.join("lib/static_model") - class MergeRequest < ActiveRecord::Base include InternalId include Issuable @@ -45,11 +42,13 @@ class MergeRequest < ActiveRecord::Base serialize :merge_params, Hash - after_create :create_merge_request_diff + after_create :create_merge_request_diff, unless: :importing after_update :update_merge_request_diff delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil + attr_accessor :importing + # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests attr_accessor :allow_broken @@ -123,12 +122,12 @@ class MergeRequest < ActiveRecord::Base end end - validates :source_project, presence: true, unless: :allow_broken + validates :source_project, presence: true, unless: [:allow_broken, :importing] validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true validates :merge_user, presence: true, if: :merge_when_build_succeeds? - validate :validate_branches + validate :validate_branches, unless: [:allow_broken, :importing] validate :validate_fork scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } @@ -218,7 +217,7 @@ class MergeRequest < ActiveRecord::Base end if opened? || reopened? - similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened + similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id if similar_mrs.any? errors.add :validate_branches, @@ -345,7 +344,7 @@ class MergeRequest < ActiveRecord::Base def hook_attrs attrs = { - source: source_project.hook_attrs, + source: source_project.try(:hook_attrs), target: target_project.hook_attrs, last_commit: nil, work_in_progress: work_in_progress? @@ -589,7 +588,7 @@ class MergeRequest < ActiveRecord::Base end def ci_commit - @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project + @ci_commit ||= source_project.ci_commit(last_commit.id, source_branch) if last_commit && source_project end def diff_refs @@ -605,4 +604,8 @@ class MergeRequest < ActiveRecord::Base def can_be_reverted?(current_user = nil) merge_commit && !merge_commit.has_been_reverted?(current_user, self) end + + def can_be_cherry_picked? + merge_commit + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 33884118595cd39ea9c9340bea090a657daf5972..9de07db5b77ca497c572a897378284cb8caa9a38 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -11,8 +11,6 @@ # updated_at :datetime # -require Rails.root.join("app/models/commit") - class MergeRequestDiff < ActiveRecord::Base include Sortable @@ -37,7 +35,9 @@ class MergeRequestDiff < ActiveRecord::Base serialize :st_commits serialize :st_diffs - after_create :reload_content + after_create :reload_content, unless: :importing + + attr_accessor :importing def reload_content reload_commits diff --git a/app/models/note.rb b/app/models/note.rb index 87ced65c65027deab49dd0c70d47e7feeef7ffb0..71b4293d57a593fb98896fd4a49b6dee79571575 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -20,7 +20,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class Note < ActiveRecord::Base include Gitlab::CurrentSettings diff --git a/app/models/notification.rb b/app/models/notification.rb deleted file mode 100644 index 171b8df45c2fd05bed7daf0c0eff06e375bb2ec0..0000000000000000000000000000000000000000 --- a/app/models/notification.rb +++ /dev/null @@ -1,77 +0,0 @@ -class Notification - # - # Notification levels - # - N_DISABLED = 0 - N_PARTICIPATING = 1 - N_WATCH = 2 - N_GLOBAL = 3 - N_MENTION = 4 - - attr_accessor :target - - class << self - def notification_levels - [N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH] - end - - def options_with_labels - { - disabled: N_DISABLED, - participating: N_PARTICIPATING, - watch: N_WATCH, - mention: N_MENTION, - global: N_GLOBAL - } - end - - def project_notification_levels - [N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH, N_GLOBAL] - end - end - - def initialize(target) - @target = target - end - - def disabled? - target.notification_level == N_DISABLED - end - - def participating? - target.notification_level == N_PARTICIPATING - end - - def watch? - target.notification_level == N_WATCH - end - - def global? - target.notification_level == N_GLOBAL - end - - def mention? - target.notification_level == N_MENTION - end - - def level - target.notification_level - end - - def to_s - case level - when N_DISABLED - 'Disabled' - when N_PARTICIPATING - 'Participating' - when N_WATCH - 'Watching' - when N_MENTION - 'On mention' - when N_GLOBAL - 'Global' - else - # do nothing - end - end -end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb new file mode 100644 index 0000000000000000000000000000000000000000..5001738f411f43a05c678d7edf6d0fd85bbda8d6 --- /dev/null +++ b/app/models/notification_setting.rb @@ -0,0 +1,28 @@ +class NotificationSetting < ActiveRecord::Base + enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 } + + default_value_for :level, NotificationSetting.levels[:global] + + belongs_to :user + belongs_to :source, polymorphic: true + + validates :user, presence: true + validates :source, presence: true + validates :level, presence: true + validates :user_id, uniqueness: { scope: [:source_type, :source_id], + message: "already exists in source", + allow_nil: true } + + scope :for_groups, -> { where(source_type: 'Namespace') } + scope :for_projects, -> { where(source_type: 'Project') } + + def self.find_or_create_for(source) + setting = find_or_initialize_by(source: source) + + unless setting.persisted? + setting.save + end + + setting + end +end diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb new file mode 100644 index 0000000000000000000000000000000000000000..c78c7f4aa0e3eb0c3cbd3213b348b2b0e9eb00b9 --- /dev/null +++ b/app/models/oauth_access_token.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: oauth_access_tokens +# +# id :integer not null, primary key +# resource_owner_id :integer +# application_id :integer +# token :string not null +# refresh_token :string +# expires_in :integer +# revoked_at :datetime +# created_at :datetime not null +# scopes :string +# + +class OauthAccessToken < ActiveRecord::Base + belongs_to :resource_owner, class_name: 'User' + belongs_to :application, class_name: 'Doorkeeper::Application' +end diff --git a/app/models/project.rb b/app/models/project.rb index 3e1f04b41585627fd4e7e3ac0206c4587a905204..7aa21b19e674f19d999840e9366079ad712a8840 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -40,7 +40,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class Project < ActiveRecord::Base include Gitlab::ConfigHelper @@ -154,6 +153,7 @@ class Project < ActiveRecord::Base has_many :project_group_links, dependent: :destroy has_many :invited_groups, through: :project_group_links, source: :group has_many :todos, dependent: :destroy + has_many :notification_settings, dependent: :destroy, as: :source has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" @@ -388,9 +388,15 @@ class Project < ActiveRecord::Base def add_import_job if forked? - RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) + job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) else - RepositoryImportWorker.perform_async(self.id) + job_id = RepositoryImportWorker.perform_async(self.id) + end + + if job_id + Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}" + else + Rails.logger.error "Import job failed to start for #{path_with_namespace}" end end @@ -402,6 +408,35 @@ class Project < ActiveRecord::Base self.import_data.destroy if self.import_data end + def import_url=(value) + import_url = Gitlab::ImportUrl.new(value) + create_or_update_import_data(credentials: import_url.credentials) + super(import_url.sanitized_url) + end + + def import_url + if import_data && super + import_url = Gitlab::ImportUrl.new(super, credentials: import_data.credentials) + import_url.full_url + else + super + end + end + + def create_or_update_import_data(data: nil, credentials: nil) + project_import_data = import_data || build_import_data + if data + project_import_data.data ||= {} + project_import_data.data = project_import_data.data.merge(data) + end + if credentials + project_import_data.credentials ||= {} + project_import_data.credentials = project_import_data.credentials.merge(credentials) + end + + project_import_data.save + end + def import? external_import? || forked? end @@ -795,8 +830,8 @@ class Project < ActiveRecord::Base end end - def hook_attrs - { + def hook_attrs(backward: true) + attrs = { name: name, description: description, web_url: web_url, @@ -807,12 +842,19 @@ class Project < ActiveRecord::Base visibility_level: visibility_level, path_with_namespace: path_with_namespace, default_branch: default_branch, - # Backward compatibility - homepage: web_url, - url: url_to_repo, - ssh_url: ssh_url_to_repo, - http_url: http_url_to_repo } + + # Backward compatibility + if backward + attrs.merge!({ + homepage: web_url, + url: url_to_repo, + ssh_url: ssh_url_to_repo, + http_url: http_url_to_repo + }) + end + + attrs end # Reset events cache related to this project @@ -858,7 +900,9 @@ class Project < ActiveRecord::Base def change_head(branch) repository.before_change_head - gitlab_shell.update_repository_head(self.path_with_namespace, branch) + repository.rugged.references.create('HEAD', + "refs/heads/#{branch}", + force: true) reload_default_branch end @@ -919,12 +963,12 @@ class Project < ActiveRecord::Base !namespace.share_with_group_lock end - def ci_commit(sha) - ci_commits.find_by(sha: sha) + def ci_commit(sha, ref) + ci_commits.order(id: :desc).find_by(sha: sha, ref: ref) end - def ensure_ci_commit(sha) - ci_commit(sha) || ci_commits.create(sha: sha) + def ensure_ci_commit(sha, ref) + ci_commit(sha, ref) || ci_commits.create(sha: sha, ref: ref) end def enable_ci diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index cd3319f077e136f07d934875c14593202ce52ca6..2c0ae312f1babbf034f49b686d224d3dcff5e471 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -8,12 +8,23 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class ProjectImportData < ActiveRecord::Base belongs_to :project - + attr_encrypted :credentials, + key: Gitlab::Application.secrets.db_key_base, + marshal: true, + encode: true, + mode: :per_attribute_iv_and_salt + serialize :data, JSON validates :project, presence: true + + before_validation :symbolize_credentials + + def symbolize_credentials + # bang doesn't work here - attr_encrypted makes it not to work + self.credentials = self.credentials.deep_symbolize_keys unless self.credentials.blank? + end end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 9e7f642180e6926618a5c676de7860294c52e360..060062aaf7ad94bf96a77f8980cebcbbf1042b07 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -82,17 +82,17 @@ class BambooService < CiService end def build_info(sha) - url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}") + url = URI.join(bamboo_url, "/rest/api/latest/result?label=#{sha}").to_s if username.blank? && password.blank? - @response = HTTParty.get(parsed_url.to_s, verify: false) + @response = HTTParty.get(url, verify: false) else - get_url = "#{url}&os_authType=basic" + url << '&os_authType=basic' auth = { - username: username, - password: password, + username: username, + password: password } - @response = HTTParty.get(get_url, verify: false, basic_auth: auth) + @response = HTTParty.get(url, verify: false, basic_auth: auth) end end @@ -101,11 +101,11 @@ class BambooService < CiService if @response.code != 200 || @response['results']['results']['size'] == '0' # If actual build link can't be determined, send user to build summary page. - "#{bamboo_url}/browse/#{build_key}" + URI.join(bamboo_url, "/browse/#{build_key}").to_s else # If actual build link is available, go to build result page. result_key = @response['results']['results']['result']['planResultKey']['key'] - "#{bamboo_url}/browse/#{result_key}" + URI.join(bamboo_url, "/browse/#{result_key}").to_s end end @@ -134,7 +134,7 @@ class BambooService < CiService return unless supported_events.include?(data[:object_kind]) # Bamboo requires a GET and does not take any data. - self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}", - verify: false) + url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s + self.class.get(url, verify: false) end end diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index f9f04838766fd9e331296536d418799171b2a76c..6ab6d7417b715ef7afaca937b368e83d8122d2b3 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -23,7 +23,7 @@ class BuildsEmailService < Service prop_accessor :recipients boolean_accessor :add_pusher boolean_accessor :notify_only_broken_builds - validates :recipients, presence: true, if: :activated? + validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? } def initialize_properties if properties.nil? @@ -87,10 +87,14 @@ class BuildsEmailService < Service end def all_recipients(data) - all_recipients = recipients.split(',').compact.reject(&:blank?) + all_recipients = [] + + unless recipients.blank? + all_recipients += recipients.split(',').compact.reject(&:blank?) + end if add_pusher? && data[:user][:email] - all_recipients << "#{data[:user][:email]}" + all_recipients << data[:user][:email] end all_recipients diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 0e3fa4a40fe5d0a967b00df49f0c76439c445f18..064ef8e8674a80f7ca5a0e3d4dead37d4cd47302 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -183,7 +183,7 @@ class HipchatService < Service title = obj_attr[:title] merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" - merge_request_link = "<a href=\"#{merge_request_url}\">merge request ##{merge_request_id}</a>" + merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>" message = "#{user_name} #{state} #{merge_request_link} in " \ "#{project_link}: <b>#{title}</b>" @@ -224,7 +224,7 @@ class HipchatService < Service when "MergeRequest" subj_attr = HashWithIndifferentAccess.new(data[:merge_request]) subject_id = subj_attr[:iid] - subject_desc = "##{subject_id}" + subject_desc = "!#{subject_id}" subject_type = "merge request" title = format_title(subj_attr[:title]) when "Snippet" diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index d89cf6d17b272602bb765a0ffd6486b2f671a379..fd65027f08476a6cebd1170a8f4a9f30b2c9b3eb 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -60,7 +60,7 @@ class SlackService < Service end def supported_events - %w(push issue merge_request note tag_push build) + %w(push issue merge_request note tag_push build wiki_page) end def execute(data) @@ -90,6 +90,8 @@ class SlackService < Service NoteMessage.new(data) when "build" BuildMessage.new(data) if should_build_be_notified?(data) + when "wiki_page" + WikiPageMessage.new(data) end opt = {} @@ -133,3 +135,4 @@ require "slack_service/push_message" require "slack_service/merge_message" require "slack_service/note_message" require "slack_service/build_message" +require "slack_service/wiki_page_message" diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb index e792c258f7394a5e6e1c12dd222c75cd93d39620..11fc691022bbfe23fc85ef05cd75c242f9891f67 100644 --- a/app/models/project_services/slack_service/merge_message.rb +++ b/app/models/project_services/slack_service/merge_message.rb @@ -50,7 +50,7 @@ class SlackService end def merge_request_link - "[merge request ##{merge_request_id}](#{merge_request_url})" + "[merge request !#{merge_request_id}](#{merge_request_url})" end def merge_request_url diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb index b15d9a1467771833aa698e02496ca91398a39fd5..89ba51cb6627ee3d3b48fb00363e0e99c3a60a31 100644 --- a/app/models/project_services/slack_service/note_message.rb +++ b/app/models/project_services/slack_service/note_message.rb @@ -58,7 +58,7 @@ class SlackService def create_merge_note(merge_request) commented_on_message( - "[merge request ##{merge_request[:iid]}](#{@note_url})", + "[merge request !#{merge_request[:iid]}](#{@note_url})", format_title(merge_request[:title])) end diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb new file mode 100644 index 0000000000000000000000000000000000000000..f336d9e7691d56fdc2109823c91c41c60590f895 --- /dev/null +++ b/app/models/project_services/slack_service/wiki_page_message.rb @@ -0,0 +1,53 @@ +class SlackService + class WikiPageMessage < BaseMessage + attr_reader :user_name + attr_reader :title + attr_reader :project_name + attr_reader :project_url + attr_reader :wiki_page_url + attr_reader :action + attr_reader :description + + def initialize(params) + @user_name = params[:user][:name] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @title = obj_attr[:title] + @wiki_page_url = obj_attr[:url] + @description = obj_attr[:content] + + @action = + case obj_attr[:action] + when "create" + "created" + when "update" + "edited" + end + end + + def attachments + description_message + end + + private + + def message + "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*" + end + + def description_message + [{ text: format(@description), color: attachment_color }] + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def wiki_page_link + "[wiki page](#{wiki_page_url})" + end + end +end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index b8e9416131a9ab9a27b28c13bdddf97a929bad23..8dceee5e2c50c0f05f9b7e7765b9a9ffd33251fe 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -85,13 +85,15 @@ class TeamcityService < CiService end def build_info(sha) - url = URI.parse("#{teamcity_url}/httpAuth/app/rest/builds/"\ - "branch:unspecified:any,number:#{sha}") + url = URI.join( + teamcity_url, + "/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}" + ).to_s auth = { username: username, - password: password, + password: password } - @response = HTTParty.get("#{url}", verify: false, basic_auth: auth) + @response = HTTParty.get(url, verify: false, basic_auth: auth) end def build_page(sha, ref) @@ -100,12 +102,14 @@ class TeamcityService < CiService if @response.code != 200 # If actual build link can't be determined, # send user to build summary page. - "#{teamcity_url}/viewLog.html?buildTypeId=#{build_type}" + URI.join(teamcity_url, "/viewLog.html?buildTypeId=#{build_type}").to_s else # If actual build link is available, go to build result page. built_id = @response['build']['id'] - "#{teamcity_url}/viewLog.html?buildId=#{built_id}"\ - "&buildTypeId=#{build_type}" + URI.join( + teamcity_url, + "/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}" + ).to_s end end @@ -140,12 +144,13 @@ class TeamcityService < CiService branch = Gitlab::Git.ref_name(data[:ref]) - self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue", - body: "<build branchName=\"#{branch}\">"\ - "<buildType id=\"#{build_type}\"/>"\ - '</build>', - headers: { 'Content-type' => 'application/xml' }, - basic_auth: auth - ) + self.class.post( + URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s, + body: "<build branchName=\"#{branch}\">"\ + "<buildType id=\"#{build_type}\"/>"\ + '</build>', + headers: { 'Content-type' => 'application/xml' }, + basic_auth: auth + ) end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 462b48118ef504e53fa0a8f842b8e23482e5bf18..da751591103d4b17acb909072d2249df513f18a0 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -12,11 +12,13 @@ class Repository attr_accessor :path_with_namespace, :project def self.clean_old_archives - repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path + Gitlab::Metrics.measure(:clean_old_archives) do + repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path - return unless File.directory?(repository_downloads_path) + return unless File.directory?(repository_downloads_path) - Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) + Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) + end end def initialize(path_with_namespace, project) @@ -169,7 +171,12 @@ class Repository def rm_tag(tag_name) before_remove_tag - gitlab_shell.rm_tag(path_with_namespace, tag_name) + begin + rugged.tags.delete(tag_name) + true + rescue Rugged::ReferenceError + false + end end def branch_names @@ -221,7 +228,8 @@ class Repository def cache_keys %i(size branch_names tag_names commit_count - readme version contribution_guide changelog license) + readme version contribution_guide changelog + license_blob license_key) end def build_cache @@ -253,6 +261,8 @@ class Repository # This ensures this particular cache is flushed after the first commit to a # new repository. expire_emptiness_caches if empty? + expire_branch_count_cache + expire_tag_count_cache end def expire_branch_cache(branch_name = nil) @@ -452,27 +462,21 @@ class Repository end end - def license - cache.fetch(:license) do - licenses = tree(:head).blobs.find_all do |file| - file.name =~ /\A(copying|license|licence)/i - end - - preferences = [ - /\Alicen[sc]e\z/i, # LICENSE, LICENCE - /\Alicen[sc]e\./i, # LICENSE.md, LICENSE.txt - /\Acopying\z/i, # COPYING - /\Acopying\.(?!lesser)/i, # COPYING.txt - /Acopying.lesser/i # COPYING.LESSER - ] + def license_blob + return nil if !exists? || empty? - license = nil - preferences.each do |r| - license = licenses.find { |l| l.name =~ r } - break if license + cache.fetch(:license_blob) do + if licensee_project.license + blob_at_branch(root_ref, licensee_project.matched_file.filename) end + end + end - license + def license_key + return nil if !exists? || empty? + + cache.fetch(:license_key) do + licensee_project.license.try(:key) || 'no-license' end end @@ -540,15 +544,18 @@ class Repository commit(sha) end - def next_patch_branch - patch_branch_ids = self.branch_names.map do |n| - result = n.match(/\Apatch-([0-9]+)\z/) + def next_branch(name, opts={}) + branch_ids = self.branch_names.map do |n| + next 1 if n == name + result = n.match(/\A#{name}-([0-9]+)\z/) result[1].to_i if result end.compact - highest_patch_branch_id = patch_branch_ids.max || 0 + highest_branch_id = branch_ids.max || 0 + + return name if opts[:mild] && 0 == highest_branch_id - "patch-#{highest_patch_branch_id + 1}" + "#{name}-#{highest_branch_id + 1}" end # Remove archives older than 2 hours @@ -751,6 +758,28 @@ class Repository end end + def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) + source_sha = find_branch(base_branch).target + cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) + + return false unless cherry_pick_tree_id + + commit_with_hooks(user, base_branch) do |ref| + committer = user_to_committer(user) + source_sha = Rugged::Commit.create(rugged, + message: commit.message, + author: { + email: commit.author_email, + name: commit.author_name, + time: commit.authored_date + }, + committer: committer, + tree: cherry_pick_tree_id, + parents: [rugged.lookup(source_sha)], + update_ref: ref) + end + end + def check_revert_content(commit, base_branch) source_sha = find_branch(base_branch).target args = [commit.id, source_sha] @@ -765,6 +794,20 @@ class Repository tree_id end + def check_cherry_pick_content(commit, base_branch) + source_sha = find_branch(base_branch).target + args = [commit.id, source_sha] + args << 1 if commit.merge_commit? + + cherry_pick_index = rugged.cherrypick_commit(*args) + return false if cherry_pick_index.conflicts? + + tree_id = cherry_pick_index.write_tree(rugged) + return false unless diff_exists?(source_sha, tree_id) + + tree_id + end + def diff_exists?(sha1, sha2) rugged.diff(sha1, sha2).size > 0 end @@ -795,7 +838,7 @@ class Repository def search_files(query, ref) offset = 2 - args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) + args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{Regexp.escape(query)} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end @@ -916,4 +959,8 @@ class Repository def cache @cache ||= RepositoryCache.new(path_with_namespace) end + + def licensee_project + @licensee_project ||= Licensee.project(path) + end end diff --git a/app/models/service.rb b/app/models/service.rb index 721273250ea78d9516149fac720e182ffbdee936..2645b8321d722b0bbfd8e686b7340765a7c5b424 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -32,6 +32,7 @@ class Service < ActiveRecord::Base default_value_for :tag_push_events, true default_value_for :note_events, true default_value_for :build_events, true + default_value_for :wiki_page_events, true after_initialize :initialize_properties @@ -53,6 +54,7 @@ class Service < ActiveRecord::Base scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } scope :note_hooks, -> { where(note_events: true, active: true) } scope :build_hooks, -> { where(build_events: true, active: true) } + scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } default_value_for :category, 'common' @@ -94,7 +96,7 @@ class Service < ActiveRecord::Base end def supported_events - %w(push tag_push issue merge_request) + %w(push tag_push issue merge_request wiki_page) end def execute(data) diff --git a/app/models/user.rb b/app/models/user.rb index 2b0bee2099f2f5110bb1b08be29fca08e298e4da..ab48f8f1960d28219bcb6387ffffa8e76490a9bf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -63,7 +63,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class User < ActiveRecord::Base extend Gitlab::ConfigHelper @@ -143,6 +142,7 @@ class User < ActiveRecord::Base has_many :spam_logs, dependent: :destroy has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :todos, dependent: :destroy + has_many :notification_settings, dependent: :destroy # # Validations @@ -157,7 +157,7 @@ class User < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true + validates :notification_level, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :unique_email, if: ->(user) { user.email_changed? } @@ -190,6 +190,13 @@ class User < ActiveRecord::Base # Note: When adding an option, it MUST go on the end of the array. enum project_view: [:readme, :activity, :files] + # Notification level + # Note: When adding an option, it MUST go on the end of the array. + # + # TODO: Add '_prefix: :notification' to enum when update to Rails 5. https://github.com/rails/rails/pull/19813 + # Because user.notification_disabled? is much better than user.disabled? + enum notification_level: [:disabled, :participating, :watch, :global, :mention] + alias_attribute :private_token, :authentication_token delegate :path, to: :namespace, allow_nil: true, prefix: true @@ -349,10 +356,6 @@ class User < ActiveRecord::Base "#{self.class.reference_prefix}#{username}" end - def notification - @notification ||= Notification.new(self) - end - def generate_password if self.force_random_password self.password = self.password_confirmation = Devise.friendly_token.first(8) @@ -827,6 +830,10 @@ class User < ActiveRecord::Base end end + def notification_settings_for(source) + notification_settings.find_or_initialize_by(source: source) + end + private def projects_union diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 526760779a4d4cca53420e75d575c3e7fa9864cf..3d5fd9d3ee96291b4be9933dd07076f3c84cc473 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -29,6 +29,10 @@ class WikiPage # new Page values before writing to the Gollum repository. attr_accessor :attributes + def hook_attrs + attributes + end + def initialize(wiki, page = nil, persisted = false) @wiki = wiki @page = page diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 2cd51a7610f6310e5c0c098244e74e2ddba27e0a..18274ce24e229706d30a74717461ec9c0dc42797 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -1,7 +1,11 @@ module Ci class CreateBuildsService - def execute(commit, stage, ref, tag, user, trigger_request, status) - builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag, trigger_request) + def initialize(commit) + @commit = commit + end + + def execute(stage, user, status, trigger_request = nil) + builds_attrs = config_processor.builds_for_stage_and_ref(stage, @commit.ref, @commit.tag, trigger_request) # check when to create next build builds_attrs = builds_attrs.select do |build_attrs| @@ -17,7 +21,8 @@ module Ci builds_attrs.map do |build_attrs| # don't create the same build twice - unless commit.builds.find_by(ref: ref, tag: tag, trigger_request: trigger_request, name: build_attrs[:name]) + unless @commit.builds.find_by(ref: @commit.ref, tag: @commit.tag, + trigger_request: trigger_request, name: build_attrs[:name]) build_attrs.slice!(:name, :commands, :tag_list, @@ -26,17 +31,21 @@ module Ci :stage, :stage_idx) - build_attrs.merge!(ref: ref, - tag: tag, + build_attrs.merge!(ref: @commit.ref, + tag: @commit.tag, trigger_request: trigger_request, user: user, - project: commit.project) + project: @commit.project) - build = commit.builds.create!(build_attrs) - build.execute_hooks - build + @commit.builds.create!(build_attrs) end end end + + private + + def config_processor + @config_processor ||= @commit.config_processor + end end end diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index b3dfc707221b54e569042faabf5743eedbf7d29f..993acf11db968a158f0e116960167f19ba3673e4 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -7,14 +7,14 @@ module Ci # check if ref is tag tag = project.repository.find_tag(ref).present? - ci_commit = project.ensure_ci_commit(commit.sha) + ci_commit = project.ci_commits.create(sha: commit.sha, ref: ref, tag: tag) trigger_request = trigger.trigger_requests.create!( variables: variables, commit: ci_commit, ) - if ci_commit.create_builds(ref, tag, nil, trigger_request) + if ci_commit.create_builds(nil, trigger_request) trigger_request end end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 50c95ced8a7e2ce0b4f79466d2e2e1ba42100725..3018f27ec059e47921efcfe0105f1da9ecc660a3 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -3,8 +3,9 @@ module Ci def execute(project, opts) sha = opts[:sha] || ref_sha(project, opts[:ref]) - commit = project.ci_commits.find_by(sha: sha) - image_name = image_for_commit(commit) + ci_commits = project.ci_commits.where(sha: sha) + ci_commits = ci_commits.where(ref: opts[:ref]) if opts[:ref] + image_name = image_for_status(ci_commits.status) image_path = Rails.root.join('public/ci', image_name) OpenStruct.new(path: image_path, name: image_name) @@ -16,9 +17,9 @@ module Ci project.commit(ref).try(:sha) if ref end - def image_for_commit(commit) - return 'build-unknown.svg' unless commit - 'build-' + commit.status + ".svg" + def image_for_status(status) + status ||= 'unknown' + 'build-' + status + ".svg" end end end diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..6b69cb53b2c6bad66dc972023c3e12f476b4dd18 --- /dev/null +++ b/app/services/commits/change_service.rb @@ -0,0 +1,47 @@ +module Commits + class ChangeService < ::BaseService + class ValidationError < StandardError; end + class ChangeError < StandardError; end + + def execute + @source_project = params[:source_project] || @project + @target_branch = params[:target_branch] + @commit = params[:commit] + @create_merge_request = params[:create_merge_request].present? + + check_push_permissions unless @create_merge_request + commit + rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, + ValidationError, ChangeError => ex + error(ex.message) + end + + def commit + raise NotImplementedError + end + + private + + def check_push_permissions + allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch) + + unless allowed + raise ValidationError.new('You are not allowed to push into this branch') + end + + true + end + + def create_target_branch(new_branch) + # Temporary branch exists and contains the change commit + return success if repository.find_branch(new_branch) + + result = CreateBranchService.new(@project, current_user) + .execute(new_branch, @target_branch, source_project: @source_project) + + if result[:status] == :error + raise ChangeError, "There was an error creating the source branch: #{result[:message]}" + end + end + end +end diff --git a/app/services/commits/cherry_pick_service.rb b/app/services/commits/cherry_pick_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..f9a4efa7182ead1398cfbd02fed217e9acc510c6 --- /dev/null +++ b/app/services/commits/cherry_pick_service.rb @@ -0,0 +1,19 @@ +module Commits + class CherryPickService < ChangeService + def commit + cherry_pick_into = @create_merge_request ? @commit.cherry_pick_branch_name : @target_branch + cherry_pick_tree_id = repository.check_cherry_pick_content(@commit, @target_branch) + + if cherry_pick_tree_id + create_target_branch(cherry_pick_into) if @create_merge_request + + repository.cherry_pick(current_user, @commit, cherry_pick_into, cherry_pick_tree_id) + success + else + error_msg = "Sorry, we cannot cherry-pick this #{@commit.change_type_title} automatically. + It may have already been cherry-picked, or a more recent commit may have updated some of its content." + raise ChangeError, error_msg + end + end + end +end diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb index a3c950ede1f51b33d60a417633da867646e0b138..c7de9f6f35e47cf0c704a8136abcfb4996666e19 100644 --- a/app/services/commits/revert_service.rb +++ b/app/services/commits/revert_service.rb @@ -1,21 +1,5 @@ module Commits - class RevertService < ::BaseService - class ValidationError < StandardError; end - class ReversionError < StandardError; end - - def execute - @source_project = params[:source_project] || @project - @target_branch = params[:target_branch] - @commit = params[:commit] - @create_merge_request = params[:create_merge_request].present? - - check_push_permissions unless @create_merge_request - commit - rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, - ValidationError, ReversionError => ex - error(ex.message) - end - + class RevertService < ChangeService def commit revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch revert_tree_id = repository.check_revert_content(@commit, @target_branch) @@ -26,34 +10,10 @@ module Commits repository.revert(current_user, @commit, revert_into, revert_tree_id) success else - error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically. + error_msg = "Sorry, we cannot revert this #{@commit.change_type_title} automatically. It may have already been reverted, or a more recent commit may have updated some of its content." - raise ReversionError, error_msg + raise ChangeError, error_msg end end - - private - - def create_target_branch(new_branch) - # Temporary branch exists and contains the revert commit - return success if repository.find_branch(new_branch) - - result = CreateBranchService.new(@project, current_user) - .execute(new_branch, @target_branch, source_project: @source_project) - - if result[:status] == :error - raise ReversionError, "There was an error creating the source branch: #{result[:message]}" - end - end - - def check_push_permissions - allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch) - - unless allowed - raise ValidationError.new('You are not allowed to push into this branch') - end - - true - end end end diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 69d5c42a87758dd1a791f374b6592b2a87017ccd..0d2aa1ff03dc47213de24fa54df28ecc2c1dc018 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -2,6 +2,7 @@ class CreateCommitBuildsService def execute(project, user, params) return false unless project.builds_enabled? + before_sha = params[:checkout_sha] || params[:before] sha = params[:checkout_sha] || params[:after] origin_ref = params[:ref] @@ -10,15 +11,16 @@ class CreateCommitBuildsService end ref = Gitlab::Git.ref_name(origin_ref) + tag = Gitlab::Git.tag_ref?(origin_ref) # Skip branch removal if sha == Gitlab::Git::BLANK_SHA return false end - commit = project.ci_commit(sha) + commit = project.ci_commit(sha, ref) unless commit - commit = project.ci_commits.new(sha: sha) + commit = project.ci_commits.new(sha: sha, ref: ref, before_sha: before_sha, tag: tag) # Skip creating ci_commit when no gitlab-ci.yml is found unless commit.ci_yaml_file @@ -32,10 +34,10 @@ class CreateCommitBuildsService # Skip creating builds for commits that have [ci skip] unless commit.skip_ci? # Create builds for commit - tag = Gitlab::Git.tag_ref?(origin_ref) - commit.create_builds(ref, tag, user) + commit.create_builds(user) end + commit.touch commit end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index dc74c02760b58ea6d6fe936a2bfe93df1660190b..1e1be8cd04b7230cee3c90f311113607251045c0 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -73,6 +73,7 @@ class GitPushService < BaseService @project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user) EventCreateService.new.push(@project, current_user, build_push_data) + SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) CreateCommitBuildsService.new.execute(@project, current_user, build_push_data) @@ -138,6 +139,11 @@ class GitPushService < BaseService build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits) end + def build_push_data_system_hook + @push_data_system ||= Gitlab::PushDataBuilder. + build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], []) + end + def push_to_existing_branch? # Return if this is not a push to a branch (e.g. new commits) Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev]) diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index c88c76728051e9861f98fbb850b45096821d4992..64271d8bc5c6b7a3ab457b37b58f59d371760cca 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -1,16 +1,16 @@ -class GitTagPushService - attr_accessor :project, :user, :push_data +class GitTagPushService < BaseService + attr_accessor :push_data - def execute(project, user, oldrev, newrev, ref) + def execute project.repository.before_push_tag - @project, @user = project, user - @push_data = build_push_data(oldrev, newrev, ref) + @push_data = build_push_data - EventCreateService.new.push(project, user, @push_data) + EventCreateService.new.push(project, current_user, @push_data) + SystemHooksService.new.execute_hooks(build_system_push_data.dup, :tag_push_hooks) project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) - CreateCommitBuildsService.new.execute(project, @user, @push_data) + CreateCommitBuildsService.new.execute(project, current_user, @push_data) ProjectCacheWorker.perform_async(project.id) true @@ -18,14 +18,14 @@ class GitTagPushService private - def build_push_data(oldrev, newrev, ref) + def build_push_data commits = [] message = nil - if !Gitlab::Git.blank_ref?(newrev) - tag_name = Gitlab::Git.ref_name(ref) + if !Gitlab::Git.blank_ref?(params[:newrev]) + tag_name = Gitlab::Git.ref_name(params[:ref]) tag = project.repository.find_tag(tag_name) - if tag && tag.target == newrev + if tag && tag.target == params[:newrev] commit = project.commit(tag.target) commits = [commit].compact message = tag.message @@ -33,6 +33,11 @@ class GitTagPushService end Gitlab::PushDataBuilder. - build(project, user, oldrev, newrev, ref, commits, message) + build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message) + end + + def build_system_push_data + Gitlab::PushDataBuilder. + build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '') end end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 770f32de9448277bdbc9c7d6cc74ecf5c70b7cad..772f5c5fffad6b69f302f2578b405ae14d88e225 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -3,7 +3,7 @@ module Issues def hook_data(issue, action) issue_data = issue.to_hook_data(current_user) - issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id) + issue_url = Gitlab::UrlBuilder.build(issue) issue_data[:object_attributes].merge!(url: issue_url, action: action) issue_data end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index ac5b58db8623755e252981c02b9e5bb5d6b43bae..e6837a186963e1df39d02bec64ed44cade17efcd 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -20,8 +20,7 @@ module MergeRequests def hook_data(merge_request, action) hook_data = merge_request.to_hook_data(current_user) - merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) - hook_data[:object_attributes][:url] = merge_request_url + hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request) hook_data[:object_attributes][:action] = action hook_data end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 6e9152e444ec6eb1eeed939ed678ef8534cedbce..fa34753c4fd2d3e4f5b4c2fe0a98d30438a1df9f 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -51,7 +51,7 @@ module MergeRequests # be interpreted as the use wants to close that issue on this project # Pattern example: 112-fix-mep-mep # Will lead to appending `Closes #112` to the description - if match = merge_request.source_branch.match(/-(\d+)\z/) + if match = merge_request.source_branch.match(/\A(\d+)-/) iid = match[1] closes_issue = "Closes ##{iid}" diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index eff0d96f93dd07e0d1ec4336f952d957768b7274..42ec1ac9e1a15ed2b6a76c0cf001fe302e5ec492 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -253,8 +253,8 @@ class NotificationService def project_watchers(project) project_members = project_member_notification(project) - users_with_project_level_global = project_member_notification(project, Notification::N_GLOBAL) - users_with_group_level_global = group_member_notification(project, Notification::N_GLOBAL) + users_with_project_level_global = project_member_notification(project, :global) + users_with_group_level_global = group_member_notification(project, :global) users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq) users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users) @@ -264,18 +264,16 @@ class NotificationService end def project_member_notification(project, notification_level=nil) - project_members = project.project_members - if notification_level - project_members.where(notification_level: notification_level).pluck(:user_id) + project.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id) else - project_members.pluck(:user_id) + project.notification_settings.pluck(:user_id) end end def group_member_notification(project, notification_level) if project.group - project.group.group_members.where(notification_level: notification_level).pluck(:user_id) + project.group.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id) else [] end @@ -284,13 +282,13 @@ class NotificationService def users_with_global_level_watch(ids) User.where( id: ids, - notification_level: Notification::N_WATCH + notification_level: NotificationSetting.levels[:watch] ).pluck(:id) end # Build a list of users based on project notifcation settings def select_project_member_setting(project, global_setting, users_global_level_watch) - users = project_member_notification(project, Notification::N_WATCH) + users = project_member_notification(project, :watch) # If project setting is global, add to watch list if global setting is watch global_setting.each do |user_id| @@ -304,7 +302,7 @@ class NotificationService # Build a list of users based on group notification settings def select_group_member_setting(project, project_members, global_setting, users_global_level_watch) - uids = group_member_notification(project, Notification::N_WATCH) + uids = group_member_notification(project, :watch) # Group setting is watch, add to users list if user is not project member users = [] @@ -331,40 +329,46 @@ class NotificationService # Remove users with disabled notifications from array # Also remove duplications and nil recipients def reject_muted_users(users, project = nil) - reject_users(users, :disabled?, project) + reject_users(users, :disabled, project) end # Remove users with notification level 'Mentioned' def reject_mention_users(users, project = nil) - reject_users(users, :mention?, project) + reject_users(users, :mention, project) end - # Reject users which method_name from notification object returns true. + # Reject users which has certain notification level # # Example: - # reject_users(users, :watch?, project) + # reject_users(users, :watch, project) # - def reject_users(users, method_name, project = nil) + def reject_users(users, level, project = nil) + level = level.to_s + + unless NotificationSetting.levels.keys.include?(level) + raise 'Invalid notification level' + end + users = users.to_a.compact.uniq users = users.reject(&:blocked?) users.reject do |user| - next user.notification.send(method_name) unless project + next user.notification_level == level unless project - member = project.project_members.find_by(user_id: user.id) + setting = user.notification_settings_for(project) - if !member && project.group - member = project.group.group_members.find_by(user_id: user.id) + if !setting && project.group + setting = user.notification_settings_for(project.group) end - # reject users who globally set mention notification and has no membership - next user.notification.send(method_name) unless member + # reject users who globally set mention notification and has no setting per project/group + next user.notification_level == level unless setting # reject users who set mention notification in project - next true if member.notification.send(method_name) + next true if setting.level == level - # reject users who have N_MENTION in project and disabled in global settings - member.notification.global? && user.notification.send(method_name) + # reject users who have mention level in project and disabled in global settings + setting.global? && user.notification_level == level end end diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index a0973c5d2603ef1af0f25df03d6a7c2c803fb037..3b7c36f0908b61526836c6ef4a93ebad8a138326 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -26,7 +26,9 @@ module Projects GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace) ensure - @project.update_column(:pushes_since_gc, 0) + Gitlab::Metrics.measure(:reset_pushes_since_gc) do + @project.update_column(:pushes_since_gc, 0) + end end def needed? @@ -34,14 +36,18 @@ module Projects end def increment! - @project.increment!(:pushes_since_gc) + Gitlab::Metrics.measure(:increment_pushes_since_gc) do + @project.increment!(:pushes_since_gc) + end end private def try_obtain_lease - lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) - lease.try_obtain + Gitlab::Metrics.measure(:obtain_housekeeping_lease) do + lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) + lease.try_obtain + end end end end diff --git a/app/services/projects/import_export/import_service.rb b/app/services/projects/import_export/import_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..20eda2f228de1435e018b62816b5aea10c221891 --- /dev/null +++ b/app/services/projects/import_export/import_service.rb @@ -0,0 +1,30 @@ +module Projects + module ImportExport + class ExportService < BaseService + def execute(options = {}) + archive_file = options[:archive_file] + Gitlab::ImportExport::Importer.import(archive_file: archive_file, storage_path: storage_path) + restore_project_tree + restore_repo(project_tree.project) + end + + private + + def restore_project_tree + project_tree.restore + end + + def project_tree + @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, user: @current_user) + end + + def restore_repo(project) + Gitlab::ImportExport::RepoRestorer.new(path: storage_path, project: project).restore + end + + def storage_path + @storage_path ||= Gitlab::ImportExport.export_path(relative_path: project.path_with_namespace) + end + end + end +end diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index 0004a399f474f0bde777b921873d09f3bd76f22c..02c4eee3d02b2eed9a22ea12a4f81254ce475f8f 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -1,28 +1,37 @@ module Projects class ParticipantsService < BaseService - def execute(note_type, note_id) - participating = - if note_type && note_id - participants_in(note_type, note_id) - else - [] - end + def execute(noteable_type, noteable_id) + @noteable_type = noteable_type + @noteable_id = noteable_id project_members = sorted(project.team.members) - participants = all_members + groups + project_members + participating + participants = target_owner + participants_in_target + all_members + groups + project_members participants.uniq end - def participants_in(type, id) - target = - case type + def target + @target ||= + case @noteable_type when "Issue" - project.issues.find_by_iid(id) + project.issues.find_by_iid(@noteable_id) when "MergeRequest" - project.merge_requests.find_by_iid(id) + project.merge_requests.find_by_iid(@noteable_id) when "Commit" - project.commit(id) + project.commit(@noteable_id) + else + nil end - + end + + def target_owner + return [] unless target && target.author.present? + + [{ + name: target.author.name, + username: target.author.username + }] + end + + def participants_in_target return [] unless target users = target.participants(current_user) @@ -30,13 +39,13 @@ module Projects end def sorted(users) - users.uniq.to_a.compact.sort_by(&:username).map do |user| + users.uniq.to_a.compact.sort_by(&:username).map do |user| { username: user.username, name: user.name } end end def groups - current_user.authorized_groups.sort_by(&:path).map do |group| + current_user.authorized_groups.sort_by(&:path).map do |group| count = group.users.count { username: group.path, name: group.name, count: count } end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 2e734654466b8ac43054b8f814d44cf82c0ce6be..79a27f4af7e5a939392261c753b2204505367f94 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -34,8 +34,9 @@ module Projects raise TransferError.new("Project with same path in target namespace already exists") end - # Apply new namespace id + # Apply new namespace id and visibility level project.namespace = new_namespace + project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group? project.save! # Notifications @@ -56,7 +57,7 @@ module Projects Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) project.old_path_with_namespace = old_path - + SystemHooksService.new.execute_hooks_for(project, :transfer) true end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index f0615ec7420b2da0705c777d9c5140b73fc4580b..e43b5b51e5b8b9de837edf2c7c055cf5fa0278fa 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -3,17 +3,13 @@ class SystemHooksService execute_hooks(build_event_data(model, event)) end - private - - def execute_hooks(data) - SystemHook.all.each do |sh| - async_execute_hook(sh, data, 'system_hooks') + def execute_hooks(data, hooks_scope = :all) + SystemHook.send(hooks_scope).each do |hook| + hook.async_execute(data, 'system_hooks') end end - def async_execute_hook(hook, data, hook_name) - Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data, hook_name) - end + private def build_event_data(model, event) data = { diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 658b086496f55102bcf50690497c148f88150c2d..82a0e2fd1f563ef98ff8108e7ac7f0fa7330c564 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -222,7 +222,7 @@ class SystemNoteService # Called when a branch is created from the 'new branch' button on a issue # Example note text: # - # "Started branch `issue-branch-button-201`" + # "Started branch `201-issue-branch-button`" def self.new_issue_branch(issue, project, author, branch) h = Gitlab::Routing.url_helpers link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..9162f1286026c7f071f0a8d6054739556db22f4b --- /dev/null +++ b/app/services/wiki_pages/base_service.rb @@ -0,0 +1,27 @@ +module WikiPages + class BaseService < ::BaseService + + def hook_data(page, action) + hook_data = { + object_kind: page.class.name.underscore, + user: current_user.hook_attrs, + project: @project.hook_attrs, + object_attributes: page.hook_attrs, + # DEPRECATED + repository: @project.hook_attrs.slice(:name, :url, :description, :homepage) + } + + page_url = Gitlab::UrlBuilder.build(page) + hook_data[:object_attributes].merge!(url: page_url, action: action) + hook_data + end + + private + + def execute_hooks(page, action = 'create') + page_data = hook_data(page, action) + @project.execute_hooks(page_data, :wiki_page_hooks) + @project.execute_services(page_data, :wiki_page_hooks) + end + end +end diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..988c663b9d0364461d9cef6fcd9f4ca510ab3a2f --- /dev/null +++ b/app/services/wiki_pages/create_service.rb @@ -0,0 +1,13 @@ +module WikiPages + class CreateService < WikiPages::BaseService + def execute + page = WikiPage.new(@project.wiki) + + if page.create(@params) + execute_hooks(page, 'create') + end + + page + end + end +end diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..8f6a50da8381443ac95fd076f547945fc3b9ccf2 --- /dev/null +++ b/app/services/wiki_pages/update_service.rb @@ -0,0 +1,11 @@ +module WikiPages + class UpdateService < WikiPages::BaseService + def execute(page) + if page.update(@params[:content], @params[:format], @params[:message]) + execute_hooks(page, 'update') + end + + page + end + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index a8cca1a81cb3537728aa505f8320cc2a5878e738..e0d8d16a9545c77cb8ea52aeb2fbb6c52df94c1c 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -26,7 +26,9 @@ .btn-group{ data: data_attrs } - restricted_level_checkboxes('restricted-visibility-help').each do |level| = level - %span.help-block#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets + %span.help-block#restricted-visibility-help + Selected levels cannot be used by non-admin users for projects or snippets. + If the public level is restricted, user profiles are only visible to logged in users. .form-group = f.label :import_sources, class: 'control-label col-sm-2' .col-sm-10 @@ -153,7 +155,11 @@ = f.label :shared_runners_enabled do = f.check_box :shared_runners_enabled Enable shared runners for new projects - + .form-group + = f.label :shared_runners_text, class: 'control-label col-sm-2' + .col-sm-10 + = f.text_area :shared_runners_text, class: 'form-control', rows: 4 + .help-block Markdown enabled .form-group = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2' .col-sm-10 @@ -212,6 +218,13 @@ .help-block The sampling interval in seconds. Sampled data includes memory usage, retained Ruby objects, file descriptors and so on. + .form-group + = f.label :metrics_packet_size, 'Metrics per packet', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :metrics_packet_size, class: 'form-control' + .help-block + The amount of points to store in a single UDP packet. More points + results in fewer but larger UDP packets being sent. %fieldset %legend Spam and Anti-bot Protection @@ -271,5 +284,24 @@ .col-sm-10 = f.text_field :sentry_dsn, class: 'form-control' + %fieldset + %legend Repository Checks + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :repository_checks_enabled do + = f.check_box :repository_checks_enabled + Enable Repository Checks + .help-block + GitLab will periodically run + %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck' + in all project and wiki repositories to look for silent disk corruption issues. + .form-group + .col-sm-offset-2.col-sm-10 + = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" + .help-block + If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. + + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index ad952052f253efc3ef1ab423502621d7cb677e37..67d23c80233e1c31c1cb4b95b6750aa7f4db4fc2 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -16,6 +16,27 @@ = f.label :url, "URL:", class: 'control-label' .col-sm-10 = f.text_field :url, class: "form-control" + .form-group + = f.label :url, "Trigger", class: 'control-label' + .col-sm-10.prepend-top-10 + %div + System hook will be triggered on set of events like creating project + or adding ssh key. But you can also enable extra triggers like Push events. + + %div.prepend-top-default + = f.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = f.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + %div + = f.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = f.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository .form-group = f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox' .col-sm-10 @@ -31,13 +52,16 @@ .panel.panel-default .panel-heading System hooks (#{@hooks.count}) - %ul.well-list + %ul.content-list - @hooks.each do |hook| %li - .list-item-name - %strong= hook.url - %p SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} - - .pull-right + .controls = link_to 'Test Hook', admin_hook_test_path(hook), class: "btn btn-sm" = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" + .monospace= hook.url + %div + - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| + - if hook.send(trigger) + %span.label.label-gray= trigger.titleize + %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} + diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index af9fdeb073454e205a91d762b4a55d84eb0e6ada..4b475a4d8fafd2bf96c756a970799a637d31746f 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,6 +1,7 @@ - page_title "Logs" - loggers = [Gitlab::GitLogger, Gitlab::AppLogger, - Gitlab::ProductionLogger, Gitlab::SidekiqLogger] + Gitlab::ProductionLogger, Gitlab::SidekiqLogger, + Gitlab::RepositoryCheckLogger] %ul.nav-links.log-tabs - loggers.each do |klass| %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index d39c0f440319b077143902042006a22158800f2f..aa07afa0d62ccb3164117277668510f8b41626db 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -3,7 +3,7 @@ .row.prepend-top-default %aside.col-md-3 - .admin-filter + .panel.admin-filter = form_tag admin_namespaces_projects_path, method: :get, class: '' do .form-group = label_tag :name, 'Name:' @@ -38,7 +38,13 @@ %span.descr = visibility_level_icon(level) = label - %hr + %fieldset + %strong Problems + .checkbox + = label_tag :last_repository_check_failed do + = check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed] + %span Last repository check failed + = hidden_field_tag :sort, params[:sort] = button_tag "Search", class: "btn submit btn-primary" = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index c638c32a654ca59db7ade68ee4171c8f7106715c..73986d21bcf73c90a1dc435c3d28489fc9167cb0 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -5,6 +5,16 @@ %i.fa.fa-pencil-square-o Edit %hr +- if @project.last_repository_check_failed? + .row + .col-md-12 + .panel + .panel-heading.alert.alert-danger + Last repository check + = "(#{time_ago_in_words(@project.last_repository_check_at)} ago)" + failed. See + = link_to 'repocheck.log', admin_logs_path + for error messages. .row .col-md-6 .panel.panel-default @@ -95,6 +105,32 @@ .col-sm-offset-2.col-sm-10 = f.submit 'Transfer', class: 'btn btn-primary' + .panel.panel-default.repository-check + .panel-heading + Repository check + .panel-body + = form_for @project, url: repository_check_admin_namespace_project_path(@project.namespace, @project), method: :post do |f| + .form-group + - if @project.last_repository_check_at.nil? + This repository has never been checked. + - else + This repository was last checked + = @project.last_repository_check_at.to_s(:medium) + '.' + The check + - if @project.last_repository_check_failed? + = succeed '.' do + %strong.cred failed + See + = link_to 'repocheck.log', admin_logs_path + for error messages. + - else + passed. + + = link_to icon('question-circle'), help_page_path('administration', 'repository_checks') + + .form-group + = f.submit 'Trigger repository check', class: 'btn btn-primary' + .col-md-6 - if @group .panel.panel-default diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index b05fdbd5552c50d1ad8705b12c740eb56c332c79..fe0b9d3a4910aa2e5554cd43fe3b3a0037c6c2d5 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -7,17 +7,17 @@ .form-group = f.label :name, class: 'control-label' .col-sm-10 - = f.text_field :name, required: true, autocomplete: "off", class: 'form-control' + = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required .form-group = f.label :username, class: 'control-label' .col-sm-10 - = f.text_field :username, required: true, autocomplete: "off", class: 'form-control' + = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control' %span.help-inline * required .form-group = f.label :email, class: 'control-label' .col-sm-10 - = f.text_field :email, required: true, autocomplete: "off", class: 'form-control' + = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required - if @user.new_record? diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index f9ec3a89158e13a8ee3836db4dff2df5e7181ed7..49ab8aad1d5e56a248f0a0eb0d067ae9ddc9143e 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -45,6 +45,7 @@ .prepend-top-default - if @todos.any? + .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } - @todos.group_by(&:project).each do |group| .panel.panel-default.panel-small.js-todos-list - project = group[0] diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index cb93ff2465e055a77f1ee1405d02b129ed199f0b..e5607dacd0d83b811b862e71f52a252e0c07f1d5 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -6,18 +6,17 @@ .login-heading %h3 Create an account .login-body - - user = params[:user].present? ? params[:user] : {} = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| .devise-errors = devise_error_messages! %div - = f.text_field :name, class: "form-control top", value: user[:name], placeholder: "Name", required: true + = f.text_field :name, class: "form-control top", placeholder: "Name", required: true %div - = f.text_field :username, class: "form-control middle", value: user[:username], placeholder: "Username", required: true + = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true %div - = f.email_field :email, class: "form-control middle", value: user[:email], placeholder: "Email", required: true + = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true .form-group.append-bottom-20#password-strength - = f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true + = f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true %div - if current_application_settings.recaptcha_enabled = recaptcha_tags diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index 55f4a6f287d208f22254c71e20eb0c41fd404812..79df17ba6124759633720b30f6657e4677d55ce4 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -1,5 +1,4 @@ - page_title "Applications" -- header_title page_title, applications_profile_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar @@ -68,7 +67,7 @@ %td= app.name %td= token.created_at %td= token.scopes - %td= render 'delete_form', application: app + %td= render 'doorkeeper/authorized_applications/delete_form', application: app - @authorized_anonymous_tokens.each do |token| %tr %td diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 4290e0bf72e48835e448ca35c7b02be3b77ed418..7d9d27ae1fccc98f044e33e77ebf95d5c26ce6e8 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -8,7 +8,7 @@ This will create milestone in every selected project %hr -= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f| += form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f| .row - if @milestone.errors.any? #error_explanation @@ -27,7 +27,7 @@ = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' .clearfix .error-alert .form-group diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index d084559abc3385844d9e6393a61e002e1e05eb18..f12df5c8ffe282389957d609d109d7e860e5f15d 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -345,11 +345,11 @@ %ul %li %a.dropdown-menu-user-link.is-active{href: "#"} - = link_to_member_avatar(current_user, size: 30) + = link_to_member_avatar(@user, size: 30) %strong.dropdown-menu-user-full-name - = current_user.name + = @user.name .dropdown-menu-user-username - = current_user.to_reference + = @user.to_reference .example %div @@ -372,11 +372,11 @@ %ul %li %a.dropdown-menu-user-link.is-active{href: "#"} - = link_to_member_avatar(current_user, size: 30) + = link_to_member_avatar(@user, size: 30) %strong.dropdown-menu-user-full-name - = current_user.name + = @user.name .dropdown-menu-user-username - = current_user.to_reference + = @user.to_reference .dropdown-page-two .dropdown-title %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}} diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml index d8af0295b2dd0b664c5897af2bdadb478f192cbe..dfebf7768d9a7d4509daf401cd2aae5696e45b81 100644 --- a/app/views/import/base/create.js.haml +++ b/app/views/import/base/create.js.haml @@ -20,10 +20,10 @@ job.attr("id", "project_#{@project.id}") target_field = job.find(".import-target") target_field.empty() - target_field.append('<strong>#{link_to @project.path_with_namespace, namespace_project_path(@project.namespace, @project)}</strong>') + target_field.append('#{link_to @project.path_with_namespace, namespace_project_path(@project.namespace, @project)}') $("table.import-jobs tbody").prepend(job) job.addClass("active").find(".import-actions").html("<i class='fa fa-spinner fa-spin'></i> started") - else :plain job = $("tr#repo_#{@repo_id}") - job.find(".import-actions").html("<i class='fa fa-exclamation-circle'> Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}</i>") + job.find(".import-actions").html("<i class='fa fa-exclamation-circle'></i> Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}") diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index aec2e836c9fdbac543af0252b8628d77f6eadd6c..6e993e58f0d7ca8057544e60ad9f563e221f2f56 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -10,13 +10,19 @@ %hr %p - if @incompatible_repos.any? - = button_tag 'Import all compatible projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all compatible projects + = icon("spinner spin", class: "loading-icon") - else - = button_tag 'Import all projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") - -.table-holder +.table-responsive %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col %thead %tr %th From Bitbucket @@ -28,7 +34,7 @@ %td = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span @@ -47,7 +53,9 @@ %td.import-target = "#{repo["owner"]}/#{repo["slug"]}" %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") - @incompatible_repos.each do |repo| %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} %td diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index 6ee16c8be4b95386deb10702e3650de7a341312b..d3d3c595c172c08a890ed531bf39c5218f80561c 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -13,10 +13,15 @@ how FogBugz email addresses and usernames are imported into GitLab. %hr %p - = button_tag 'Import all projects', class: 'btn btn-success js-import-all' + = button_tag class: 'btn btn-import btn-success js-import-all' do + Import all projects + = icon("spinner spin", class: "loading-icon") -.table-holder +.table-responsive %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col %thead %tr %th From FogBugz @@ -28,7 +33,7 @@ %td = project.import_source %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span @@ -47,7 +52,9 @@ %td.import-target = "#{current_user.username}/#{repo.name}" %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") :javascript new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}"); diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 1416ee5bd5a0d7e00bbd6e9ba6ffd111f77c99b9..5b7f11440c102e7b3e0061f7812454b33a59c4b9 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -8,10 +8,15 @@ Select projects you want to import. %hr %p - = button_tag 'Import all projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") -.table-holder +.table-responsive %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col %thead %tr %th From GitHub @@ -21,9 +26,9 @@ - @already_added_projects.each do |project| %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} %td - = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank" + = github_project_link(project.import_source) %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span @@ -38,11 +43,13 @@ - @repos.each do |repo| %tr{id: "repo_#{repo.id}"} %td - = link_to repo.full_name, "https://github.com/#{repo.full_name}", target: "_blank" + = github_project_link(repo.full_name) %td.import-target = repo.full_name %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") :javascript new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}"); diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index 911a55eb85dfc20eeb98ff2477796f610af5e051..e3a356b53792c26fd88c805159462044ab5d682a 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -8,10 +8,15 @@ Select projects you want to import. %hr %p - = button_tag 'Import all projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") -.table-holder +.table-responsive %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col %thead %tr %th From GitLab.com @@ -23,7 +28,7 @@ %td = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span @@ -42,7 +47,9 @@ %td.import-target = repo["path_with_namespace"] %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + = button_tag class: "btn js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") :javascript new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}"); diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index 6b0fa1edf8ce2d5a6e20b859b6d594de0208005c..267eee4f262ae8ebe702cc892b61facd577468a8 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -8,10 +8,15 @@ Select projects you want to import. %hr %p - = button_tag 'Import all projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") -.table-holder +.table-responsive %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col %thead %tr %th From Gitorious.org @@ -23,7 +28,7 @@ %td = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span @@ -42,7 +47,9 @@ %td.import-target = repo.full_name %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") :javascript new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}"); diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index 175ef6921cd19a656c652467ebb9c889127c1421..5ada6b174ebc3a36b8db6adefa1eb6016ea25c0b 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -14,12 +14,19 @@ %hr %p - if @incompatible_repos.any? - = button_tag 'Import all compatible projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all compatible projects + = icon("spinner spin", class: "loading-icon") - else - = button_tag 'Import all projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") -.table-holder +.table-responsive %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col %thead %tr %th From Google Code @@ -31,7 +38,7 @@ %td = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span @@ -50,7 +57,9 @@ %td.import-target = "#{current_user.username}/#{repo.name}" %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") - @incompatible_repos.each do |repo| %tr{id: "repo_#{repo.id}"} %td diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index c799e9c588d60fdd383e0f835641d1319a43d45c..ca9c2a0bf2eb54d86e1f9928cce9296fb06c7b51 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -25,6 +25,10 @@ .content-wrapper = render "layouts/flash" = yield :flash_message + - if defined?(nav) && nav + .layout-nav + %div{ class: container_class } + = render "layouts/nav/#{nav}" %div{ class: (container_class unless @no_container) } .content .clearfix diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index babfb03223663a38980f817ab1248dd84ba3d01c..e4d1c773d0390ea00cb84b65ff470a7e070057be 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -6,6 +6,6 @@ = yield :scripts_body_top = render "layouts/header/default", title: header_title - = render 'layouts/page', sidebar: sidebar + = render 'layouts/page', sidebar: sidebar, nav: nav = yield :scripts_body diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 0f3b8119379cf124a1f1ff617c33118a545a0936..3beb8ff7c0daa7a49386a1d14220b1f7d267de08 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -8,7 +8,7 @@ .navbar-collapse.collapse %ul.nav.navbar-nav %li.hidden-sm.hidden-xs - = render 'layouts/search' + = render 'layouts/search' unless current_controller?(:search) %li.visible-sm.visible-xs = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('search') @@ -45,6 +45,8 @@ %h1.title= title + = yield :header_content + = render 'shared/outdated_browser' - if @project && !@project.empty_repo? diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 5cef652da14b7a44f1801f2f15127089d8fcf623..ca49c313ff7fddf1dfdce136873b8b624a1b07d1 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -48,8 +48,7 @@ %span Help - %li.separate-item - = nav_link(controller: :profile) do + = nav_link(html_options: {class: profile_tab_class}) do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = icon('user fw') %span diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 3b9d31a6fc5d4f13d67850381ffb71c6ba656f44..d730840d63a9e9b3bfb2383d11b465d87661928e 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,17 +1,9 @@ -%ul.nav.nav-sidebar - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - +%ul.nav-links = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do = icon('user fw') %span - Profile Settings + Profile = nav_link(controller: [:accounts, :two_factor_auths]) do = link_to profile_account_path, title: 'Account' do = icon('gear fw') @@ -27,7 +19,6 @@ = icon('envelope-o fw') %span Emails - %span.count= number_with_delimiter(current_user.emails.count + 1) - unless current_user.ldap_user? = nav_link(controller: :passwords) do = link_to edit_profile_password_path, title: 'Password' do @@ -45,7 +36,6 @@ = icon('key fw') %span SSH Keys - %span.count= number_with_delimiter(current_user.keys.count) = nav_link(controller: :preferences) do = link_to profile_preferences_path, title: 'Preferences' do -# TODO (rspeicher): Better icon? diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 86b46e8c75ec33cf5f4be6463a0c0546f4d98740..a15b7758c4bb089aa203bd3f2781ee4233bcdf33 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -77,7 +77,7 @@ Merge Requests %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) - - if project_nav_tab? :settings + - if project_nav_tab? :team = nav_link(controller: [:project_members, :teams]) do = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do = icon('users fw') diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index dfa6cc5702e2dfb2075e0d09e02b6cfceb5ddd38..b77d3402a2e6381e56e066905c0b5938ed8a4dd6 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,5 +1,6 @@ - page_title "Profile Settings" - header_title "Profile Settings", profile_path unless header_title -- sidebar "profile" +- sidebar "dashboard" +- nav "profile" = render template: "layouts/application" diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index a7ef31acd3dc4c49a36b8a61b02fe333cdde20cf..6dfe7fbdae8115046b6b3aaf44bde52583f3682e 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -17,4 +17,12 @@ - content_for :scripts_body do = render "layouts/init_auto_complete" if current_user +- content_for :header_content do + .js-dropdown-menu-projects + .dropdown-menu.dropdown-select.dropdown-menu-projects + = dropdown_title("Go to a project") + = dropdown_filter("Search your projects") + = dropdown_content + = dropdown_loading + = render template: "layouts/application" diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml index 574e8bfef245a06fe81d8b1540ce683d5fc128de..81c7c88fc96b0dda2306e9a73dabf008281f6acb 100644 --- a/app/views/notify/closed_merge_request_email.html.haml +++ b/app/views/notify/closed_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}" + = "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}" diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml index 59db86b08bc404062f8723b0fb6689e295b25bdb..b435067d5a6618f9363c36aa402653619a9ba391 100644 --- a/app/views/notify/closed_merge_request_email.text.haml +++ b/app/views/notify/closed_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}" += "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}" Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml index c9bf04f514e107ac9565a8df8604943cf6251866..41a320d6bd8260dd4eac16db791812857ce14777 100644 --- a/app/views/notify/merge_request_status_email.html.haml +++ b/app/views/notify/merge_request_status_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request ##{@merge_request.iid} was #{@mr_status} by #{@updated_by.name}" + = "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}" diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml index b96dd0fd8abb901acb088d37cfd2841c77c31fbd..7a5074a1dc3110b4bac765ff210081b2b2f82070 100644 --- a/app/views/notify/merge_request_status_email.text.haml +++ b/app/views/notify/merge_request_status_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request ##{@merge_request.iid} was #{@mr_status} by #{@updated_by.name}" += "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}" Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/merged_merge_request_email.html.haml b/app/views/notify/merged_merge_request_email.html.haml index 6762fae7f64f3c7fd2c3394441341305572c6429..fbe506d4f4ddaa05f53d36822254b4496803a555 100644 --- a/app/views/notify/merged_merge_request_email.html.haml +++ b/app/views/notify/merged_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request ##{@merge_request.iid} was merged" + = "Merge Request #{@merge_request.to_reference} was merged" diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml index 34dbc60e19b5fbbfa2b8026e3f758036798d4cdb..bfbae01094fb4b19a9b073fdd3c68e114132ac49 100644 --- a/app/views/notify/merged_merge_request_email.text.haml +++ b/app/views/notify/merged_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request ##{@merge_request.iid} was merged" += "Merge Request #{@merge_request.to_reference} was merged" Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index bdcca6e4ab7bf12da80c5004163af2f459250807..d4aad8d1862641254549c9a615f5bf9d46c3265f 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -1,4 +1,4 @@ -New Merge Request #<%= @merge_request.iid %> +New Merge Request <%= @merge_request.to_reference %> <%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %> diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb index 1d1411992a653493d1b5fb1f35f584ee6b004efc..8cdab63829e28565522ec0180e51b5803531517c 100644 --- a/app/views/notify/note_merge_request_email.text.erb +++ b/app/views/notify/note_merge_request_email.text.erb @@ -1,4 +1,4 @@ -New comment for Merge Request <%= @merge_request.iid %> +New comment for Merge Request <%= @merge_request.to_reference %> <%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 6efd119f26096082090e9b2a1d1111543420cdf0..afd3d79321f045196517adf9cc53b5796e794902 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,5 +1,4 @@ - page_title "Account" -- header_title page_title, profile_account_path - if current_user.ldap_user? .alert.alert-info diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml index f630c03e5f6fb144801ac70d4274332f65642cd0..9c404b6935fc7e2284d7ed5d327d854cef01b7e7 100644 --- a/app/views/profiles/audit_log.html.haml +++ b/app/views/profiles/audit_log.html.haml @@ -1,5 +1,4 @@ - page_title "Audit Log" -- header_title page_title, audit_log_profile_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 3f328f96cea0cb922bcc127df7daa576433cb26c..57527361eb6a6badc1e1fbacae76a9a65a380bf2 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -1,5 +1,4 @@ - page_title "Emails" -- header_title page_title, profile_emails_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index e0f8c9a573305bd8fc5315f2b06712859a18c8f3..6a067a03535023e0efdb8d882fa69ec8aa6d83cc 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,5 +1,4 @@ - page_title "SSH Keys" -- header_title page_title, profile_keys_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..89ae7ffda2bb7eb0db802cc90aeb7f468935eca3 --- /dev/null +++ b/app/views/profiles/notifications/_group_settings.html.haml @@ -0,0 +1,13 @@ +%li.notification-list-item + %span.notification.fa.fa-holder.append-right-5 + - if setting.global? + = notification_icon(current_user.notification_level) + - else + = notification_icon(setting.level) + + %span.str-truncated + = link_to group.name, group_path(group) + + .pull-right + = form_for [group, setting], remote: true, html: { class: 'update-notifications' } do |f| + = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..17c097154da66b771198cd22ff1b079985dfdf50 --- /dev/null +++ b/app/views/profiles/notifications/_project_settings.html.haml @@ -0,0 +1,13 @@ +%li.notification-list-item + %span.notification.fa.fa-holder.append-right-5 + - if setting.global? + = notification_icon(current_user.notification_level) + - else + = notification_icon(setting.level) + + %span.str-truncated + = link_to_project(project) + + .pull-right + = form_for [project.namespace.becomes(Namespace), project, setting], remote: true, html: { class: 'update-notifications' } do |f| + = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml deleted file mode 100644 index d0d044136f6f7ef07e57ecb797098aa4938ceebe..0000000000000000000000000000000000000000 --- a/app/views/profiles/notifications/_settings.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -%li.notification-list-item - %span.notification.fa.fa-holder.append-right-5 - - if notification.global? - = notification_icon(@notification) - - else - = notification_icon(notification) - - %span.str-truncated - - if membership.kind_of? GroupMember - = link_to membership.group.name, membership.group - - else - = link_to_project(membership.project) - .pull-right - = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do - = hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type') - = hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id') - = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 6609295a2a5517fcd4317cfe3b0e7554ccaa63f4..7696f112bb3a8a0e763679333103808457b005d7 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,8 +1,11 @@ - page_title "Notifications" -- header_title page_title, profile_notifications_path -= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| - = form_errors(@user) +%div + - if @user.errors.any? + %div.alert.alert-danger + %ul + - @user.errors.full_messages.each do |msg| + %li= msg = hidden_field_tag :notification_type, 'global' .row @@ -16,56 +19,55 @@ .col-lg-9 %h5 Global notification settings - .form-group - = f.label :notification_email, class: "label-light" - = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2" - .form-group - = f.label :notification_level, class: 'label-light' - .radio - = f.label :notification_level, value: Notification::N_DISABLED do - = f.radio_button :notification_level, Notification::N_DISABLED - .level-title - Disabled - %p You will not get any notifications via email - .radio - = f.label :notification_level, value: Notification::N_MENTION do - = f.radio_button :notification_level, Notification::N_MENTION - .level-title - On Mention - %p You will receive notifications only for comments in which you were @mentioned + = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| + .form-group + = f.label :notification_email, class: "label-light" + = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2" + .form-group + = f.label :notification_level, class: 'label-light' + .radio + = f.label :notification_level, value: :disabled do + = f.radio_button :notification_level, :disabled + .level-title + Disabled + %p You will not get any notifications via email - .radio - = f.label :notification_level, value: Notification::N_PARTICIPATING do - = f.radio_button :notification_level, Notification::N_PARTICIPATING - .level-title - Participating - %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) + .radio + = f.label :notification_level, value: :mention do + = f.radio_button :notification_level, :mention + .level-title + On Mention + %p You will receive notifications only for comments in which you were @mentioned - .radio - = f.label :notification_level, value: Notification::N_WATCH do - = f.radio_button :notification_level, Notification::N_WATCH - .level-title - Watch - %p You will receive notifications for any activity + .radio + = f.label :notification_level, value: :participating do + = f.radio_button :notification_level, :participating + .level-title + Participating + %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) - .prepend-top-default - = f.submit 'Update settings', class: "btn btn-create" + .radio + = f.label :notification_level, value: :watch do + = f.radio_button :notification_level, :watch + .level-title + Watch + %p You will receive notifications for any activity + + .prepend-top-default + = f.submit 'Update settings', class: "btn btn-create" %hr -.col-lg-9.col-lg-push-3 - %h5 - Groups (#{@group_members.count}) - %div - %ul.bordered-list - - @group_members.each do |group_member| - - notification = Notification.new(group_member) - = render 'settings', type: 'group', membership: group_member, notification: notification - %h5 - Projects (#{@project_members.count}) - %p.account-well - To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group. - .append-bottom-default - %ul.bordered-list - - @project_members.each do |project_member| - - notification = Notification.new(project_member) - = render 'settings', type: 'project', membership: project_member, notification: notification + %h5 + Groups (#{@group_notifications.count}) + %div + %ul.bordered-list + - @group_notifications.each do |setting| + = render 'group_settings', setting: setting, group: setting.source + %h5 + Projects (#{@project_notifications.count}) + %p.account-well + To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there. + .append-bottom-default + %ul.bordered-list + - @project_notifications.each do |setting| + = render 'project_settings', setting: setting, project: setting.source diff --git a/app/views/profiles/notifications/update.js.haml b/app/views/profiles/notifications/update.js.haml deleted file mode 100644 index 84c6ab25599e4356f30a14c33264d884a51b3e07..0000000000000000000000000000000000000000 --- a/app/views/profiles/notifications/update.js.haml +++ /dev/null @@ -1,6 +0,0 @@ -- if @saved - :plain - new Flash("Notification settings saved", "notice") -- else - :plain - new Flash("Failed to save new settings", "alert") diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 5ac8a8b9d093e9f3340d621384911e4274ca4a38..243428b690e4d2247081a9d7451d046e3e879eb4 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Password" -- header_title page_title, edit_profile_password_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index f80211669fb4ab998e3ba815f75c56e863c6a426..bfe53be6854c0ac1c725be966bc11bc1884eff95 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,5 +1,4 @@ - page_title 'Preferences' -- header_title page_title, profile_preferences_path = 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 diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 961b61d2e763d02264648b299f3f96bfdb42a7bd..48b0dd6b12142c132cea9a6549e73ca0f3e64d37 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -9,4 +9,7 @@ = spinner :javascript - new Activities(); + var activity = new Activities(); + $(document).on('page:restore', function (event) { + activity.reloadActivities() + }) diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml index 9ae6964aaac044ed1e63dc8955d593d3c24016f4..0de019983ca4ef1e8de021fdbc198b3d766d9820 100644 --- a/app/views/projects/_builds_settings.html.haml +++ b/app/views/projects/_builds_settings.html.haml @@ -52,6 +52,12 @@ %li phpunit --coverage-text --colors=never (PHP) - %code ^\s*Lines:\s*\d+.\d+\% + %li + gcovr (C/C++) - + %code ^TOTAL.*\s+(\d+\%)$ + %li + tap --coverage-report=text-summary (Node.js) - + %code ^Statements\s*:\s*([^%]+) .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml index 386d72e77872333baab57b66ac26386bb24bcbd1..66c30283c7a39638d1109d44b30380e0162edf42 100644 --- a/app/views/projects/_last_commit.html.haml +++ b/app/views/projects/_last_commit.html.haml @@ -1,9 +1,8 @@ .project-last-commit - - ci_commit = project.ci_commit(commit.sha) - - if ci_commit - = link_to ci_status_path(ci_commit), class: "ci-status ci-#{ci_commit.status}" do - = ci_status_icon(ci_commit) - = ci_status_label(ci_commit) + - if commit.status + = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{commit.status}" do + = ci_icon_for_status(commit.status) + = ci_label_for_status(commit.status) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message" diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 7a78d61a611fabfac4276396fbef43dab47264e4..8de44a6c91498033972c31f9d8ca1ec344d73c97 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,6 +1,6 @@ .md-area .md-header - %ul.nav-links + %ul.nav-links.clearfix %li.active %a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 } Write diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index d1191928d4fd36811b80baa02cccbb4c09cc6961..a9908eaeccaf0c5f95806c65e36b57cd2662788e 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -9,7 +9,7 @@ - else .gray-content-block.second-block.center %h3.page-title - This project does not have README yet + This project does not have a README yet - if can?(current_user, :push_code, @project) %p A @@ -18,5 +18,5 @@ distributed with computer software, forming part of its documentation. %p We recommend you to - = link_to "add README", new_readme_path, class: 'underlined-link' + = link_to "add a README", new_readme_path, class: 'underlined-link' file to the repository and GitLab will render it here instead of this message. diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index bddff5cdcbc5a990cb09fe735ce496dd3084181e..e1e3501396851682cf8d897c13dc838ee9e2a4d7 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,8 +1,8 @@ .zen-backdrop - classes << ' js-gfm-input js-autosize markdown-area' - if defined?(f) && f - = f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..." + = f.text_area attr, class: classes, placeholder: placeholder - else - = text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..." + = text_area_tag attr, nil, class: classes, placeholder: placeholder %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index f8b6fa253c4ff9ddfa6714976bfae25dbb9ba867..fefa652a3da9ccd9350e58bab8b24a065fbb1095 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -13,7 +13,11 @@ required: true, class: 'form-control new-file-name' .pull-right - = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' + .license-selector.js-license-selector.hide + = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name} + + .encoding-selector + = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' .file-content.code %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]} diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index abcfca4cd11bf876978ca7e8023502c5534b39e5..38e62c81fed95fe850fe616dfd7a897e9dc09b0e 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -1,20 +1,20 @@ - if @lines.present? - if @form.unfold? && @form.since != 1 && !@form.bottom? %tr.line_holder{ id: @form.since } - = render "projects/diffs/match_line", {line: @match_line, - line_old: @form.since, line_new: @form.since, bottom: false, new_file: false} + = render "projects/diffs/match_line", { line: @match_line, + line_old: @form.since, line_new: @form.since, bottom: false, new_file: false } - @lines.each_with_index do |line, index| - line_new = index + @form.since - line_old = line_new - @form.offset %tr.line_holder - %td.old_line.diff-line-num{data: {linenumber: line_old}} + %td.old_line.diff-line-num{ data: { linenumber: line_old } } = link_to raw(line_old), "#" - %td.new_line.diff-line-num + %td.new_line.diff-line-num{ data: { linenumber: line_old } } = link_to raw(line_new) , "#" %td.line_content.noteable_line==#{' ' * @form.indent}#{line} - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to } - = render "projects/diffs/match_line", {line: @match_line, - line_old: @form.to, line_new: @form.to, bottom: true, new_file: false} + = render "projects/diffs/match_line", { line: @match_line, + line_old: @form.to, line_new: @form.to, bottom: true, new_file: false } diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 1dd2b5c0af76a209a2e3314cad0cbf0d9ba57626..0459699432e8b985f48d0460621543a1277897aa 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -14,5 +14,5 @@ cancel_path: namespace_project_tree_path(@project.namespace, @project, @id) :javascript - blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null) + blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}") new NewCommitForm($('.js-new-blob-form')) diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index aa85f495e396ec3a998a8a94fbeeb85b988a9f8f..0406fc21d7799d3ae26116a5b217a6bec1a7f6ad 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -58,6 +58,6 @@ %th Coverage %th - = render @builds, commit_sha: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled? + = render @builds, commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled? = paginate @builds, theme: 'gitlab' diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index b02aee3db212e730bccf6605778799c326c15228..99d72aa7935a41303abb6eee7140fbbed1a002bd 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -10,10 +10,10 @@ - merge_request = @build.merge_request - if merge_request via - = link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request) + = link_to "merge request #{merge_request.to_reference}", merge_request_path(merge_request) #up-build-trace - - builds = @build.commit.matrix_builds(@build) + - builds = @build.commit.builds.latest.to_a - if builds.size > 1 %ul.nav-links.no-top.no-bottom - builds.each do |build| @@ -110,7 +110,7 @@ = icon('folder-open') Browse - .build-widget + .build-widget.build-controls %h4.title Build ##{@build.id} - if can?(current_user, :update_build, @project) @@ -127,6 +127,9 @@ data: { confirm: 'Are you sure you want to erase this build?' } do = icon('eraser') Erase + - if @build.has_trace? + = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), + class: 'btn btn-sm btn-success', target: '_blank' .clearfix - if @build.duration diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index a3786c35a1f93400ec9e8d9d81e2c5116004b116..c1e3e5b73a2c3d945767969881aff32897765bfc 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,20 +1,11 @@ -- case @membership -- when ProjectMember - = form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do - = hidden_field_tag :notification_type, 'project' - = hidden_field_tag :notification_id, @membership.id - = hidden_field_tag :notification_level +- if @notification_setting + = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f| + = f.hidden_field :level %span.dropdown %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} = icon('bell') - = notification_label(@membership) + = notification_title(@notification_setting.level) = icon('angle-down') %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - - Notification.project_notification_levels.each do |level| - = notification_list_item(level, @membership) - -- when GroupMember - .btn.disabled.notifications-btn.has-tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."} - = icon('bell') - = notification_label(@membership) - = icon('angle-down') + - NotificationSetting.levels.each do |level| + = notification_list_item(level.first, @notification_setting) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 2cf9115e4ddec0ab079a01896e2983051ecc4e19..e123eb1cc7a339c03a6c675b0e4b5be150e6b81b 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -19,11 +19,12 @@ %td = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace" - %td - - if build.ref - = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref) - - else - .light none + - if defined?(ref) && ref + %td + - if build.ref + = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref) + - else + .light none - if defined?(runner) && runner %td @@ -48,6 +49,8 @@ %span.label.label-info triggered - if build.try(:allow_failure) %span.label.label-danger allowed to fail + - if defined?(retried) && retried + %span.label.label-warning retried %td.duration - if build.duration diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index 003b7c18d0e699230454ffed82dfa300af6cd0f4..5c9a319edebeb0c4b4d1da5165549e7bf76d10da 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -1,67 +1,2 @@ -.gray-content-block.middle-block - .pull-right - - if can?(current_user, :update_build, @ci_commit.project) - - if @ci_commit.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post - - - if @ci_commit.builds.running_or_pending.any? - = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post - - .oneline - = pluralize @statuses.count(:id), "build" - - if defined?(link_to_commit) && link_to_commit - for commit - = link_to @ci_commit.short_sha, namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: "monospace" - - if @ci_commit.duration > 0 - in - = time_interval_in_words @ci_commit.duration - -- if @ci_commit.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - @ci_commit.yaml_errors.split(",").each do |error| - %li= error - You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} - -- if @ci_commit.project.builds_enabled? && !@ci_commit.ci_yaml_file - .bs-callout.bs-callout-warning - \.gitlab-ci.yml not found in this commit - -.table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @ci_commit.project.build_coverage_enabled? - %th Coverage - %th - - @ci_commit.refs.each do |ref| - - builds = @ci_commit.statuses.for_ref(ref).latest.ordered - = render builds, coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true - -- if @ci_commit.retried.any? - .gray-content-block.second-block - Retried builds - - .table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @ci_commit.project.build_coverage_enabled? - %th Coverage - %th - = render @ci_commit.retried, coverage: @ci_commit.project.build_coverage_enabled?, stage: true +- @ci_commits.each do |ci_commit| + = render "ci_commit", ci_commit: ci_commit diff --git a/app/views/projects/commit/_revert.html.haml b/app/views/projects/commit/_change.html.haml similarity index 62% rename from app/views/projects/commit/_revert.html.haml rename to app/views/projects/commit/_change.html.haml index 52ca3ed5b140b3d92a87d3db1fc1e86381e24552..44ef1fdbbe32c210fd537f017a4fe087da6d6fa0 100644 --- a/app/views/projects/commit/_revert.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -1,13 +1,21 @@ -#modal-revert-commit.modal +- case type.to_s +- when 'revert' + - label = 'Revert' + - target_label = 'Revert in branch' +- when 'cherry-pick' + - label = 'Cherry-pick' + - target_label = 'Pick into branch' + +.modal{id: "modal-#{type}-commit"} .modal-dialog .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3.page-title== Revert this #{revert_commit_type(commit)} + %h3.page-title== #{label} this #{commit.change_type_title} .modal-body - = form_tag revert_namespace_project_commit_path(@project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do + = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do .form-group.branch - = label_tag 'target_branch', 'Revert in branch', class: 'control-label' + = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 = select_tag "target_branch", grouped_options_refs, class: "select2 select2-sm js-target-branch" - if can?(current_user, :push_code, @project) @@ -20,7 +28,7 @@ - else = hidden_field_tag 'create_merge_request', 1 .form-actions - = submit_tag "Revert", class: 'btn btn-create' + = submit_tag label, class: 'btn btn-create' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" - unless can?(current_user, :push_code, @project) @@ -28,4 +36,4 @@ = commit_in_fork_help :javascript - new NewCommitForm($('.js-create-dir-form')) + new NewCommitForm($('.js-#{type}-form')) diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..25714e6cb476a0cb6321a48ba3aa6fc2a1993c9f --- /dev/null +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -0,0 +1,69 @@ +.gray-content-block.middle-block + .pull-right + - if can?(current_user, :update_build, @project) + - if ci_commit.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post + + - if ci_commit.builds.running_or_pending.any? + = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + + .oneline + = pluralize ci_commit.statuses.count(:id), "build" + - if ci_commit.ref + for + %span.label.label-info + = ci_commit.ref + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace" + - if ci_commit.duration > 0 + in + = time_interval_in_words ci_commit.duration + +- if ci_commit.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - ci_commit.yaml_errors.split(",").each do |error| + %li= error + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + +- if @project.builds_enabled? && !ci_commit.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + +.table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Stage + %th Name + %th Duration + %th Finished at + - if @project.build_coverage_enabled? + %th Coverage + %th + - builds = ci_commit.statuses.latest.ordered + = render builds, coverage: @project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true + +- if ci_commit.retried.any? + .gray-content-block.second-block + Retried builds + + .table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Ref + %th Stage + %th Name + %th Duration + %th Finished at + - if @project.build_coverage_enabled? + %th Coverage + %th + = render ci_commit.retried, coverage: @project.build_coverage_enabled?, stage: true, ref: false diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 71995fcc487aea3db796952e4ff299880e2444d4..3d7c18a5f58008af7d3886e55e4a2711a58b14bb 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -18,6 +18,7 @@ Browse Files - unless @commit.has_been_reverted?(current_user) = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id)) + = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id)) %div %p @@ -42,12 +43,12 @@ - @commit.parents.each do |parent| = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace" -- if @ci_commit +- if @commit.status .pull-right - = link_to ci_status_path(@ci_commit), class: "ci-status ci-#{@ci_commit.status}" do - = ci_status_icon(@ci_commit) + = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status ci-#{@commit.status}" do + = ci_icon_for_status(@commit.status) build: - = ci_status_label(@ci_commit) + = ci_label_for_status(@commit.status) .commit-info-row.branches %i.fa.fa-spinner.fa-spin diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 21e186120c375666f65a9be3ff97f36f9006336f..e5e3d69603523c3797ede13e943c61f0cb286292 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -5,7 +5,7 @@ .prepend-top-default = render "commit_box" -- if @ci_commit +- if @commit.status = render "ci_menu" - else %div.block-connector @@ -13,4 +13,5 @@ diff_refs: @diff_refs = render "projects/notes/notes_with_form" - if can_collaborate_with_project? - = render "projects/commit/revert", commit: @commit, title: @commit.title + - %w(revert cherry-pick).each do |type| + = render "projects/commit/change", type: type, commit: @commit, title: @commit.title diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 7da892312435f4c33da0015d0105d1d9563bd7d9..c7d8c9a0d15b319f43441adab71c161393541d8e 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -4,21 +4,20 @@ - notes = commit.notes - note_count = notes.user.count -- ci_commit = project.ci_commit(commit.sha) - cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] -- cache_key.push(ci_commit.status) if ci_commit +- cache_key.push(commit.status) if commit.status = cache(cache_key) do %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } .commit-row-title - %span.item-title.str-truncated + %span.item-title = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" - if commit.description? %a.text-expander.js-toggle-button ... .pull-right - - if ci_commit - = render_ci_status(ci_commit) + - if commit.status + = render_ci_status(commit) = clipboard_button(clipboard_text: commit.id) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" @@ -31,5 +30,5 @@ by = commit_author_link(commit, avatar: true, size: 24) .committed_ago - #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} + #{time_ago_with_tooltip(commit.committed_date)} = link_to_browse_code(project, commit) diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 9464c8dc9965b9285584dae3b915117237a0cc17..107097ad9635b658498a5fff0ff072d0d24172ce 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,26 +1,26 @@ - type = line.type -%tr.line_holder{id: line_code, class: type} +%tr.line_holder{ id: line_code, class: type } - case type - when 'match' - = render "projects/diffs/match_line", {line: line.text, - line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file} + = render "projects/diffs/match_line", { line: line.text, + line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file } - when 'nonewline' %td.old_line.diff-line-num %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num{class: type} - - link_text = raw(type == "new" ? " " : line.old_pos) + %td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } + - link_text = type == "new" ? " ".html_safe : line.old_pos - if defined?(plain) && plain = link_text - else - = link_to link_text, "##{line_code}", id: line_code + = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code) - %td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}} - - link_text = raw(type == "old" ? " " : line.new_pos) + %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } + - link_text = type == "old" ? " ".html_safe : line.new_pos - if defined?(plain) && plain = link_text - else - = link_to link_text, "##{line_code}", id: line_code - %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text) + = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } + %td.line_content{ class: ['noteable_line', type, line_code], data: { line_code: line_code } }= diff_line_content(line.text, type) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index d7c490687452048974bb951e4f9232676644a70e..81948513e43c5cc2d93177677428d6eeeb244d32 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -14,11 +14,11 @@ %td.new_line.diff-line-num %td.line_content.parallel.match= left[:text] - else - %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]}"} + %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(left[:line_code], 'old') - %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) + %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) - if right[:type] == 'new' - new_line_class = 'new' @@ -27,11 +27,11 @@ - new_line_class = nil - new_line_code = left[:line_code] - %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class}", data: { linenumber: right[:number] }} + %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(right[:line_code], 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) - if @reply_allowed - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 37ac0aa1f5862c6027c9a01e9df5a988b193b8f4..251fba474afa9f2bc011dfbcb25c3a6e0df00b7c 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -226,6 +226,7 @@ %li Be careful. Changing the project's namespace can have unintended side effects. %li You can only transfer the project to namespaces you manage. %li You will need to update your local repositories to point to the new location. + %li Project visibility level will be changed to match namespace rules when transfering to a group. .form-actions = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - else diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 6ad7b05155a32b2dc1297fea821e6f5d1bf96678..52d093871b40f8f78ed35ead662940863a6010fa 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -14,8 +14,10 @@ %p If you already have files you can push them using command line instructions below. %p - Otherwise you can start with - = link_to "adding README", new_readme_path, class: 'underlined-link' + Otherwise you can start with adding a + = link_to "README", new_readme_path, class: 'underlined-link' + or a + = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link' file to this project. - if can?(current_user, :push_code, @project) diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index c15386b48831e093b51cce5531915ffb79bf577b..f21c864e35c44e06aa5d93e4410ca90bcb95d208 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -15,12 +15,13 @@ - if defined?(commit_sha) && commit_sha %td = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace" - - %td - - if generic_commit_status.ref - = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref) - - else - .light none + + - if defined?(ref) && ref + %td + - if generic_commit_status.ref + = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref) + - else + .light none - if defined?(runner) && runner %td diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index e39224d86c6a1c88f60c31961ca6492bd40fb7db..aae3abcad4b7efceb67a10e5c21c3aeeb8f2feb8 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -74,16 +74,15 @@ .panel.panel-default .panel-heading Webhooks (#{@hooks.count}) - %ul.well-list + %ul.content-list - @hooks.each do |hook| %li - .pull-right + .controls = link_to 'Test Hook', test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped" = link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" - .clearfix - %span.monospace= hook.url - %p + .monospace= hook.url + %div - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray= trigger.titleize - SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} + %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 33c48199ba589b3fff743d001117fd34f4bc2bb1..7076f5db015fe93af4e8994fcbbc694b8986c87b 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-quick-submit js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f| = render 'shared/issuable/form', f: f, issuable: @issue :javascript diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 4aa92d0b39e2470046354cc9ff8d860286806bf1..9ad86ed71c98f531a050ec6bfc53398a6b38c2a8 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -27,7 +27,7 @@ = icon('thumbs-down') = downvotes - - note_count = issue.notes.user.count + - note_count = issue.notes.user.nonawards.count - if note_count > 0 %li = link_to issue_path(issue) + "#notes" do @@ -48,6 +48,11 @@ = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do = icon('clock-o') = issue.milestone.title + - if issue.due_date + %span{class: "#{'cred' if issue.overdue?}"} + + = icon('calendar') + = issue.due_date.to_s(:medium) - if issue.labels.any? - issue.labels.each do |label| diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index b10cd03515f28866dd98e0b732ef52f55f3abf96..bdfa0c7009eecab628777abcccc44cd67bc34cbe 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -5,7 +5,7 @@ - @related_branches.each do |branch| %li - sha = @project.repository.find_branch(branch).target - - ci_commit = @project.ci_commit(sha) if sha + - ci_commit = @project.ci_commit(sha, branch) if sha - if ci_commit %span.related-branch-ci-status = render_ci_status(ci_commit) diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 6fa059cbe68a179dbb0c886ac661354671ffcbf1..bde80bbb54b228667f89a3ce599c20caed293d58 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,80 +1,79 @@ - page_title "#{@issue.title} (##{@issue.iid})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes +- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project)) -= render "header_title" +.clearfix.detail-page-header + .issuable-header + .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) } + = icon('check', class: "hidden-sm hidden-md hidden-lg") + %span.hidden-xs + Closed + .issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) } + = icon('circle-o', class: "hidden-sm hidden-md hidden-lg") + %span.hidden-xs Open -.issue - .detail-page-header.issuable-header - .pull-left - .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} - %span.hidden-xs - Closed - %span.hidden-sm.hidden-md.hidden-lg - = icon('check') - .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} - %span.hidden-xs - Open - %span.hidden-sm.hidden-md.hidden-lg - = icon('circle-o') - - %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" } + %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" } = icon('angle-double-left') - .issue-meta + .issuable-meta = confidential_icon(@issue) - %strong.identifier - Issue ##{@issue.iid} - %span.creator - opened - .editor-details - .editor-details - = time_ago_with_tooltip(@issue.created_at) - by - %strong - = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-xs") - %strong - = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg", - by_username: true, avatar: false) + = issuable_meta(@issue, @project, "Issue") + + - if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue) + .issuable-actions + .clearfix.issue-btn-group.dropdown + %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } } + %span.caret + Options + .dropdown-menu.dropdown-menu-align-right.hidden-lg + %ul + - if can?(current_user, :create_issue, @project) + %li + = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link' + - if can?(current_user, :update_issue, @issue) + %li + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + %li + = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, 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 can?(current_user, :create_issue, @project) + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do + = icon('plus') + New issue + - if can?(current_user, :update_issue, @issue) + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit' do + = icon('pencil-square-o') + Edit - .pull-right.issue-btn-group - - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do - = icon('plus') - New issue - - if can?(current_user, :update_issue, @issue) - = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do - = icon('pencil-square-o') - Edit +.issue-details.issuable-details + .detail-page-description.content-block + %h2.title + = markdown escape_once(@issue.title), pipeline: :single_line + - if @issue.description.present? + .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } + .wiki + = preserve do + = markdown(@issue.description, cache_key: [@issue, "description"]) + %textarea.hidden.js-task-list-field + = @issue.description + = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') - .issue-details.issuable-details - .detail-page-description.content-block - %h2.title - = markdown escape_once(@issue.title), pipeline: :single_line - %div - - if @issue.description.present? - .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''} - .wiki - = preserve do - = markdown(@issue.description, cache_key: [@issue, "description"]) - %textarea.hidden.js-task-list-field - = @issue.description - = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') + #merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } } + // This element is filled in using JavaScript. - .merge-requests - = render 'merge_requests' - = render 'related_branches' + #related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } } + // This element is filled in using JavaScript. - .content-block.content-block-small - = render 'new_branch' - = render 'votes/votes_block', votable: @issue + .content-block.content-block-small + = render 'new_branch' + = render 'votes/votes_block', votable: @issue - .row - %section.col-md-12 - .issuable-discussion - = render 'projects/issues/discussion' + %section.issuable-discussion + = render 'projects/issues/discussion' = render 'shared/issuable/sidebar', issuable: @issue diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml index 986d8c220dbbd7ee4530c233949368921c34bdd1..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/app/views/projects/issues/update.js.haml +++ b/app/views/projects/issues/update.js.haml @@ -1,3 +0,0 @@ -$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}"; -$('aside.right-sidebar').effect('highlight'); -new IssuableContext(); diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 097a65969a65010ee76c532bdf8a2e7a49491395..8bf544b83710b804438ddad6931be20387313b31 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -14,7 +14,7 @@ .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}} .subscription-status{data: {status: label_subscription_status(label)}} - %a.subscribe-button.btn.action-buttons{data: {toggle: "tooltip"}} + %button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: "button", data: { toggle: "tooltip" } } %span= label_subscription_toggle_button_text(label) - if can? current_user, :admin_label, @project diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 3e4ab09c6d41366914838bbd3830edf769436fc9..88525f4036a7ac2f13f1124d6b82c54991e05d3d 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request :javascript diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 391193eed6c6c22abe2878386ee143878632326a..e740fe8c84d64b1fb7ba77af698b8a304bc8a642 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -35,7 +35,7 @@ = icon('thumbs-down') = downvotes - - note_count = merge_request.mr_and_commit_notes.user.count + - note_count = merge_request.mr_and_commit_notes.user.nonawards.count - if note_count > 0 %li = link_to merge_request_path(merge_request) + "#notes" do diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 7d7c487e9709daebd056021597f754486e212485..b08524574e40fad7dc77f225c1bf0f51f825237b 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -16,11 +16,9 @@ = dropdown_title("Select source project") = dropdown_filter("Search projects") = dropdown_content do - - is_active = f.object.source_project_id == @merge_request.source_project.id - %ul - %li - %a{ href: "#", class: "#{("is-active" if is_active)}", data: { id: @merge_request.source_project.id } } - = @merge_request.source_project_path + = render 'projects/merge_requests/dropdowns/project', + projects: [@merge_request.source_project], + selected: f.object.source_project_id .merge-request-select.dropdown = f.hidden_field :source_branch = dropdown_toggle "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" } @@ -28,11 +26,9 @@ = dropdown_title("Select source branch") = dropdown_filter("Search branches") = dropdown_content do - %ul - - @merge_request.source_branches.each do |branch| - %li - %a{ href: "#", class: "#{("is-active" if f.object.source_branch == branch)}", data: { id: branch } } - = branch + = render 'projects/merge_requests/dropdowns/branch', + branches: @merge_request.source_branches, + selected: f.object.source_branch .panel-footer = icon('spinner spin', class: 'js-source-loading') %ul.list-unstyled.mr_source_commit @@ -50,11 +46,9 @@ = dropdown_title("Select target project") = dropdown_filter("Search projects") = dropdown_content do - %ul - - projects.each do |project| - %li - %a{ href: "#", class: "#{("is-active" if f.object.target_project_id == project.id)}", data: { id: project.id } } - = project.path_with_namespace + = render 'projects/merge_requests/dropdowns/project', + projects: projects, + selected: f.object.target_project_id .merge-request-select.dropdown = f.hidden_field :target_branch = dropdown_toggle f.object.target_branch, { toggle: "dropdown", field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch" } @@ -62,11 +56,9 @@ = dropdown_title("Select target branch") = dropdown_filter("Search branches") = dropdown_content do - %ul - - @merge_request.target_branches.each do |branch| - %li - %a{ href: "#", class: "#{("is-active" if f.object.target_branch == branch)}", data: { id: branch } } - = branch + = render 'projects/merge_requests/dropdowns/branch', + branches: @merge_request.target_branches, + selected: f.object.target_branch .panel-footer = icon('spinner spin', class: "js-target-loading") %ul.list-unstyled.mr_target_commit diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 9e59f7df71be8c2986b3128844c15f9809d611b0..2f14a91e64f4c1ced8b70b8521872a6a796ac215 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -10,7 +10,7 @@ %span.pull-right = link_to 'Change branches', mr_change_branches_path(@merge_request) %hr -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request = f.hidden_field :source_project_id = f.hidden_field :source_branch diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 1dd8f721f7efbfc8b944335f27f956aeabda5dfb..8d05060f5638e21f5f045e300b71658fad4c1591 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,8 +1,7 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - -= render "header_title" +- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project)) - if params[:view] == 'parallel' - fluid_layout true @@ -32,8 +31,8 @@ %span Request to merge %span.label-branch= source_branch_with_namespace(@merge_request) %span into - = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do - = @merge_request.target_branch + %span.label-branch + = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) - if @merge_request.open? && @merge_request.diverged_from_target_branch? %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) @@ -51,7 +50,7 @@ %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count + %span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count %li.commits-tab = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits @@ -87,8 +86,10 @@ = spinner = render 'shared/issuable/sidebar', issuable: @merge_request -- if @merge_request.can_be_reverted? - = render "projects/commit/revert", commit: @merge_request.merge_commit, title: @merge_request.title +- if @merge_request.can_be_reverted?(current_user) + = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title +- if @merge_request.can_be_cherry_picked? + = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title :javascript var merge_request; diff --git a/app/views/projects/merge_requests/dropdowns/_branch.html.haml b/app/views/projects/merge_requests/dropdowns/_branch.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..ba8d9a5835c010c18c96dbcb3fcf551e1caf1be0 --- /dev/null +++ b/app/views/projects/merge_requests/dropdowns/_branch.html.haml @@ -0,0 +1,5 @@ +%ul + - branches.each do |branch| + %li + %a{ href: '#', class: "#{('is-active' if selected == branch)}", data: { id: branch } } + = branch diff --git a/app/views/projects/merge_requests/dropdowns/_project.html.haml b/app/views/projects/merge_requests/dropdowns/_project.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..25d5dc92f8aaa950cc558a1e08bd413846ff022e --- /dev/null +++ b/app/views/projects/merge_requests/dropdowns/_project.html.haml @@ -0,0 +1,5 @@ +%ul + - projects.each do |project| + %li + %a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id } } + = project.path_with_namespace diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index fc62bb5bce9ae72d5e310ac5e33b2c049aa1a8fe..b31ea5e532157afd0421d130ffced1ab4f7aaed7 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -1,7 +1,7 @@ -- page_title "Edit", "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" +- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" = render "header_title" %h3.page-title - Edit Merge Request ##{@merge_request.iid} + Edit Merge Request #{@merge_request.to_reference} %hr = render 'form' diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml index fc03ee73a3d571870815f9923a8d7fb8a07289ef..8ac653427c937732fb650fb2d6297d4b443f2ef5 100644 --- a/app/views/projects/merge_requests/invalid.html.haml +++ b/app/views/projects/merge_requests/invalid.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" +- page_title "#{@merge_request.title} (#{merge_request.to_reference}", "Merge Requests" = render "header_title" .merge-request diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml index 307a75d02cac0c3ce4f6bf17960e564b76067567..a116ffe2e151ed27664ff47d3d40d784e85cc1e9 100644 --- a/app/views/projects/merge_requests/show/_builds.html.haml +++ b/app/views/projects/merge_requests/show/_builds.html.haml @@ -1 +1,2 @@ -= render "projects/commit/builds", link_to_commit: true += render "projects/commit/ci_commit", ci_commit: @ci_commit, link_to_commit: true + diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index ab4b1f14be5ab6146e008fdf7d92ef3ed8b804f1..36c275e8be11efdaa2adef6220abf1148ea2db05 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,35 +1,32 @@ -.detail-page-header - .status-box{ class: status_box_class(@merge_request) } - %span.hidden-xs - = @merge_request.state_human_name - %span.hidden-sm.hidden-md.hidden-lg - = icon(@merge_request.state_icon_name) - %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" } - = icon('angle-double-left') - .issue-meta - %strong.identifier - %span.hidden-sm.hidden-md.hidden-lg - MR +.clearfix.detail-page-header + .issuable-header + .issuable-status-box.status-box{ class: status_box_class(@merge_request) } + = icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg") %span.hidden-xs - Merge Request - !#{@merge_request.iid} - %span.creator - opened - .editor-details - = time_ago_with_tooltip(@merge_request.created_at) - by - %strong - = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-xs") - %strong - = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg", - by_username: true, avatar: false) + = @merge_request.state_human_name - .issue-btn-group.pull-right - - if can?(current_user, :update_merge_request, @merge_request) - - if @merge_request.open? - = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close merge request' - = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-nr btn-grouped issuable-edit', id: 'edit_merge_request' do + %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" } + = icon('angle-double-left') + + .issuable-meta + = issuable_meta(@merge_request, @project, "Merge Request") + + - if can?(current_user, :update_merge_request, @merge_request) + .issuable-actions + .clearfix.issue-btn-group.dropdown + %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } } + %span.caret + Options + .dropdown-menu.dropdown-menu-align-right.hidden-lg + %ul + %li{ class: issue_button_visibility(@merge_request, true) } + = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' + %li{ class: issue_button_visibility(@merge_request, false) } + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' + %li + = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit' + = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request' + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' + = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit" do = icon('pencil-square-o') Edit - - if @merge_request.closed? - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-nr btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request' diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml index 9cce5660e1c509c8dfbcce51614f6a42869bf122..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/app/views/projects/merge_requests/update.js.haml +++ b/app/views/projects/merge_requests/update.js.haml @@ -1,3 +0,0 @@ -$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}"; -$('aside.right-sidebar').effect('highlight'); -new IssuableContext(); diff --git a/app/views/projects/merge_requests/update_branches.html.haml b/app/views/projects/merge_requests/update_branches.html.haml index 1b93188a10c70a2683224bac1e0e4dcd8d14cec6..64482973a89f34ae962a44d7a5071837a59afc17 100644 --- a/app/views/projects/merge_requests/update_branches.html.haml +++ b/app/views/projects/merge_requests/update_branches.html.haml @@ -1,5 +1,3 @@ -%ul - - @target_branches.each do |branch| - %li - %a{ href: "#", class: "#{("is-active" if "a" == branch)}", data: { id: branch } } - = branch += render 'projects/merge_requests/dropdowns/branch', +branches: @target_branches, +selected: nil diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index 3abae9f0bf6b82813be2092ebce6ea6634985375..ec4beae9727b3ec07e64d584ae92e11662843b36 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -44,3 +44,8 @@ $('.remove_source_branch_in_progress').hide(); $('.remove_source_branch_widget.failed').show(); }); + - else + %p + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. + = render 'projects/merge_requests/widget/merged_buttons' diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml index 85a3a6ba9e2c5c50820ca020aa25fd1b9087973a..56167509af97e75ad6bda88ef27b8a3f52f48c6d 100644 --- a/app/views/projects/merge_requests/widget/_merged_buttons.haml +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -1,11 +1,14 @@ -- source_branch_exists = local_assigns.fetch(:source_branch_exists, false) -- mr_can_be_reverted = @merge_request.can_be_reverted? +- can_remove_source_branch = local_assigns.fetch(:source_branch_exists, false) && @merge_request.can_remove_source_branch?(current_user) +- mr_can_be_reverted = @merge_request.can_be_reverted?(current_user) +- mr_can_be_cherry_picked = @merge_request.can_be_cherry_picked? -- if source_branch_exists || mr_can_be_reverted +- if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked .btn-group - - if source_branch_exists + - if can_remove_source_branch = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-grouped btn-sm remove_source_branch" do = icon('trash-o') Remove Source Branch - if mr_can_be_reverted = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') + - if mr_can_be_cherry_picked + = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index b2dae1c70ee0acb614d9dca54b3808a7df5a8f17..687222fa92f3a356e3a29b6a148f3d9a3fc323b8 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,6 +1,5 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f| += form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input'} do |f| = form_errors(@milestone) - .row .col-md-6 .form-group @@ -11,7 +10,7 @@ = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' = render 'projects/notes/hints' .clearfix .error-alert diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index b8068835b3ace9eb47c059def33c55d7b4fd9414..572b00a38c76a73a02f234644090015984740114 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -1,5 +1,5 @@ - note = discussion_notes.first -.timeline-entry +%li.note.note-discussion.timeline-entry .timeline-entry-inner .timeline-icon = link_to user_path(note.author) do diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 23e4f93eab5a3d9586412710febc7c8c15eaee18..c87a3fadf725996ff5d100a34fde27fee9096cb5 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -2,7 +2,7 @@ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f| = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do - = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .note-form-actions.clearfix diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index c446ecec2c386d20f617f5120e3cc7d5b036566f..d0ac380f216c41ea509208bc8c3822c7876c5825 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f| += form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form" }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view = hidden_field_tag :line_type = note_target_fields(@note) @@ -8,7 +8,7 @@ = f.hidden_field :noteable_type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .error-alert diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 5c42423541e6021f9b39667a41cf20db8433b4fc..aeb7c1d5ee43df0ec6a75360f1b030778db8cca9 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -1,4 +1,5 @@ -%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)] } +- note_editable = note_editable?(note) +%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } .timeline-entry-inner .timeline-icon %a{href: user_path(note.author)} @@ -7,24 +8,26 @@ .note-header = link_to_member(note.project, note.author, avatar: false) .inline.note-headline-light - = "#{note.author.to_reference} commented" + = "#{note.author.to_reference}" + - if !note.system + commented %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - - if note_editable?(note) - .note-actions - - access = note.project.team.human_max_access(note.author.id) - - if access - %span.note-role - = access + .note-actions + - access = note.project.team.human_max_access(note.author.id) + - if access + %span.note-role + = access + - if note_editable = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = icon('trash-o') - .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} + .note-body{class: note_editable ? 'js-task-list-container' : ''} .note-text = preserve do = markdown(note.note, pipeline: :note, cache_key: [note, "note"]) - - if note_editable?(note) + - if note_editable = render 'projects/notes/edit_form', note: note = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index cc42aab5c52415aceac2987c78bd25a25549c364..1c39ce897a3fb148f06187a5d9f21465f2f3704e 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,6 +1,6 @@ %ul#notes-list.notes.main-notes-list.timeline = render "projects/notes/notes" -%ul.notes.timeline +%ul.notes.notes-form.timeline %li.timeline-entry - if can? current_user, :create_note, @project .timeline-icon.hidden-xs.hidden-sm diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml index cd8a5f0bd023fd5d33d84737aa7f71185f73af4a..0ea8862a684d13b3f021cdd06a3c51b45f236b1b 100644 --- a/app/views/projects/notes/discussions/_active.html.haml +++ b/app/views/projects/notes/discussions/_active.html.haml @@ -6,15 +6,11 @@ = "#{note.author.to_reference} started a discussion" = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do on the diff + = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") .discussion-actions = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-up Show/hide discussion - .last-update.hide.js-toggle-content - - last_note = discussion_notes.last - last updated by - = link_to_member(@project, last_note.author, avatar: false) - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml index 46f2ba4bbcf2f9e828ac59db79b1cbc326ed21e2..2a2ead58eeb5846383702cc16e29f54384df03e5 100644 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ b/app/views/projects/notes/discussions/_commit.html.haml @@ -8,21 +8,18 @@ = "#{note.author.to_reference} started a discussion on #{commit_description}" - if commit = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') + = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") .discussion-actions = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-up Show/hide discussion - .last-update.hide.js-toggle-content - - last_note = discussion_notes.last - last updated by - = link_to_member(@project, last_note.author, avatar: false) - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content - if note.for_diff_line? = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note - else .panel.panel-default .notes{ data: { discussion_id: discussion_notes.first.discussion_id } } - = render discussion_notes + %ul.notes.timeline + = render discussion_notes .discussion-reply-holder = link_to_reply_diff(discussion_notes.first) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index 820e31ccd61b8528cdc4f2388688918572f46bd8..d46aab000c37560c75b28b38eacfa00218d9ae0d 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -20,11 +20,9 @@ %td.new_line.diff-line-num= "..." %td.line_content.match= line.text - else - %td.old_line.diff-line-num - = raw(type == "new" ? " " : line.old_pos) - %td.new_line.diff-line-num - = raw(type == "old" ? " " : line.new_pos) - %td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text) + %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } + %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } + %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) - if line_code == note.line_code = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml index f8e000b424f800f1dc31f2032a217d2f315aeb84..45141bcd1dfd59edf3e41cd94b67fc3e5a036f99 100644 --- a/app/views/projects/notes/discussions/_outdated.html.haml +++ b/app/views/projects/notes/discussions/_outdated.html.haml @@ -5,14 +5,10 @@ .inline.discussion-headline-light = "#{note.author.to_reference} started a discussion" on the outdated diff + = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") .discussion-actions = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-down Show/hide discussion - .last-update.hide.js-toggle-content - - last_note = discussion_notes.last - last updated by - = link_to_member(@project, last_note.author, avatar: false) - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content.hide = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index c4a3f06ee064c100399258b29168598d3875a28e..6f0b32aa165a16310411db197ca826802da168cb 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -9,11 +9,11 @@ %strong #{@tag.name} .prepend-top-default - = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form js-quick-submit' }) do |f| + = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/notes/hints' - .error-alert - .form-actions.prepend-top-default - = f.submit 'Save changes', class: 'btn btn-save' - = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" + .error-alert + .form-actions.prepend-top-default + = f.submit 'Save changes', class: 'btn btn-save' + = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index 6a37f444bb7bef9c73cf8ed118292b2304f34147..9fa4127c9484e6384051f7c04b2df7167ee25b07 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -1,7 +1,10 @@ %h3 Shared runners -.bs-callout.bs-callout-warning - GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X. +.bs-callout.bs-callout-warning.shared-runners-description + - if shared_runners_text.present? + = markdown(shared_runners_text, pipeline: 'plain_markdown') + - else + Shared runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com). %hr - if @project.shared_runners_enabled? = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-warning', method: :post do diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 4310f038fc94e5b083c3713253218325759d8ea7..d854ac217256b8c042e1abda2793f34d8f1455e7 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -36,9 +36,9 @@ %li = link_to 'Changelog', changelog_path(@project) - - if @repository.license + - if @repository.license_blob %li - = link_to 'License', license_path(@project) + = link_to license_short_name(@project), license_path(@project) - if @repository.contribution_guide %li @@ -47,15 +47,15 @@ - if current_user && can_push_branch?(@project, @project.default_branch) - unless @repository.changelog %li.missing - = link_to add_changelog_path(@project) do + = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do Add Changelog - - unless @repository.license + - unless @repository.license_blob %li.missing - = link_to add_license_path(@project) do + = link_to add_special_file_path(@project, file_name: 'LICENSE') do Add License - unless @repository.contribution_guide %li.missing - = link_to add_contribution_guide_path(@project) do + = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do Add Contribution guide - if @repository.commit diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 77c7c4d23de18ee3f730d654326b1e51b35e5259..b40a6e5cb2d3c1c297995c9f47ce84e36264b76f 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -10,7 +10,7 @@ New Tag %hr -= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-quick-submit js-requires-input" do += form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal common-note-form tag-form js-quick-submit js-requires-input" do .form-group = label_tag :tag_name, nil, class: 'control-label' .col-sm-10 @@ -30,9 +30,9 @@ = label_tag :release_description, 'Release notes', class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', attr: :release_description, classes: 'description form-control' + = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/notes/hints' - .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. + .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. .form-actions = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 812876e283562f30afff796211508b2a78871362..797a1a59e9f7e5cab24e5c9b26b5dd72517b65e2 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f| = form_errors(@page) = f.hidden_field :title, value: @page.title @@ -11,7 +11,7 @@ = f.label :content, class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :content, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...' = render 'projects/notes/hints' .clearfix diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..a585147ddd18fd36194b0f52e0f612e460258ee5 --- /dev/null +++ b/app/views/repository_check_mailer/notify.html.haml @@ -0,0 +1,8 @@ +%p + #{@message}. + +%p + = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1) + +%p + You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}. diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml new file mode 100644 index 0000000000000000000000000000000000000000..93db151329efecc297ef3be4c8b1700b3ecccbda --- /dev/null +++ b/app/views/repository_check_mailer/notify.text.haml @@ -0,0 +1,6 @@ +#{@message}. +\ +View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)} + +You are receiving this message because you are a GitLab administrator +for #{Gitlab.config.gitlab.url}. diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index faeb2b55c6f7a6d3791053f858b725c2a8f06983..333f6533213f6bdfcb37187e7c3c6d5cdaf8edbc 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -2,7 +2,7 @@ %h4 = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do %span.term.str-truncated= merge_request.title - .pull-right ##{merge_request.iid} + .pull-right #{merge_request.to_reference} - if merge_request.description.present? .description.term = preserve do diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index 9544e3d3e17ac89858365ba0ca91bf4398ff49c9..d9400b1d9fa63e76c97000c6703c63fb01bca098 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -1,5 +1,5 @@ - project = note.project -- note_url = Gitlab::UrlBuilder.new(:note).build(note.id) +- note_url = Gitlab::UrlBuilder.build(note) - noteable_identifier = note.noteable.try(:iid) || note.noteable.id .search-result-row %h5.note-search-caption.str-truncated diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index b38c5e18efba387eee30a26316c1c6a006c698a6..9ce5562e6673b87cbaf5f15eb3d97ee32c8dbcd5 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -2,4 +2,4 @@ %span.label-name = link_to_label(label, tooltip: false) %span.prepend-left-10 - = markdown(label.description, pipeline: :single_line) + = markdown(label.description, pipeline: :single_line) \ No newline at end of file diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..dc89e36419cd462d539974cf1c4ee155a57dff9c --- /dev/null +++ b/app/views/shared/_labels_row.html.haml @@ -0,0 +1,3 @@ +- labels.each do |label| + %span.label-row + = link_to_label(label, tooltip: false) diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index fc935166bf67379cb59f8de94df0ed00776e753f..4eaf7c2a025aad454ab734b90347b40a4f14fd84 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -62,6 +62,14 @@ %strong Build events %p.light This url will be triggered when a build status changes + - if @service.supported_events.include?("wiki_page") + %div + = form.check_box :wiki_page_events, class: 'pull-left' + .prepend-left-20 + = form.label :wiki_page_events, class: 'list-label' do + %strong Wiki Page events + %p.light + This url will be triggered when a wiki page is created/updated - @service.fields.each do |field| diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index e3a6a5a68b681a84ea62e7113579f72caef58b50..d327bd0a96f8f7966816b3e9c3a311b5a6b09a50 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -20,6 +20,11 @@ = sort_title_milestone_soon = link_to page_filter_path(sort: sort_value_milestone_later) do = sort_title_milestone_later + - if controller.controller_name == 'issues' || controller.action_name == 'issues' + = link_to page_filter_path(sort: sort_value_due_date_soon) do + = sort_title_due_date_soon + = link_to page_filter_path(sort: sort_value_due_date_later) do + = sort_title_due_date_later = link_to page_filter_path(sort: sort_value_upvotes) do = sort_title_upvotes = link_to page_filter_path(sort: sort_value_downvotes) do diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 921eaefd79ad2e683224d86dbf8a2de0c7531f76..fc1101646fb6ea37b019964f2a069cc96580af28 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -23,6 +23,7 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown" + .pull-right = render 'shared/sort_dropdown' @@ -46,9 +47,10 @@ .filter-item.inline = button_tag "Update issues", class: "btn update_selected_issues btn-save" -- if @label - .gray-content-block.second-block - = render "shared/label_row", label: @label + - if !@labels.nil? + .gray-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) } + - if @labels.any? + = render "shared/labels_row", labels: @labels :javascript new UsersSelect(); diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 757a3812deb94588a1aef40237486d8c5f2cef02..18b091df39ba611d3afbcfe85bfb97340942c795 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -4,7 +4,7 @@ = f.label :title, class: 'control-label' .col-sm-10 = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off', - class: 'form-control pad js-gfm-input', required: true + class: 'form-control pad', required: true - if issuable.is_a?(MergeRequest) %p.help-block @@ -29,7 +29,8 @@ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :description, - classes: 'description form-control' + classes: 'note-textarea', + placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .clearfix .error-alert @@ -70,13 +71,14 @@ - if can? current_user, :admin_milestone, issuable.project = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank .form-group + - has_labels = issuable.project.labels.any? = f.label :label_ids, "Labels", class: 'control-label' - .col-sm-10 - - if issuable.project.labels.any? - = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } + .col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) } + - if has_labels + .issuable-form-select-holder + = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, + { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - else - .prepend-top-10 %span.light No labels yet. - if can? current_user, :admin_label, issuable.project @@ -87,9 +89,10 @@ .form-group = label_tag :move_to_project_id, 'Move', class: 'control-label' .col-sm-10 - - projects = project_options(issuable, current_user, ability: :admin_issue) - = select_tag(:move_to_project_id, projects, include_blank: true, - class: 'select2', data: { placeholder: 'Select project' }) + .issuable-form-select-holder + - projects = project_options(issuable, current_user, ability: :admin_issue) + = select_tag(:move_to_project_id, projects, include_blank: true, + class: 'select2', data: { placeholder: 'Select project' }) %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default', title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } @@ -101,13 +104,15 @@ .form-group = f.label :source_branch, class: 'control-label' .col-sm-10 - = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) + .issuable-form-select-holder + = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) .form-group = f.label :target_branch, class: 'control-label' .col-sm-10 - = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) + .issuable-form-select-holder + = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) - if @merge_request.new_record? - %p.help-block + = link_to 'Change branches', mr_change_branches_path(@merge_request) - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) @@ -128,8 +133,6 @@ - else .pull-right - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) - = link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, - method: :delete, class: 'btn btn-grouped' do - = icon('trash-o') - Delete + = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, + method: :delete, class: 'btn btn-danger btn-grouped' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index fd5e58c1f1f7099ca84ee9e27b0fba7aac81f5fe..3eaa45258f0ea591d5a0906553802d44016e9438 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,9 +1,11 @@ - if params[:label_name].present? - = hidden_field_tag(:label_name, params[:label_name]) + - if params[:label_name].respond_to?('any?') + - params[:label_name].each do |label| + = hidden_field_tag "label_name[]", label, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} + %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name[]", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} %span.dropdown-toggle-text - = h(params[:label_name].presence || "Label") + = h(multi_label_name(params[:label_name], "Label")) = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable .dropdown-page-one diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index a6970b7eebbeb115edec69f555717c87fce14f01..1d9b09a5ef1429176bc912d53b24b7579337c644 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -4,22 +4,22 @@ - else - page_context_word = 'issues' %li{class: ("active" if params[:state] == 'opened')} - = link_to page_filter_path(state: 'opened'), title: "Filter by #{page_context_word} that are currently opened." do + = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do #{state_filters_text_for(:opened, @project)} - if defined?(type) && type == :merge_requests %li{class: ("active" if params[:state] == 'merged')} - = link_to page_filter_path(state: 'merged'), title: 'Filter by merge requests that are currently merged.' do + = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do #{state_filters_text_for(:merged, @project)} %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed'), title: 'Filter by merge requests that are currently closed and unmerged.' do + = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do #{state_filters_text_for(:closed, @project)} - else %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed'), title: 'Filter by issues that are currently closed.' do + = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do #{state_filters_text_for(:closed, @project)} %li{class: ("active" if params[:state] == 'all')} - = link_to page_filter_path(state: 'all'), title: "Show all #{page_context_word}." do + = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do #{state_filters_text_for(:all, @project)} diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 94affa4b59a5efb400669743ef56d583986975a7..a50b4b9669340df1b8d844a3c0fcfab4a30f4c96 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -10,14 +10,14 @@ = sidebar_gutter_toggle_icon .issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'} - if prev_issuable = prev_issuable_for(issuable) - = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn' + = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn issuable-pager' - else - %a.btn.btn-default.disabled{href: '#'} + %a.btn.btn-default.issuable-pager.disabled{href: '#'} Prev - if next_issuable = next_issuable_for(issuable) - = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn' + = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn issuable-pager' - else - %a.btn.btn-default.disabled{href: '#'} + %a.btn.btn-default.issuable-pager.disabled{href: '#'} Next = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| @@ -49,7 +49,7 @@ .selectbox.hide-collapsed = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id' - = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }) + = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }) .block.milestone .sidebar-collapsed-icon @@ -58,7 +58,7 @@ - if issuable.milestone = issuable.milestone.title - else - No + None .title.hide-collapsed Milestone = icon('spinner spin', class: 'block-loading') @@ -75,6 +75,34 @@ = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) + - if issuable.has_attribute?(:due_date) + .block.due_date + .sidebar-collapsed-icon + = icon('calendar') + %span.js-due-date-sidebar-value + = issuable.due_date.try(:to_s, :medium) || 'None' + .title.hide-collapsed + Due date + = icon('spinner spin', class: 'block-loading') + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + = link_to 'Edit', '#', class: 'edit-link pull-right' + .value.bold.hide-collapsed + - if issuable.due_date + = issuable.due_date.to_s(:medium) + - else + .light None + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + .selectbox.hide-collapsed + = f.hidden_field :due_date, value: issuable.due_date + .dropdown + %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } } + %span.dropdown-toggle-text Due date + = icon('chevron-down') + .dropdown-menu.dropdown-menu-due-date + = dropdown_title('Due date') + = dropdown_content do + .js-due-date-calendar + - if issuable.project.labels.any? .block.labels .sidebar-collapsed-icon @@ -118,6 +146,7 @@ Manage labels - else View labels + = dropdown_loading = render "shared/issuable/participants", participants: issuable.participants(current_user) - if current_user @@ -128,7 +157,7 @@ .title.hide-collapsed Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - %button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'} + %button.btn.btn-block.btn-gray.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } %span= subscribed ? 'Unsubscribe' : 'Subscribe' .subscription-status.hide-collapsed{data: {status: subscribtion_status}} .unsubscribed{class: ( 'hidden' if subscribed )} @@ -150,6 +179,7 @@ :javascript new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); new LabelsSelect(); - new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}'); + new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); new Subscription('.subscription') - new Sidebar(); \ No newline at end of file + new Sidebar(); + new DueDateSelect(); diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index e1127b2311cec445b4dbd61d76ed55678ce2a0d1..47b66d44e43fcb953fce413d48b45d51bad1a8d9 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -23,5 +23,5 @@ - if assignee = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }), - class: 'has-tooltip', data: { 'original-title' => "Assigned to #{sanitize(assignee.name)}", container: 'body' } do + class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '') diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index 868b2357003e8b14da5701cd5e3dd3b2378ebb61..b15e8ea73fe515b3bcf3ebb1c4ac3a4480a31825 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -4,15 +4,16 @@ %li %span.label-row - = link_to milestones_label_path(options) do - - render_colored_label(label, tooltip: false) - %span.prepend-left-10 + %span.label-name + = link_to milestones_label_path(options) do + - render_colored_label(label, tooltip: false) + %span.prepend-description-left = markdown(label.description, pipeline: :single_line) - .pull-right - %strong.issues-count + .pull-info-right + %span.append-right-20 = link_to milestones_label_path(options.merge(state: 'opened')) do - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' - %strong.issues-count + %span.append-right-20 = link_to milestones_label_path(options.merge(state: 'closed')) do - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue' diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 53ff8959bc8c3ec2213148dc52e1850f92ed0f60..ab8b022411d09c9be926fff78c1cccddb4fd59ee 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -6,9 +6,8 @@ - css_class = '' unless local_assigns[:css_class] - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description -- ci_commit = project.ci_commit(project.commit.sha) if ci && !project.empty_repo? && project.commit - cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3'] -- cache_key.push(ci_commit.status) if ci_commit +- cache_key.push(project.commit.status) if project.commit.try(:status) %li.project-row{ class: css_class } = cache(cache_key) do @@ -16,9 +15,9 @@ - if project.main_language %span = project.main_language - - if ci_commit + - if project.commit.try(:status) %span - = render_ci_status(ci_commit) + = render_ci_status(project.commit) - if forks %span = icon('code-fork') diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 0c4b6a5618b1c4cc9c6337f53a81d3063255692c..3028491e5b6a6f36dd39bb090bec168322e21614 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -6,8 +6,6 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") -= render 'shared/show_aside' - .user-profile .cover-block .cover-controls diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..667fff031dd34f788ddbdc5983c178e63acdcd13 --- /dev/null +++ b/app/workers/admin_email_worker.rb @@ -0,0 +1,12 @@ +class AdminEmailWorker + include Sidekiq::Worker + + sidekiq_options retry: false # this job auto-repeats via sidekiq-cron + + def perform + repository_check_failed_count = Project.where(last_repository_check_failed: true).count + return if repository_check_failed_count.zero? + + RepositoryCheckMailer.notify(repository_check_failed_count).deliver_now + end +end diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 3cc232ef1aeb662b774d1c0849fe222b5b9d0344..f3327ca9e61909f117fee35a63ba4daf8c5b1a5b 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -39,15 +39,15 @@ class PostReceive end if Gitlab::Git.tag_ref?(ref) - GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref) - else + GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute + elsif Gitlab::Git.branch_ref?(ref) GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute end end end private - + def log(message) Gitlab::GitLogger.error("POST-RECEIVE: #{message}") end diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..44b3145d50f3246c8b6db523fbb4ec494f45c79c --- /dev/null +++ b/app/workers/repository_check/batch_worker.rb @@ -0,0 +1,63 @@ +module RepositoryCheck + class BatchWorker + include Sidekiq::Worker + + RUN_TIME = 3600 + + sidekiq_options retry: false + + def perform + start = Time.now + + # This loop will break after a little more than one hour ('a little + # more' because `git fsck` may take a few minutes), or if it runs out of + # projects to check. By default sidekiq-cron will start a new + # RepositoryCheckWorker each hour so that as long as there are repositories to + # check, only one (or two) will be checked at a time. + project_ids.each do |project_id| + break if Time.now - start >= RUN_TIME + break unless current_settings.repository_checks_enabled + + next unless try_obtain_lease(project_id) + + SingleRepositoryWorker.new.perform(project_id) + end + end + + private + + # Project.find_each does not support WHERE clauses and + # Project.find_in_batches does not support ordering. So we just build an + # array of ID's. This is OK because we do it only once an hour, because + # getting ID's from Postgres is not terribly slow, and because no user + # has to sit and wait for this query to finish. + def project_ids + limit = 10_000 + never_checked_projects = Project.where('last_repository_check_at IS NULL').limit(limit). + pluck(:id) + old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago). + reorder('last_repository_check_at ASC').limit(limit).pluck(:id) + never_checked_projects + old_check_projects + end + + def try_obtain_lease(id) + # Use a 24-hour timeout because on servers/projects where 'git fsck' is + # super slow we definitely do not want to run it twice in parallel. + Gitlab::ExclusiveLease.new( + "project_repository_check:#{id}", + timeout: 24.hours + ).try_obtain + end + + def current_settings + # No caching of the settings! If we cache them and an admin disables + # this feature, an active RepositoryCheckWorker would keep going for up + # to 1 hour after the feature was disabled. + if Rails.env.test? + Gitlab::CurrentSettings.fake_application_settings + else + ApplicationSetting.current + end + end + end +end diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..b7202ddff34473cdeb24ca753b639718da8c3540 --- /dev/null +++ b/app/workers/repository_check/clear_worker.rb @@ -0,0 +1,17 @@ +module RepositoryCheck + class ClearWorker + include Sidekiq::Worker + + sidekiq_options retry: false + + def perform + # Do small batched updates because these updates will be slow and locking + Project.select(:id).find_in_batches(batch_size: 100) do |batch| + Project.where(id: batch.map(&:id)).update_all( + last_repository_check_failed: nil, + last_repository_check_at: nil, + ) + end + end + end +end diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..a76729e3c74ead9b2a8e5fbc434772876297b602 --- /dev/null +++ b/app/workers/repository_check/single_repository_worker.rb @@ -0,0 +1,36 @@ +module RepositoryCheck + class SingleRepositoryWorker + include Sidekiq::Worker + + sidekiq_options retry: false + + def perform(project_id) + project = Project.find(project_id) + project.update_columns( + last_repository_check_failed: !check(project), + last_repository_check_at: Time.now, + ) + end + + private + + def check(project) + repositories = [project.repository] + repositories << project.wiki.repository if project.wiki_enabled? + # Use 'map do', not 'all? do', to prevent short-circuiting + repositories.map { |repository| git_fsck(repository.path_to_repo) }.all? + end + + def git_fsck(path) + cmd = %W(nice git --git-dir=#{path} fsck) + output, status = Gitlab::Popen.popen(cmd) + + if status.zero? + true + else + Gitlab::RepositoryCheckLogger.error("command failed: #{cmd.join(' ')}\n#{output}") + false + end + end + end +end diff --git a/bin/setup b/bin/setup index acdb2c1389c502f79a967384cac1c5adcea10ec2..6cb2d7f1e3a52f1621dd62b6b2790878b4984aa0 100755 --- a/bin/setup +++ b/bin/setup @@ -18,7 +18,7 @@ Dir.chdir APP_ROOT do # end puts "\n== Preparing database ==" - system "bin/rake db:setup" + system "bin/rake db:reset" puts "\n== Removing old logs and tempfiles ==" system "rm -f log/*" diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 35c7c425a5a75999bae5fd14c28cea605337c9fa..d9c15f814048b0bd2996944f858d3c4718710428 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -46,6 +46,15 @@ production: &base # # relative_url_root: /gitlab + # Trusted Proxies + # Customize if you have GitLab behind a reverse proxy which is running on a different machine. + # Add the IP address for your reverse proxy to the list, otherwise users will appear signed in from that address. + trusted_proxies: + # Examples: + #- 192.168.1.0/24 + #- 192.168.2.1 + #- 2001:0db8::/32 + # Uncomment and customize if you can't use the default user to run GitLab (default: 'git') # user: git @@ -155,7 +164,17 @@ production: &base # Flag stuck CI builds as failed stuck_ci_builds_worker: cron: "0 0 * * *" + # Periodically run 'git fsck' on all repositories. If started more than + # once per hour you will have concurrent 'git fsck' jobs. + repository_check_worker: + cron: "20 * * * *" + # Send admin emails once a day + admin_email_worker: + cron: "0 0 * * *" + # Remove outdated repository archives + repository_archive_cache_worker: + cron: "0 * * * *" # # 2. GitLab CI settings @@ -304,6 +323,13 @@ production: &base # (default: false) auto_link_saml_user: false + # Set different Omniauth providers as external so that all users creating accounts + # via these providers will not be able to have access to internal projects. You + # will need to use the full name of the provider, like `google_oauth2` for Google. + # Refer to the examples below for the full names of the supported providers. + # (default: []) + external_providers: [] + ## Auth providers # Uncomment the following lines and fill in the data of the auth provider you want to use # If your favorite auth provider is not listed you can use others: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 72c4d8d61ced7f7a9ee85d6f1d7481133334ada8..10c25044b75029cb9ddbd99c3564f867aebd172c 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -129,6 +129,7 @@ Settings['omniauth'] ||= Settingslogic.new({}) Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil? Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil? +Settings.omniauth['external_providers'] = [] if Settings.omniauth['external_providers'].nil? Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil? Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil? @@ -190,6 +191,7 @@ Settings.gitlab.default_projects_features['visibility_level'] = Settings.send Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] +Settings.gitlab['trusted_proxies'] ||= [] # @@ -239,7 +241,15 @@ Settings['cron_jobs'] ||= Settingslogic.new({}) Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *' Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker' - +Settings.cron_jobs['repository_check_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['repository_check_worker']['cron'] ||= '20 * * * *' +Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheck::BatchWorker' +Settings.cron_jobs['admin_email_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['admin_email_worker']['cron'] ||= '0 0 * * *' +Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker' +Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *' +Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'RepositoryArchiveCacheWorker' # # GitLab Shell diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 70285255877b6399bae7b52259023d6ece08aca3..88cb859871c31bff0c3b7519b1cf9b011046467c 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -14,7 +14,7 @@ if Rails.env.test? Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" else redis_config = Gitlab::Redis.redis_store_options - redis_config[:namespace] = 'session:gitlab' + redis_config[:namespace] = Gitlab::Redis::SESSION_NAMESPACE Gitlab::Application.config.session_store( :redis_store, # Using the cookie_store would enable session replay attacks. diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 9182d9298097dca390dda7d8e4240afb498fa23a..f1eec674888e2f3f70ea84d9251403e4f9de4f96 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,9 +1,7 @@ -SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab' - Sidekiq.configure_server do |config| config.redis = { url: Gitlab::Redis.url, - namespace: SIDEKIQ_REDIS_NAMESPACE + namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE } config.server_middleware do |chain| @@ -30,6 +28,6 @@ end Sidekiq.configure_client do |config| config.redis = { url: Gitlab::Redis.url, - namespace: SIDEKIQ_REDIS_NAMESPACE + namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE } end diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb new file mode 100644 index 0000000000000000000000000000000000000000..b8cc025bae2048b4bc85ceac0c284f10b18efae8 --- /dev/null +++ b/config/initializers/trusted_proxies.rb @@ -0,0 +1,2 @@ +Rails.application.config.action_dispatch.trusted_proxies = + [ '127.0.0.1', '::1' ] + Array(Gitlab.config.gitlab.trusted_proxies) diff --git a/config/routes.rb b/config/routes.rb index 21164ba0edaa689f13114c1c55ddec43eddd77a9..a18190d8bcbf521d3edb64d43dfe769c3e2655f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -264,6 +264,7 @@ Rails.application.routes.draw do member do put :transfer + post :repository_check end resources :runner_projects @@ -281,6 +282,7 @@ Rails.application.routes.draw do resource :application_settings, only: [:show, :update] do resources :services put :reset_runners_token + put :clear_repository_check_states end resources :labels @@ -326,7 +328,7 @@ Rails.application.routes.draw do end end resource :preferences, only: [:show, :update] - resources :keys, except: [:new] + resources :keys resources :emails, only: [:index, :create, :destroy] resource :avatar, only: [:destroy] resource :two_factor_auth, only: [:new, :create, :destroy] do @@ -406,6 +408,7 @@ Rails.application.routes.draw do resource :avatar, only: [:destroy] resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] + resource :notification_setting, only: [:update] end end @@ -548,6 +551,7 @@ Rails.application.routes.draw do post :cancel_builds post :retry_builds post :revert + post :cherry_pick end end @@ -609,6 +613,7 @@ Rails.application.routes.draw do resources :forks, only: [:index, :new, :create] resource :import, only: [:new, :create, :show] + resource :notification_setting, only: [:update] resources :refs, only: [] do collection do @@ -666,6 +671,7 @@ Rails.application.routes.draw do post :cancel post :retry post :erase + get :raw end resource :artifacts, only: [] do @@ -701,6 +707,8 @@ Rails.application.routes.draw do resources :issues, constraints: { id: /\d+/ } do member do post :toggle_subscription + get :referenced_merge_requests + get :related_branches end collection do post :bulk_update diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb index e3ca2b4eea359e4dab0c97250e270c95ae939fc6..b99d24a03c91c357d5d765c0e364c4a1747d6251 100644 --- a/db/fixtures/development/14_builds.rb +++ b/db/fixtures/development/14_builds.rb @@ -19,7 +19,7 @@ class Gitlab::Seeder::Builds commits = @project.repository.commits('master', nil, 5) commits_sha = commits.map { |commit| commit.raw.id } commits_sha.map do |sha| - @project.ensure_ci_commit(sha) + @project.ensure_ci_commit(sha, 'master') end rescue [] diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb index eed6d366814c5e956a0ee7fe7d5bd8bc974ff6d3..efdf53112fd491f4350328705ade33cb36a4417d 100644 --- a/db/migrate/20140502125220_migrate_repo_size.rb +++ b/db/migrate/20140502125220_migrate_repo_size.rb @@ -1,19 +1,26 @@ class MigrateRepoSize < ActiveRecord::Migration def up - Project.reset_column_information - Project.find_each(batch_size: 500) do |project| + project_data = execute('SELECT projects.id, namespaces.path AS namespace_path, projects.path AS project_path FROM projects LEFT JOIN namespaces ON projects.namespace_id = namespaces.id') + + project_data.each do |project| + id = project['id'] + namespace_path = project['namespace_path'] || '' + path = File.join(Gitlab.config.gitlab_shell.repos_path, namespace_path, project['project_path'] + '.git') + begin - if project.empty_repo? + repo = Gitlab::Git::Repository.new(path) + if repo.empty? print '-' else - project.update_repository_size + size = repo.size print '.' + execute("UPDATE projects SET repository_size = #{size} WHERE id = #{id}") end - rescue - print 'F' + rescue => e + puts "\nFailed to update project #{id}: #{e}" end end - puts 'Done' + puts "\nDone" end def down diff --git a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb index 003169c13c6e2d2a94eee810cf4c56aa049b6e7e..d7b00e3d6ed4df6791753c2bdded5f34c5f18a47 100644 --- a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb +++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb @@ -4,6 +4,8 @@ class AddTrigramIndexesForSearching < ActiveRecord::Migration def up return unless Gitlab::Database.postgresql? + create_trigrams_extension + unless trigrams_enabled? raise 'You must enable the pg_trgm extension. You can do so by running ' \ '"CREATE EXTENSION pg_trgm;" as a PostgreSQL super user, this must be ' \ @@ -37,6 +39,15 @@ class AddTrigramIndexesForSearching < ActiveRecord::Migration row && row['enabled'] == 't' ? true : false end + def create_trigrams_extension + # This may not work if the user doesn't have permission. We attempt in + # case we do have permission, particularly for test/dev environments. + begin + enable_extension 'pg_trgm' + rescue + end + end + def to_index { ci_runners: [:token, :description], diff --git a/db/migrate/20160227120001_add_event_field_for_web_hook.rb b/db/migrate/20160227120001_add_event_field_for_web_hook.rb new file mode 100644 index 0000000000000000000000000000000000000000..65f2a47bb3c451427dafeed6e5dcdc9303ccfeb2 --- /dev/null +++ b/db/migrate/20160227120001_add_event_field_for_web_hook.rb @@ -0,0 +1,5 @@ +class AddEventFieldForWebHook < ActiveRecord::Migration + def change + add_column :web_hooks, :wiki_page_events, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20160227120047_add_event_to_services.rb b/db/migrate/20160227120047_add_event_to_services.rb new file mode 100644 index 0000000000000000000000000000000000000000..f5040d770de905a04057b537ed75e29b83cfb7e1 --- /dev/null +++ b/db/migrate/20160227120047_add_event_to_services.rb @@ -0,0 +1,5 @@ +class AddEventToServices < ActiveRecord::Migration + def change + add_column :services, :wiki_page_events, :boolean, default: true + end +end diff --git a/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb b/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb new file mode 100644 index 0000000000000000000000000000000000000000..ffcd64266e3801b4676c67eab19ca98e3bf4cf05 --- /dev/null +++ b/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb @@ -0,0 +1,7 @@ +class AddImportCredentialsToProjectImportData < ActiveRecord::Migration + def change + add_column :project_import_data, :encrypted_credentials, :text + add_column :project_import_data, :encrypted_credentials_iv, :string + add_column :project_import_data, :encrypted_credentials_salt, :string + end +end diff --git a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb new file mode 100644 index 0000000000000000000000000000000000000000..8a351cf27a399193f73864ebe3473d65a0a46f18 --- /dev/null +++ b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb @@ -0,0 +1,131 @@ +# Loops through old importer projects that kept a token/password in the import URL +# and encrypts the credentials into a separate field in project#import_data +# #down method not supported +class RemoveWrongImportUrlFromProjects < ActiveRecord::Migration + + class ProjectImportDataFake + extend AttrEncrypted + attr_accessor :credentials + attr_encrypted :credentials, key: Gitlab::Application.secrets.db_key_base, marshal: true, encode: true, :mode => :per_attribute_iv_and_salt + end + + def up + say("Encrypting and migrating project import credentials...") + + # This should cover GitHub, GitLab, Bitbucket user:password, token@domain, and other similar URLs. + in_transaction(message: "Projects including GitHub and GitLab projects with an unsecured URL.") { process_projects_with_wrong_url } + + in_transaction(message: "Migrating Bitbucket credentials...") { process_project(import_type: 'bitbucket', credentials_keys: ['bb_session']) } + + in_transaction(message: "Migrating FogBugz credentials...") { process_project(import_type: 'fogbugz', credentials_keys: ['fb_session']) } + + end + + def process_projects_with_wrong_url + projects_with_wrong_import_url.each do |project| + begin + import_url = Gitlab::ImportUrl.new(project["import_url"]) + + update_import_url(import_url, project) + update_import_data(import_url, project) + rescue URI::InvalidURIError + nullify_import_url(project) + end + end + end + + def process_project(import_type:, credentials_keys: []) + unencrypted_import_data(import_type: import_type).each do |data| + replace_data_credentials(data, credentials_keys) + end + end + + def replace_data_credentials(data, credentials_keys) + data_hash = JSON.load(data['data']) if data['data'] + unless data_hash.blank? + encrypted_data_hash = encrypt_data(data_hash, credentials_keys) + unencrypted_data = data_hash.empty? ? ' NULL ' : quote(data_hash.to_json) + update_with_encrypted_data(encrypted_data_hash, data['id'], unencrypted_data) + end + end + + def encrypt_data(data_hash, credentials_keys) + new_data_hash = {} + credentials_keys.each do |key| + new_data_hash[key.to_sym] = data_hash.delete(key) if data_hash[key] + end + new_data_hash.deep_symbolize_keys + end + + def in_transaction(message:) + say_with_time(message) do + ActiveRecord::Base.transaction do + yield + end + end + end + + def update_import_data(import_url, project) + fake_import_data = ProjectImportDataFake.new + fake_import_data.credentials = import_url.credentials + import_data_id = project['import_data_id'] + if import_data_id + execute(update_import_data_sql(import_data_id, fake_import_data)) + else + execute(insert_import_data_sql(project['id'], fake_import_data)) + end + end + + def update_with_encrypted_data(data_hash, import_data_id, unencrypted_data = ' NULL ') + fake_import_data = ProjectImportDataFake.new + fake_import_data.credentials = data_hash + execute(update_import_data_sql(import_data_id, fake_import_data, unencrypted_data)) + end + + def update_import_url(import_url, project) + execute("UPDATE projects SET import_url = #{quote(import_url.sanitized_url)} WHERE id = #{project['id']}") + end + + def nullify_import_url(project) + execute("UPDATE projects SET import_url = NULL WHERE id = #{project['id']}") + end + + def insert_import_data_sql(project_id, fake_import_data) + %( + INSERT INTO project_import_data + (encrypted_credentials, + project_id, + encrypted_credentials_iv, + encrypted_credentials_salt) + VALUES ( #{quote(fake_import_data.encrypted_credentials)}, + '#{project_id}', + #{quote(fake_import_data.encrypted_credentials_iv)}, + #{quote(fake_import_data.encrypted_credentials_salt)}) + ).squish + end + + def update_import_data_sql(id, fake_import_data, data = 'NULL') + %( + UPDATE project_import_data + SET encrypted_credentials = #{quote(fake_import_data.encrypted_credentials)}, + encrypted_credentials_iv = #{quote(fake_import_data.encrypted_credentials_iv)}, + encrypted_credentials_salt = #{quote(fake_import_data.encrypted_credentials_salt)}, + data = #{data} + WHERE id = '#{id}' + ).squish + end + + #GitHub projects with token, and any user:password@ based URL + def projects_with_wrong_import_url + select_all("SELECT p.id, p.import_url, i.id as import_data_id FROM projects p LEFT JOIN project_import_data i on p.id = i.project_id WHERE p.import_url <> '' AND p.import_url LIKE '%//%@%'") + end + + # All imports with data for import_type + def unencrypted_import_data(import_type: ) + select_all("SELECT i.id, p.import_url, i.data FROM projects p INNER JOIN project_import_data i ON p.id = i.project_id WHERE p.import_url <> '' AND p.import_type = '#{import_type}' ") + end + + def quote(value) + ActiveRecord::Base.connection.quote(value) + end +end diff --git a/db/migrate/20160310124959_add_due_date_to_issues.rb b/db/migrate/20160310124959_add_due_date_to_issues.rb new file mode 100644 index 0000000000000000000000000000000000000000..ec08bd9fdfacb293f6dce607538218bbd3273a66 --- /dev/null +++ b/db/migrate/20160310124959_add_due_date_to_issues.rb @@ -0,0 +1,6 @@ +class AddDueDateToIssues < ActiveRecord::Migration + def change + add_column :issues, :due_date, :date + add_index :issues, :due_date + end +end diff --git a/db/migrate/20160315135439_project_add_repository_check.rb b/db/migrate/20160315135439_project_add_repository_check.rb new file mode 100644 index 0000000000000000000000000000000000000000..8687d5d62965bc8def4a7ab1874cbfb1ad35ae5f --- /dev/null +++ b/db/migrate/20160315135439_project_add_repository_check.rb @@ -0,0 +1,8 @@ +class ProjectAddRepositoryCheck < ActiveRecord::Migration + def change + add_column :projects, :last_repository_check_failed, :boolean + add_index :projects, :last_repository_check_failed + + add_column :projects, :last_repository_check_at, :datetime + end +end diff --git a/db/migrate/20160328112808_create_notification_settings.rb b/db/migrate/20160328112808_create_notification_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..4755da8b8066a3d498651e617fd4301c4febcad5 --- /dev/null +++ b/db/migrate/20160328112808_create_notification_settings.rb @@ -0,0 +1,11 @@ +class CreateNotificationSettings < ActiveRecord::Migration + def change + create_table :notification_settings do |t| + t.references :user, null: false + t.references :source, polymorphic: true, null: false + t.integer :level, default: 0, null: false + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160328115649_migrate_new_notification_setting.rb b/db/migrate/20160328115649_migrate_new_notification_setting.rb new file mode 100644 index 0000000000000000000000000000000000000000..3c81b2c37bfe558724b4663f3fdbdf585874cc3f --- /dev/null +++ b/db/migrate/20160328115649_migrate_new_notification_setting.rb @@ -0,0 +1,17 @@ +# This migration will create one row of NotificationSetting for each Member row +# It can take long time on big instances. +# +# This migration can be done online but with following effects: +# - during migration some users will receive notifications based on their global settings (project/group settings will be ignored) +# - its possible to get duplicate records for notification settings since we don't create uniq index yet +# +class MigrateNewNotificationSetting < ActiveRecord::Migration + def up + timestamp = Time.now.strftime('%F %T') + execute "INSERT INTO notification_settings ( user_id, source_id, source_type, level, created_at, updated_at ) SELECT user_id, source_id, source_type, notification_level, '#{timestamp}', '#{timestamp}' FROM members WHERE user_id IS NOT NULL" + end + + def down + execute "DELETE FROM notification_settings" + end +end diff --git a/db/migrate/20160328121138_add_notification_setting_index.rb b/db/migrate/20160328121138_add_notification_setting_index.rb new file mode 100644 index 0000000000000000000000000000000000000000..8aebce0244d68273de80ce36e6333b710628095f --- /dev/null +++ b/db/migrate/20160328121138_add_notification_setting_index.rb @@ -0,0 +1,6 @@ +class AddNotificationSettingIndex < ActiveRecord::Migration + def change + add_index :notification_settings, :user_id + add_index :notification_settings, [:source_id, :source_type] + end +end diff --git a/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb b/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb new file mode 100644 index 0000000000000000000000000000000000000000..ebfa4bcbc7b9dc91afd541303e476335094a4972 --- /dev/null +++ b/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb @@ -0,0 +1,5 @@ +class AddRepositoryChecksEnabledSetting < ActiveRecord::Migration + def change + add_column :application_settings, :repository_checks_enabled, :boolean, default: true + end +end diff --git a/db/migrate/20160412173416_add_fields_to_ci_commit.rb b/db/migrate/20160412173416_add_fields_to_ci_commit.rb new file mode 100644 index 0000000000000000000000000000000000000000..125956a3ddd9c784b99b93aaf6e7365b4247e68e --- /dev/null +++ b/db/migrate/20160412173416_add_fields_to_ci_commit.rb @@ -0,0 +1,8 @@ +class AddFieldsToCiCommit < ActiveRecord::Migration + def change + add_column :ci_commits, :status, :string + add_column :ci_commits, :started_at, :timestamp + add_column :ci_commits, :finished_at, :timestamp + add_column :ci_commits, :duration, :integer + end +end diff --git a/db/migrate/20160412173417_update_ci_commit.rb b/db/migrate/20160412173417_update_ci_commit.rb new file mode 100644 index 0000000000000000000000000000000000000000..fd92444dbacff1c96adfdf2d310c3b864a507bc3 --- /dev/null +++ b/db/migrate/20160412173417_update_ci_commit.rb @@ -0,0 +1,35 @@ +class UpdateCiCommit < ActiveRecord::Migration + # This migration can be run online, but needs to be executed for the second time after restarting Unicorn workers + # Otherwise Offline migration should be used. + def change + execute("UPDATE ci_commits SET status=#{status}, ref=#{ref}, tag=#{tag} WHERE status IS NULL") + end + + private + + def status + builds = '(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id)' + success = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND status='success')" + ignored = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND (status='failed' OR status='canceled') AND allow_failure)" + pending = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND status='pending')" + running = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND status='running')" + canceled = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND status='canceled')" + + "(CASE + WHEN #{builds}=0 THEN 'skipped' + WHEN #{builds}=#{success}+#{ignored} THEN 'success' + WHEN #{builds}=#{pending} THEN 'pending' + WHEN #{builds}=#{canceled} THEN 'canceled' + WHEN #{running}+#{pending}>0 THEN 'running' + ELSE 'failed' + END)" + end + + def ref + '(SELECT ref FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id ORDER BY id DESC LIMIT 1)' + end + + def tag + '(SELECT tag FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id ORDER BY id DESC LIMIT 1)' + end +end diff --git a/db/migrate/20160412173418_add_ci_commit_indexes.rb b/db/migrate/20160412173418_add_ci_commit_indexes.rb new file mode 100644 index 0000000000000000000000000000000000000000..603d4a4161088ca631c7481aa053867cba40fba1 --- /dev/null +++ b/db/migrate/20160412173418_add_ci_commit_indexes.rb @@ -0,0 +1,19 @@ +class AddCiCommitIndexes < ActiveRecord::Migration + disable_ddl_transaction! + + def change + add_index :ci_commits, [:gl_project_id, :sha], index_options + add_index :ci_commits, [:gl_project_id, :status], index_options + add_index :ci_commits, [:status], index_options + end + + private + + def index_options + if Gitlab::Database.postgresql? + { algorithm: :concurrently } + else + { } + end + end +end diff --git a/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb b/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..d493044c67b83148e561688cd8c9716c257decb3 --- /dev/null +++ b/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddSharedRunnersTextToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :shared_runners_text, :text + end +end diff --git a/db/migrate/20160419120017_add_metrics_packet_size.rb b/db/migrate/20160419120017_add_metrics_packet_size.rb new file mode 100644 index 0000000000000000000000000000000000000000..78c163d62acf8782c3d7294b6858e174d5921392 --- /dev/null +++ b/db/migrate/20160419120017_add_metrics_packet_size.rb @@ -0,0 +1,5 @@ +class AddMetricsPacketSize < ActiveRecord::Migration + def change + add_column :application_settings, :metrics_packet_size, :integer, default: 1 + end +end diff --git a/db/schema.rb b/db/schema.rb index ec235c19131645d54a26a40d451599b2ace8130a..a743e6824235c7d94e8e34e19b912983969115cf 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: 20160331223143) do +ActiveRecord::Schema.define(version: 20160419120017) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -77,6 +77,9 @@ ActiveRecord::Schema.define(version: 20160331223143) do t.string "akismet_api_key" t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" + t.boolean "repository_checks_enabled", default: true + t.integer "metrics_packet_size", default: 1 + t.text "shared_runners_text" end create_table "audit_events", force: :cascade do |t| @@ -168,14 +171,21 @@ ActiveRecord::Schema.define(version: 20160331223143) do t.text "yaml_errors" t.datetime "committed_at" t.integer "gl_project_id" + t.string "status" + t.datetime "started_at" + t.datetime "finished_at" + t.integer "duration" end + add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree + add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree + add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree create_table "ci_events", force: :cascade do |t| t.integer "project_id" @@ -419,6 +429,7 @@ ActiveRecord::Schema.define(version: 20160331223143) do t.integer "moved_to_id" t.boolean "confidential", default: false t.datetime "deleted_at" + t.date "due_date" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -428,6 +439,7 @@ ActiveRecord::Schema.define(version: 20160331223143) do add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} + add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree @@ -637,6 +649,18 @@ ActiveRecord::Schema.define(version: 20160331223143) do add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree + create_table "notification_settings", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "source_id", null: false + t.string "source_type", null: false + t.integer "level", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree + add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree + create_table "oauth_access_grants", force: :cascade do |t| t.integer "resource_owner_id", null: false t.integer "application_id", null: false @@ -691,6 +715,9 @@ ActiveRecord::Schema.define(version: 20160331223143) do create_table "project_import_data", force: :cascade do |t| t.integer "project_id" t.text "data" + t.text "encrypted_credentials" + t.text "encrypted_credentials_iv" + t.text "encrypted_credentials_salt" end create_table "projects", force: :cascade do |t| @@ -700,37 +727,39 @@ ActiveRecord::Schema.define(version: 20160331223143) do t.datetime "created_at" t.datetime "updated_at" t.integer "creator_id" - t.boolean "issues_enabled", default: true, null: false - t.boolean "wall_enabled", default: true, null: false - t.boolean "merge_requests_enabled", default: true, null: false - t.boolean "wiki_enabled", default: true, null: false + t.boolean "issues_enabled", default: true, null: false + t.boolean "wall_enabled", default: true, null: false + t.boolean "merge_requests_enabled", default: true, null: false + t.boolean "wiki_enabled", default: true, null: false t.integer "namespace_id" - t.string "issues_tracker", default: "gitlab", null: false + t.string "issues_tracker", default: "gitlab", null: false t.string "issues_tracker_id" - t.boolean "snippets_enabled", default: true, null: false + t.boolean "snippets_enabled", default: true, null: false t.datetime "last_activity_at" t.string "import_url" - t.integer "visibility_level", default: 0, null: false - t.boolean "archived", default: false, null: false + t.integer "visibility_level", default: 0, null: false + t.boolean "archived", default: false, null: false t.string "avatar" t.string "import_status" - t.float "repository_size", default: 0.0 - t.integer "star_count", default: 0, null: false + t.float "repository_size", default: 0.0 + t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" - t.integer "commit_count", default: 0 + t.integer "commit_count", default: 0 t.text "import_error" t.integer "ci_id" - t.boolean "builds_enabled", default: true, null: false - t.boolean "shared_runners_enabled", default: true, null: false + t.boolean "builds_enabled", default: true, null: false + t.boolean "shared_runners_enabled", default: true, null: false t.string "runners_token" t.string "build_coverage_regex" - t.boolean "build_allow_git_fetch", default: true, null: false - t.integer "build_timeout", default: 3600, null: false - t.boolean "pending_delete", default: false - t.boolean "public_builds", default: true, null: false + t.boolean "build_allow_git_fetch", default: true, null: false + t.integer "build_timeout", default: 3600, null: false + t.boolean "pending_delete", default: false + t.boolean "public_builds", default: true, null: false t.string "main_language" - t.integer "pushes_since_gc", default: 0 + t.integer "pushes_since_gc", default: 0 + t.boolean "last_repository_check_failed" + t.datetime "last_repository_check_at" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree @@ -740,6 +769,7 @@ ActiveRecord::Schema.define(version: 20160331223143) do add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree + add_index "projects", ["last_repository_check_failed"], name: "index_projects_on_last_repository_check_failed", using: :btree add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["path"], name: "index_projects_on_path", using: :btree @@ -799,6 +829,7 @@ ActiveRecord::Schema.define(version: 20160331223143) do t.boolean "build_events", default: false, null: false t.string "category", default: "common", null: false t.boolean "default", default: false + t.boolean "wiki_page_events", default: true end add_index "services", ["category"], name: "index_services_on_category", using: :btree @@ -993,6 +1024,7 @@ ActiveRecord::Schema.define(version: 20160331223143) do t.boolean "note_events", default: false, null: false t.boolean "enable_ssl_verification", default: true t.boolean "build_events", default: false, null: false + t.boolean "wiki_page_events", default: false, null: false end add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree diff --git a/doc/README.md b/doc/README.md index 724c7cca0f17454cf560f17a438396e05ea4f940..e6ac47948278380637e6e18bf965fc1029a86cc9 100644 --- a/doc/README.md +++ b/doc/README.md @@ -3,7 +3,7 @@ ## User documentation - [API](api/README.md) Automate GitLab via a simple and powerful API. -- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, .gitlab-ci.yml options, and examples. +- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, `.gitlab-ci.yml` options, and examples. - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. - [Importing to GitLab](workflow/importing/README.md). @@ -31,6 +31,7 @@ - [Environment Variables](administration/environment_variables.md) to configure GitLab. - [Operations](operations/README.md) Keeping GitLab up and running - [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects. +- [Repository checks](administration/repository_checks.md) Periodic Git repository checks - [Security](security/README.md) Learn what you can do to further secure your GitLab instance. - [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. - [Update](update/README.md) Update guides to upgrade your installation. diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md new file mode 100644 index 0000000000000000000000000000000000000000..61bf8ce61619e4ad9542af5d10a40fbf8f8576ba --- /dev/null +++ b/doc/administration/repository_checks.md @@ -0,0 +1,43 @@ +# Repository checks + +>**Note:** +This feature was [introduced][ce-3232] in GitLab 8.7. + +Git has a built-in mechanism, [git fsck][git-fsck], to verify the +integrity of all data commited to a repository. GitLab administrators +can trigger such a check for a project via the project page under the +admin panel. The checks run asynchronously so it may take a few minutes +before the check result is visible on the project admin page. If the +checks failed you can see their output on the admin log page under +'repocheck.log'. + +## Periodic checks + +GitLab periodically runs a repository check on all project repositories and +wiki repositories in order to detect data corruption problems. A +project will be checked no more than once per week. If any projects +fail their repository checks all GitLab administrators will receive an email +notification of the situation. This notification is sent out no more +than once a day. + +## Disabling periodic checks + +You can disable the periodic checks on the 'Settings' page of the admin +panel. + +## What to do if a check failed + +If the repository check fails for some repository you should look up the error +in repocheck.log (in the admin panel or on disk; see +`/var/log/gitlab/gitlab-rails` for Omnibus installations or +`/home/git/gitlab/log` for installations from source). Once you have +resolved the issue use the admin panel to trigger a new repository check on +the project. This will clear the 'check failed' state. + +If for some reason the periodic repository check caused a lot of false +alarms you can choose to clear ALL repository check states from the +'Settings' page of the admin panel. + +--- +[ce-3232]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3232 "Auto git fsck" +[git-fsck]: https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html "git fsck documentation" \ No newline at end of file diff --git a/doc/api/README.md b/doc/api/README.md index 7629ef294ac9897d1f56b2c6acb311b8f621ef7c..ff039f1886f161a2a1a97ba2b7fa6670ed6a465c 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -33,6 +33,7 @@ following locations: - [Build triggers](build_triggers.md) - [Build Variables](build_variables.md) - [Runners](runners.md) +- [Licenses](licenses.md) ## Authentication @@ -108,6 +109,7 @@ The following table shows the possible return codes for API requests. | ------------- | ----------- | | `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. | | `201 Created` | The `POST` request was successful and the resource is returned as JSON. | +| `304 Not Modified` | Indicates that the resource has not been modified since the last request. | | `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. | | `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. | | `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. | diff --git a/doc/api/groups.md b/doc/api/groups.md index d1b5c9f5f048f7de844f817335123291b2da97e2..2821bc21b81e5884f0996ecd5b01a6595aa83ba8 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -126,6 +126,87 @@ Parameters: - `id` (required) - The ID or path of a group - `project_id` (required) - The ID of a project +## Update group + +Updates the project group. Only available to group owners and administrators. + +``` +PUT /groups/:id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the group | +| `name` | string | no | The name of the group | +| `path` | string | no | The path of the group | +| `description` | string | no | The description of the group | +| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. | + +```bash +curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" + +``` + +Example response: + +```json +{ + "id": 5, + "name": "Experimental", + "path": "h5bp", + "description": "foo", + "visibility_level": 10, + "avatar_url": null, + "web_url": "http://gitlab.example.com/groups/h5bp", + "projects": [ + { + "id": 9, + "description": "foo", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 10, + "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git", + "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git", + "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate", + "name": "Html5 Boilerplate", + "name_with_namespace": "Experimental / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": true, + "created_at": "2016-04-05T21:40:50.169Z", + "last_activity_at": "2016-04-06T16:52:08.432Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "Experimental", + "path": "h5bp", + "owner_id": null, + "created_at": "2016-04-05T21:40:49.152Z", + "updated_at": "2016-04-07T08:07:48.466Z", + "description": "foo", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 10 + }, + "avatar_url": null, + "star_count": 1, + "forks_count": 0, + "open_issues_count": 3, + "public_builds": true + } + ] +} +``` + ## Remove group Removes group with all projects inside. diff --git a/doc/api/issues.md b/doc/api/issues.md index 1c635a6cdcf82f49f587950a3a820eafce5117a9..3e78149f442f9d16cc72d15427028e4f7c19e96b 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -298,6 +298,7 @@ PUT /projects/:id/issues/:issue_id | `milestone_id` | integer | no | The ID of a milestone to assign the issue to | | `labels` | string | no | Comma-separated label names for an issue | | `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | +| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` | ```bash curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close @@ -351,6 +352,170 @@ DELETE /projects/:id/issues/:issue_id curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85 ``` +## Move an issue + +Moves an issue to a different project. If the operation is successful, a status +code `201` together with moved issue is returned. If the project, issue, or +target project is not found, error `404` is returned. If the target project +equals the source project or the user has insufficient permissions to move an +issue, error `400` together with an explaining error message is returned. + +``` +POST /projects/:id/issues/:issue_id/move +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | +| `to_project_id` | integer | yes | The ID of the new project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move +``` + +Example response: + +```json +{ + "id": 92, + "iid": 11, + "project_id": 5, + "title": "Sit voluptas tempora quisquam aut doloribus et.", + "description": "Repellat voluptas quibusdam voluptatem exercitationem.", + "state": "opened", + "created_at": "2016-04-05T21:41:45.652Z", + "updated_at": "2016-04-07T12:20:17.596Z", + "labels": [], + "milestone": null, + "assignee": { + "name": "Miss Monserrate Beier", + "username": "axel.block", + "id": 12, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/axel.block" + }, + "author": { + "name": "Kris Steuber", + "username": "solon.cremin", + "id": 10, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/solon.cremin" + } +} +``` + +## Subscribe to an issue + +Subscribes the authenticated user to an issue to receive notifications. If the +operation is successful, status code `201` together with the updated issue is +returned. If the user is already subscribed to the issue, the status code `304` +is returned. If the project or issue is not found, status code `404` is +returned. + +``` +POST /projects/:id/issues/:issue_id/subscription +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription +``` + +Example response: + +```json +{ + "id": 92, + "iid": 11, + "project_id": 5, + "title": "Sit voluptas tempora quisquam aut doloribus et.", + "description": "Repellat voluptas quibusdam voluptatem exercitationem.", + "state": "opened", + "created_at": "2016-04-05T21:41:45.652Z", + "updated_at": "2016-04-07T12:20:17.596Z", + "labels": [], + "milestone": null, + "assignee": { + "name": "Miss Monserrate Beier", + "username": "axel.block", + "id": 12, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/axel.block" + }, + "author": { + "name": "Kris Steuber", + "username": "solon.cremin", + "id": 10, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/solon.cremin" + } +} +``` + +## Unsubscribe from an issue + +Unsubscribes the authenticated user from the issue to not receive notifications +from it. If the operation is successful, status code `200` together with the +updated issue is returned. If the user is not subscribed to the issue, the +status code `304` is returned. If the project or issue is not found, status code +`404` is returned. + +``` +DELETE /projects/:id/issues/:issue_id/subscription +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription +``` + +Example response: + +```json +{ + "id": 93, + "iid": 12, + "project_id": 5, + "title": "Incidunt et rerum ea expedita iure quibusdam.", + "description": "Et cumque architecto sed aut ipsam.", + "state": "opened", + "created_at": "2016-04-05T21:41:45.217Z", + "updated_at": "2016-04-07T13:02:37.905Z", + "labels": [], + "milestone": null, + "assignee": { + "name": "Edwardo Grady", + "username": "keyon", + "id": 21, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/keyon" + }, + "author": { + "name": "Vivian Hermann", + "username": "orville", + "id": 11, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", + "web_url": "http://lgitlab.example.com/u/orville" + }, + "subscribed": false +} +``` + ## Comments on issues Comments are done via the [notes](notes.md) resource. diff --git a/doc/api/licenses.md b/doc/api/licenses.md new file mode 100644 index 0000000000000000000000000000000000000000..855b0eab56fe265fdad31585f8ed8e4dade28bd0 --- /dev/null +++ b/doc/api/licenses.md @@ -0,0 +1,147 @@ +# Licenses + +## List license templates + +Get all license templates. + +``` +GET /licenses +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `popular` | boolean | no | If passed, returns only popular licenses | + +```bash +curl https://gitlab.example.com/api/v3/licenses?popular=1 +``` + +Example response: + +```json +[ + { + "key": "apache-2.0", + "name": "Apache License 2.0", + "nickname": null, + "featured": true, + "html_url": "http://choosealicense.com/licenses/apache-2.0/", + "source_url": "http://www.apache.org/licenses/LICENSE-2.0.html", + "description": "A permissive license that also provides an express grant of patent rights from contributors to users.", + "conditions": [ + "include-copyright", + "document-changes" + ], + "permissions": [ + "commercial-use", + "modifications", + "distribution", + "patent-use", + "private-use" + ], + "limitations": [ + "trademark-use", + "no-liability" + ], + "content": " Apache License\n Version 2.0, January 2004\n [...]" + }, + { + "key": "gpl-3.0", + "name": "GNU General Public License v3.0", + "nickname": "GNU GPLv3", + "featured": true, + "html_url": "http://choosealicense.com/licenses/gpl-3.0/", + "source_url": "http://www.gnu.org/licenses/gpl-3.0.txt", + "description": "The GNU GPL is the most widely used free software license and has a strong copyleft requirement. When distributing derived works, the source code of the work must be made available under the same license.", + "conditions": [ + "include-copyright", + "document-changes", + "disclose-source", + "same-license" + ], + "permissions": [ + "commercial-use", + "modifications", + "distribution", + "patent-use", + "private-use" + ], + "limitations": [ + "no-liability" + ], + "content": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n [...]" + }, + { + "key": "mit", + "name": "MIT License", + "nickname": null, + "featured": true, + "html_url": "http://choosealicense.com/licenses/mit/", + "source_url": "http://opensource.org/licenses/MIT", + "description": "A permissive license that is short and to the point. It lets people do anything with your code with proper attribution and without warranty.", + "conditions": [ + "include-copyright" + ], + "permissions": [ + "commercial-use", + "modifications", + "distribution", + "private-use" + ], + "limitations": [ + "no-liability" + ], + "content": "The MIT License (MIT)\n\nCopyright (c) [year] [fullname]\n [...]" + } +] +``` + +## Single license template + +Get a single license template. You can pass parameters to replace the license +placeholder. + +``` +GET /licenses/:key +``` + +| Attribute | Type | Required | Description | +| ---------- | ------ | -------- | ----------- | +| `key` | string | yes | The key of the license template | +| `project` | string | no | The copyrighted project name | +| `fullname` | string | no | The full-name of the copyright holder | + +>**Note:** +If you omit the `fullname` parameter but authenticate your request, the name of +the authenticated user will be used to replace the copyright holder placeholder. + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/licenses/mit?project=My+Cool+Project +``` + +Example response: + +```json +{ + "key": "mit", + "name": "MIT License", + "nickname": null, + "featured": true, + "html_url": "http://choosealicense.com/licenses/mit/", + "source_url": "http://opensource.org/licenses/MIT", + "description": "A permissive license that is short and to the point. It lets people do anything with your code with proper attribution and without warranty.", + "conditions": [ + "include-copyright" + ], + "permissions": [ + "commercial-use", + "modifications", + "distribution", + "private-use" + ], + "limitations": [ + "no-liability" + ], + "content": "The MIT License (MIT)\n\nCopyright (c) 2016 John Doe\n [...]" +} +``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 20db73ea6c0b13a03610acda10be396bf753443b..2057f9d77aa486cb155ac89b4e46ff52f0eccd37 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -606,3 +606,151 @@ Example response: }, ] ``` + +## Subscribe to a merge request + +Subscribes the authenticated user to a merge request to receive notification. If +the operation is successful, status code `201` together with the updated merge +request is returned. If the user is already subscribed to the merge request, the +status code `304` is returned. If the project or merge request is not found, +status code `404` is returned. + +``` +POST /projects/:id/merge_requests/:merge_request_id/subscription +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of the merge request | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription +``` + +Example response: + +```json +{ + "id": 17, + "iid": 1, + "project_id": 5, + "title": "Et et sequi est impedit nulla ut rem et voluptatem.", + "description": "Consequatur velit eos rerum optio autem. Quia id officia quaerat dolorum optio. Illo laudantium aut ipsum dolorem.", + "state": "opened", + "created_at": "2016-04-05T21:42:23.233Z", + "updated_at": "2016-04-05T22:11:52.900Z", + "target_branch": "ui-dev-kit", + "source_branch": "version-1-9", + "upvotes": 0, + "downvotes": 0, + "author": { + "name": "Eileen Skiles", + "username": "leila", + "id": 19, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/leila" + }, + "assignee": { + "name": "Celine Wehner", + "username": "carli", + "id": 16, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/carli" + }, + "source_project_id": 5, + "target_project_id": 5, + "labels": [], + "work_in_progress": false, + "milestone": { + "id": 7, + "iid": 1, + "project_id": 5, + "title": "v2.0", + "description": "Corrupti eveniet et velit occaecati dolorem est rerum aut.", + "state": "closed", + "created_at": "2016-04-05T21:41:40.905Z", + "updated_at": "2016-04-05T21:41:40.905Z", + "due_date": null + }, + "merge_when_build_succeeds": false, + "merge_status": "cannot_be_merged", + "subscribed": true +} +``` + +## Unsubscribe from a merge request + +Unsubscribes the authenticated user from a merge request to not receive +notifications from that merge request. If the operation is successful, status +code `200` together with the updated merge request is returned. If the user is +not subscribed to the merge request, the status code `304` is returned. If the +project or merge request is not found, status code `404` is returned. + +``` +DELETE /projects/:id/merge_requests/:merge_request_id/subscription +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of the merge request | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription +``` + +Example response: + +```json +{ + "id": 17, + "iid": 1, + "project_id": 5, + "title": "Et et sequi est impedit nulla ut rem et voluptatem.", + "description": "Consequatur velit eos rerum optio autem. Quia id officia quaerat dolorum optio. Illo laudantium aut ipsum dolorem.", + "state": "opened", + "created_at": "2016-04-05T21:42:23.233Z", + "updated_at": "2016-04-05T22:11:52.900Z", + "target_branch": "ui-dev-kit", + "source_branch": "version-1-9", + "upvotes": 0, + "downvotes": 0, + "author": { + "name": "Eileen Skiles", + "username": "leila", + "id": 19, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/leila" + }, + "assignee": { + "name": "Celine Wehner", + "username": "carli", + "id": 16, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/carli" + }, + "source_project_id": 5, + "target_project_id": 5, + "labels": [], + "work_in_progress": false, + "milestone": { + "id": 7, + "iid": 1, + "project_id": 5, + "title": "v2.0", + "description": "Corrupti eveniet et velit occaecati dolorem est rerum aut.", + "state": "closed", + "created_at": "2016-04-05T21:41:40.905Z", + "updated_at": "2016-04-05T21:41:40.905Z", + "due_date": null + }, + "merge_when_build_succeeds": false, + "merge_status": "cannot_be_merged", + "subscribed": false +} +``` diff --git a/doc/api/notes.md b/doc/api/notes.md index 2e0936f11b517b1e808b5b19e17641d29b0e917b..7aa1c2155bfe8d5c2c2b5f30ffd0002ac71115a4 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -89,6 +89,7 @@ Parameters: - `id` (required) - The ID of a project - `issue_id` (required) - The ID of an issue - `body` (required) - The content of a note +- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z ### Modify existing issue note diff --git a/doc/api/projects.md b/doc/api/projects.md index ab716c229dcde534b1cdf63a5f6f047901bd9196..de1faadebf58ec77093d00ef7e2059cf355b1e47 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -491,6 +491,132 @@ Parameters: - `id` (required) - The ID of the project to be forked +### Star a project + +Stars a given project. Returns status code `201` and the project on success and +`304` if the project is already starred. + +``` +POST /projects/:id/star +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" +``` + +Example response: + +```json +{ + "id": 3, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 10, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "issues_enabled": true, + "open_issues_count": 1, + "merge_requests_enabled": true, + "builds_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, + "archived": true, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 1 +} +``` + +### Unstar a project + +Unstars a given project. Returns status code `200` and the project on success +and `304` if the project is not starred. + +``` +DELETE /projects/:id/star +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" +``` + +Example response: + +```json +{ + "id": 3, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 10, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "issues_enabled": true, + "open_issues_count": 1, + "merge_requests_enabled": true, + "builds_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, + "archived": true, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 0 +} +``` + ### Archive a project Archives the project if the user is either admin or the project owner of this project. This action is diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md index d100e2611789891d3a89a7c8ff495e056255484d..79761a893dabab1400a13e2413349efd5c82fc34 100644 --- a/doc/ci/api/builds.md +++ b/doc/ci/api/builds.md @@ -26,48 +26,114 @@ This API uses two types of authentication: ### Runs oldest pending build by runner - POST /ci/api/v1/builds/register +``` +POST /ci/api/v1/builds/register +``` -Parameters: +| Attribute | Type | Required | Description | +|-----------|---------|----------|---------------------| +| `token` | string | yes | Unique runner token | - * `token` (required) - Unique runner token +``` +curl -X POST "https://gitlab.example.com/ci/api/v1/builds/register" -F "token=t0k3n" +``` ### Update details of an existing build - PUT /ci/api/v1/builds/:id +``` +PUT /ci/api/v1/builds/:id +``` + +| Attribute | Type | Required | Description | +|-----------|---------|----------|----------------------| +| `id` | integer | yes | The ID of a project | +| `token` | string | yes | Unique runner token | +| `state` | string | no | The state of a build | +| `trace` | string | no | The trace of a build | + +``` +curl -X PUT "https://gitlab.example.com/ci/api/v1/builds/1234" -F "token=t0k3n" -F "state=running" -F "trace=Running git clone...\n" +``` + +### Incremental build trace update + +Using this method you need to send trace content as a request body. You also need to provide the `Content-Range` header +with a range of sent trace part. Note that you need to send parts in the proper order, so the begining of the part +must start just after the end of the previous part. If you provide the wrong part, then GitLab CI API will return `416 +Range Not Satisfiable` response with a header `Range: 0-X`, where `X` is the current trace length. + +For example, if you receive `Range: 0-11` in the response, then your next part must contain a `Content-Range: 11-...` +header and a trace part covered by this range. + +For a valid update API will return `202` response with: +* `Build-Status: {status}` header containing current status of the build, +* `Range: 0-{length}` header with the current trace length. + +``` +PATCH /ci/api/v1/builds/:id/trace.txt +``` Parameters: - * `id` (required) - The ID of a project - * `token` (required) - Unique runner token - * `state` (optional) - The state of a build - * `trace` (optional) - The trace of a build +| Attribute | Type | Required | Description | +|-----------|---------|----------|----------------------| +| `id` | integer | yes | The ID of a build | + +Headers: + +| Attribute | Type | Required | Description | +|-----------------|---------|----------|-----------------------------------| +| `BUILD-TOKEN` | string | yes | The build authorization token | +| `Content-Range` | string | yes | Bytes range of trace that is sent | + +``` +curl -X PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" -H "BUILD-TOKEN=build_t0k3n" -H "Content-Range=0-21" -d "Running git clone...\n" +``` + ### Upload artifacts to build - POST /ci/api/v1/builds/:id/artifacts +``` +POST /ci/api/v1/builds/:id/artifacts +``` -Parameters: +| Attribute | Type | Required | Description | +|-----------|---------|----------|-------------------------------| +| `id` | integer | yes | The ID of a build | +| `token` | string | yes | The build authorization token | +| `file` | mixed | yes | Artifacts file | - * `id` (required) - The ID of a build - * `token` (required) - The build authorization token - * `file` (required) - Artifacts file +``` +curl -X POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" -F "file=@/path/to/file" +``` ### Download the artifacts file from build - GET /ci/api/v1/builds/:id/artifacts +``` +GET /ci/api/v1/builds/:id/artifacts +``` -Parameters: +| Attribute | Type | Required | Description | +|-----------|---------|----------|-------------------------------| +| `id` | integer | yes | The ID of a build | +| `token` | string | yes | The build authorization token | - * `id` (required) - The ID of a build - * `token` (required) - The build authorization token +``` +curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" +``` ### Remove the artifacts file from build - DELETE /ci/api/v1/builds/:id/artifacts +``` +DELETE /ci/api/v1/builds/:id/artifacts +``` -Parameters: +| Attribute | Type | Required | Description | +|-----------|---------|----------|-------------------------------| +| ` id` | integer | yes | The ID of a build | +| `token` | string | yes | The build authorization token | - * ` id` (required) - The ID of a build - * `token` (required) - The build authorization token +``` +curl -X DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" +``` diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 9aba4326e116096a42ead0f4eaaa060711cf46d0..6a42a935abdfbacb874502d986295265564560a9 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -13,7 +13,7 @@ GitLab offers a [continuous integration][ci] service. If you and configure your GitLab project to use a [Runner], then each merge request or push triggers a build. -The `.gitlab-ci.yml` file tells the GitLab runner what do to. By default it +The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it runs three [stages]: `build`, `test`, and `deploy`. If everything runs OK (no non-zero return values), you'll get a nice green diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index 295d953db11de8c9fd986e7c6e0a08ead5467842..a06650b338742a09a0d4c888c26719959302bea9 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -7,6 +7,10 @@ through the coordinator API of GitLab CI. A runner can be specific to a certain project or serve any project in GitLab CI. A runner that serves all projects is called a shared runner. +Ideally, GitLab Runner should not be installed on the same machine as GitLab. +Read the [requirements documentation](../../install/requirements.md#gitlab-runner) +for more information. + ## Shared vs. Specific Runners A runner that is specific only runs for the specified project. A shared runner @@ -19,7 +23,7 @@ many projects, you can have a single or a small number of runners that handle multiple projects. This makes it easier to maintain and update runners. **Specific runners** are useful for jobs that have special requirements or for -projects with a very demand. If a job has certain requirements, you can set +projects with a specific demand. If a job has certain requirements, you can set up the specific runner with this in mind, while not having to do this for all runners. For example, if you want to deploy a certain project, you can setup a specific runner to have the right credentials for this. @@ -140,7 +144,7 @@ to it. This means that if you have shared runners setup for a project and someone forks that project, the shared runners will also serve jobs of this project. -# Attack vectors in runners +## Attack vectors in Runners Mentioned briefly earlier, but the following things of runners can be exploited. We're always looking for contributions that can mitigate these [Security Considerations](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md). diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index d790015aca1a0c0fd0f901bf4f863260031a27a9..7f825e6a065780d8d18dfa1890871ea49d3b8098 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -30,7 +30,7 @@ This is the universal solution which works with any type of executor ## SSH keys when using the Docker executor You will first need to create an SSH key pair. For more information, follow the -instructions to [generate an SSH key](../ssh/README.md). +instructions to [generate an SSH key](../../ssh/README.md). Then, create a new **Secret Variable** in your project settings on GitLab following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` @@ -63,7 +63,7 @@ before_script: As a final step, add the _public_ key from the one you created earlier to the services that you want to have an access to from within the build environment. If you are accessing a private GitLab repository you need to add it as a -[deploy key](../ssh/README.md#deploy-keys). +[deploy key](../../ssh/README.md#deploy-keys). That's it! You can now have access to private servers or repositories in your build environment. @@ -79,12 +79,12 @@ on, and use that key for all projects that are run on this machine. First, you need to login to the server that runs your builds. Then from the terminal login as the `gitlab-runner` user and generate the SSH -key pair as described in the [SSH keys documentation](../ssh/README.md). +key pair as described in the [SSH keys documentation](../../ssh/README.md). As a final step, add the _public_ key from the one you created earlier to the services that you want to have an access to from within the build environment. If you are accessing a private GitLab repository you need to add it as a -[deploy key](../ssh/README.md#deploy-keys). +[deploy key](../../ssh/README.md#deploy-keys). Once done, try to login to the remote server in order to accept the fingerprint: diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index b0e53cbc2615817cd405d8fd04401751fb088bf5..70fb81492d6c1277ce06ba4ddc37a70d17790196 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -1,17 +1,20 @@ ## Variables + When receiving a build from GitLab CI, the runner prepares the build environment. It starts by setting a list of **predefined variables** (Environment Variables) and a list of **user-defined variables** The variables can be overwritten. They take precedence over each other in this order: +1. Trigger variables 1. Secure variables -1. YAML-defined variables +1. YAML-defined job-level variables +1. YAML-defined global variables 1. Predefined variables For example, if you define: -1. API_TOKEN=SECURE as Secure Variable -1. API_TOKEN=YAML as YAML-defined variable +1. `API_TOKEN=SECURE` as Secure Variable +1. `API_TOKEN=YAML` as YAML-defined variable -The API_TOKEN will take the Secure Variable value: `SECURE`. +The `API_TOKEN` will take the Secure Variable value: `SECURE`. ### Predefined variables (Environment Variables) @@ -70,15 +73,20 @@ These variables can be later used in all executed commands and scripts. The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them. +Variables can be defined at a global level, but also at a job level. + More information about Docker integration can be found in [Using Docker Images](../docker/using_docker_images.md). ### User-defined variables (Secure Variables) **This feature requires GitLab Runner 0.4.0 or higher** -GitLab CI allows you to define per-project **Secure Variables** that are set in build environment. +GitLab CI allows you to define per-project **Secure Variables** that are set in +the build environment. The secure variables are stored out of the repository (the `.gitlab-ci.yml`). -The variables are securely passed to GitLab Runner and are available in build environment. -It's desired method to use them for storing passwords, secret keys or whatever you want. +The variables are securely passed to GitLab Runner and are available in the +build environment. +It's desired method to use them for storing passwords, secret keys or whatever +you want. **The value of the variable can be shown in build log if explicitly asked to do so.** If your project is public or internal you can make the builds private. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 7da9b31e30d4c0a49cc4162d440e1a84162988f8..7e9bced7616e521a8d5c612380486cd753122e02 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1,5 +1,50 @@ # Configuration of your builds with .gitlab-ci.yml +This document describes the usage of `.gitlab-ci.yml`, the file that is used by +GitLab Runner to manage your project's builds. + +If you want a quick introduction to GitLab CI, follow our +[quick start guide](../quick_start/README.md). + +--- + +<!-- START doctoc generated TOC please keep comment here to allow auto update --> +<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [.gitlab-ci.yml](#gitlab-ci-yml) + - [image and services](#image-and-services) + - [before_script](#before_script) + - [after_script](#after_script) + - [stages](#stages) + - [types](#types) + - [variables](#variables) + - [cache](#cache) + - [cache:key](#cache-key) +- [Jobs](#jobs) + - [script](#script) + - [stage](#stage) + - [job variables](#job-variables) + - [only and except](#only-and-except) + - [tags](#tags) + - [when](#when) + - [artifacts](#artifacts) + - [artifacts:name](#artifacts-name) + - [dependencies](#dependencies) + - [before_script and after_script](#before_script-and-after_script) +- [Hidden jobs](#hidden-jobs) +- [Special YAML features](#special-yaml-features) + - [Anchors](#anchors) +- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml) +- [Skipping builds](#skipping-builds) +- [Examples](#examples) + +<!-- END doctoc generated TOC please keep comment here to allow auto update --> + +--- + +## .gitlab-ci.yml + From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML) file (`.gitlab-ci.yml`) for the project configuration. It is placed in the root of your repository and contains definitions of how your project should be built. @@ -23,12 +68,10 @@ Of course a command can execute code directly (`./configure;make;make install`) or run a script (`test.sh`) in the repository. Jobs are used to create builds, which are then picked up by -[runners](../runners/README.md) and executed within the environment of the -runner. What is important, is that each job is run independently from each +[Runners](../runners/README.md) and executed within the environment of the +Runner. What is important, is that each job is run independently from each other. -## .gitlab-ci.yml - The YAML syntax allows for using more complex job specifications than in the above example: @@ -40,6 +83,9 @@ services: before_script: - bundle install +after_script: + - rm secrets + stages: - build - test @@ -64,6 +110,7 @@ There are a few reserved `keywords` that **cannot** be used as job names: | stages | no | Define build stages | | types | no | Alias for `stages` | | before_script | no | Define commands that run before each job's script | +| after_script | no | Define commands that run after each job's script | | variables | no | Define build variables | | cache | no | Define list of files that should be cached between subsequent runs | @@ -71,13 +118,21 @@ There are a few reserved `keywords` that **cannot** be used as job names: This allows to specify a custom Docker image and a list of services that can be used for time of the build. The configuration of this feature is covered in -separate document: [Use Docker](../docker/README.md). +[a separate document](../docker/README.md). ### before_script `before_script` is used to define the command that should be run before all builds, including deploy builds. This can be an array or a multi-line string. +### after_script + +>**Note:** +Introduced in GitLab 8.7 and GitLab Runner v1.2. + +`after_script` is used to define the command that will be run after for all +builds. This has to be an array or a multi-line string. + ### stages `stages` is used to define build stages that can be used by jobs. @@ -86,7 +141,8 @@ The specification of `stages` allows for having flexible multi stage pipelines. The ordering of elements in `stages` defines the ordering of builds' execution: 1. Builds of the same stage are run in parallel. -1. Builds of next stage are run after success. +1. Builds of the next stage are run after the jobs from the previous stage + complete successfully. Let's consider the following example, which defines 3 stages: @@ -98,9 +154,9 @@ stages: ``` 1. First all jobs of `build` are executed in parallel. -1. If all jobs of `build` succeeds, the `test` jobs are executed in parallel. -1. If all jobs of `test` succeeds, the `deploy` jobs are executed in parallel. -1. If all jobs of `deploy` succeeds, the commit is marked as `success`. +1. If all jobs of `build` succeed, the `test` jobs are executed in parallel. +1. If all jobs of `test` succeed, the `deploy` jobs are executed in parallel. +1. If all jobs of `deploy` succeed, the commit is marked as `success`. 1. If any of the previous jobs fails, the commit is marked as `failed` and no jobs of further stage are executed. @@ -133,6 +189,8 @@ These variables can be later used in all executed commands and scripts. The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them. +Variables can be also defined on [job level](#job-variables). + ### cache >**Note:** @@ -278,23 +336,26 @@ job_name: | Keyword | Required | Description | |---------------|----------|-------------| -| script | yes | Defines a shell script which is executed by runner | +| script | yes | Defines a shell script which is executed by Runner | | image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | | services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | | stage | no | Defines a build stage (default: `test`) | | type | no | Alias for `stage` | +| variables | no | Define build variables on a job level | | only | no | Defines a list of git refs for which build is created | | except | no | Defines a list of git refs for which build is not created | -| tags | no | Defines a list of tags which are used to select runner | +| tags | no | Defines a list of tags which are used to select Runner | | allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status | | when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` | | dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them| | artifacts | no | Define list build artifacts | | cache | no | Define list of files that should be cached between subsequent runs | +| before_script | no | Override a set of commands that are executed before build | +| after_script | no | Override a set of commands that are executed after build | ### script -`script` is a shell script which is executed by the runner. For example: +`script` is a shell script which is executed by the Runner. For example: ```yaml job: @@ -373,15 +434,27 @@ job: The above example will run `job` for all branches on `gitlab-org/gitlab-ce`, except master. +### job variables + +It is possible to define build variables using a `variables` keyword on a job +level. It works basically the same way as its global-level equivalent but +allows you to define job-specific build variables. + +When the `variables` keyword is used on a job level, it overrides global YAML +build variables and predefined variables. + +Build variables priority is defined in +[variables documentation](../variables/README.md). + ### tags -`tags` is used to select specific runners from the list of all runners that are +`tags` is used to select specific Runners from the list of all Runners that are allowed to run this project. -During the registration of a runner, you can specify the runner's tags, for +During the registration of a Runner, you can specify the Runner's tags, for example `ruby`, `postgres`, `development`. -`tags` allow you to run builds with runners that have the specified tags +`tags` allow you to run builds with Runners that have the specified tags assigned to them: ```yaml @@ -391,7 +464,7 @@ job: - postgres ``` -The specification above, will make sure that `job` is built by a runner that +The specification above, will make sure that `job` is built by a Runner that has both `ruby` AND `postgres` tags defined. ### when @@ -635,6 +708,23 @@ deploy: script: make deploy ``` +### before_script and after_script + +It's possible to overwrite globally defined `before_script` and `after_script`: + +```yaml +before_script +- global before script + +job: + before_script: + - execute this instead of global before script + script: + - my command + after_script: + - execute this after my script +``` + ## Hidden jobs >**Note:** diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index 9f3fd69fc4eeef4e163337d101e2ea4f8c0fa590..6d04b9590e6e33ae1142f3af34f085b22f50133c 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -9,7 +9,7 @@ bundle exec rake setup ``` The `setup` task is a alias for `gitlab:setup`. -This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database. +This tasks calls `db:reset` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database. Note: `db:setup` calls `db:seed` but this does nothing. ## Run tests diff --git a/doc/install/installation.md b/doc/install/installation.md index f8f7d6a9ebedfb54fcc0669164eb63310d403a7a..e721e70a59615403d0173cd7da3c557e4b3cbdff 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -530,6 +530,16 @@ See the [omniauth integration document](../integration/omniauth.md) GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you. Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it +### Adding your Trusted Proxies + +If you are using a reverse proxy on an separate machine, you may want to add the +proxy to the trusted proxies list. Otherwise users will appear signed in from the +proxy's IP address. + +You can add trusted proxies in `config/gitlab.yml` by customizing the `trusted_proxies` +option in section 1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md) +for the changes to take effect. + ### Custom Redis Connection If you'd like Resque to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file. diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 03cb08dd1f1df0e006020f57fe9c15c225784130..eb9fe5e1b1bbf724b8d5f9bfc6a9b6d47cd9abea 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -79,6 +79,26 @@ With less memory GitLab will give strange errors during the reconfigure run and Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those. +## Gitlab Runner + +We strongly advise against installing GitLab Runner on the same machine you plan +to install GitLab on. Depending on how you decide to configure GitLab Runner and +what tools you use to exercise your application in the CI environment, GitLab +Runner can consume significant amount of available memory. + +Memory consumption calculations, that are available above, will not be valid if +you decide to run GitLab Runner and the GitLab Rails application on the same +machine. + +It is also not safe to install everything on a single machine, because of the +[security reasons] - especially when you plan to use shell executor with GitLab +Runner. + +We recommend using a separate machine for each GitLab Runner, if you plan to +use the CI features. + +[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md + ## Unicorn Workers It's possible to increase the amount of unicorn workers and this will usually help for to reduce the response time of the applications and increase the ability to handle parallel requests. diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 25f359883056511932fb9d192e4677bc0ea3c145..cab329c0decf870027b86e14d27d956bad72de1c 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -120,6 +120,29 @@ OmniAuth provider for an existing user. The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on. +## Configure OmniAuth Providers as External + +>**Note:** +This setting was introduced with version 8.7 of GitLab + +You can define which OmniAuth providers you want to be `external` so that all users +creating accounts via these providers will not be able to have access to internal +projects. You will need to use the full name of the provider, like `google_oauth2` +for Google. Refer to the examples for the full names of the supported providers. + +**For Omnibus installations** + +```ruby + gitlab_rails['omniauth_external_providers'] = ['twitter', 'google_oauth2'] +``` + +**For installations from source** + +```yaml + omniauth: + external_providers: ['twitter', 'google_oauth2'] +``` + ## Using Custom Omniauth Providers >**Note:** diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md index a0be3dd4e5cc3f3ee63cf84e1dd966aa639fbfbd..b6b2d4e5e88cdddb89b6b4e5e8a00ff63a6712f8 100644 --- a/doc/integration/shibboleth.md +++ b/doc/integration/shibboleth.md @@ -76,3 +76,50 @@ sudo gitlab-ctl reconfigure ``` On the sign in page there should now be a "Sign in with: Shibboleth" icon below the regular sign in form. Click the icon to begin the authentication process. You will be redirected to IdP server (Depends on your Shibboleth module configuration). If everything goes well the user will be returned to GitLab and will be signed in. + +## Apache 2.4 / GitLab 8.6 update +The order of the first 2 Location directives is important. If they are reversed, +you will not get a shibboleth session! + +``` + <Location /> + Require all granted + ProxyPassReverse http://127.0.0.1:8181 + ProxyPassReverse http://YOUR_SERVER_FQDN/ + </Location> + + <Location /users/auth/shibboleth/callback> + AuthType shibboleth + ShibRequestSetting requireSession 1 + ShibUseHeaders On + Require shib-session + </Location> + + Alias /shibboleth-sp /usr/share/shibboleth + + <Location /shibboleth-sp> + Require all granted + </Location> + + <Location /Shibboleth.sso> + SetHandler shib + </Location> + + RewriteEngine on + + #Don't escape encoded characters in api requests + RewriteCond %{REQUEST_URI} ^/api/v3/.* + RewriteCond %{REQUEST_URI} !/Shibboleth.sso + RewriteCond %{REQUEST_URI} !/shibboleth-sp + RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA,NE] + + #Forward all requests to gitlab-workhorse except existing files + RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f [OR] + RewriteCond %{REQUEST_URI} ^/uploads/.* + RewriteCond %{REQUEST_URI} !/Shibboleth.sso + RewriteCond %{REQUEST_URI} !/shibboleth-sp + RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA] + + RequestHeader set X_FORWARDED_PROTO 'https' + RequestHeader set X-Forwarded-Ssl on +``` \ No newline at end of file diff --git a/doc/intro/README.md b/doc/intro/README.md index fecbbe6317b0a63c4585da46d253db3f67845c1a..ab298d3808e251057cc171d497c305b43e5927dc 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -25,6 +25,7 @@ Create merge requests and review code. - [Automatically close issues from merge requests](../customization/issue_closing.md) - [Automatically merge when your builds succeed](../workflow/merge_when_build_succeeds.md) - [Revert any commit](../workflow/revert_changes.md) +- [Cherry-pick any commit](../workflow/cherry_pick_changes.md) ## Test and Deploy diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md index b856e7935a3955df647f00abfed40fa1f4efc3aa..90e99302210ee08347e0005d018171aedd831d05 100644 --- a/doc/monitoring/performance/gitlab_configuration.md +++ b/doc/monitoring/performance/gitlab_configuration.md @@ -37,3 +37,4 @@ Read more on: - [Introduction to GitLab Performance Monitoring](introduction.md) - [InfluxDB Configuration](influxdb_configuration.md) - [InfluxDB Schema](influxdb_schema.md) +- [Grafana Install/Configuration](grafana_configuration.md diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md index 416c9870aa081251adaba4893315e8781744fa33..a79c8d48d3b825f2ea43548fa2cfffbb4ba85647 100644 --- a/doc/monitoring/performance/grafana_configuration.md +++ b/doc/monitoring/performance/grafana_configuration.md @@ -61,24 +61,32 @@ contents below and paste it in to the interactive session: ``` CREATE RETENTION POLICY gitlab_30d ON gitlab DURATION 30d REPLICATION 1 DEFAULT CREATE RETENTION POLICY seven_days ON gitlab DURATION 7d REPLICATION 1 -CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.rails_transaction_timings FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.sidekiq_transaction_timings FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m), method END -CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m), method END -CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM gitlab.gitlab_30d.rails_memory_usage GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM gitlab.gitlab_30d.sidekiq_memory_usage GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM gitlab.gitlab_30d.sidekiq_file_descriptors GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM gitlab.gitlab_30d.rails_file_descriptors GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END +CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM rails_transactions GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM sidekiq_transactions GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM rails_method_calls GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM sidekiq_method_calls GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM rails_method_calls GROUP BY time(1m), method END; +CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM sidekiq_method_calls GROUP BY time(1m), method END; +CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM rails_memory_usage GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM sidekiq_memory_usage GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM sidekiq_file_descriptors GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM rails_file_descriptors GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM rails_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM rails_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM rails_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_allowed_request_counts_per_minute ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_internal_allowed_request_counts_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_allowed_request_timings_per_minute ON gitlab BEGIN SELECT percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.grape_internal_allowed_request_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_allowed_sql_timings_per_minute ON gitlab BEGIN SELECT percentile(sql_duration, 95) AS duration_95th, percentile(sql_duration, 99) AS duration_99th, mean(sql_duration) AS duration_mean INTO gitlab.seven_days.grape_internal_allowed_sql_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_authorized_keys_request_counts_per_minute ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_internal_authorized_keys_request_counts_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_authorized_keys_request_timings_per_minute ON gitlab BEGIN SELECT percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.grape_internal_authorized_keys_request_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_authorized_keys_sql_timings_per_minute ON gitlab BEGIN SELECT percentile(sql_duration, 95) AS duration_95th, percentile(sql_duration, 99) AS duration_99th, mean(sql_duration) AS duration_mean INTO gitlab.seven_days.grape_internal_authorized_keys_sql_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(view_duration, 95.000) AS view_duration_95th, percentile(view_duration, 99.000) AS view_duration_99th, mean(view_duration) AS view_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.rails_transaction_timings FROM rails_transactions GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(view_duration, 95.000) AS view_duration_95th, percentile(view_duration, 99.000) AS view_duration_99th, mean(view_duration) AS view_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.sidekiq_transaction_timings FROM sidekiq_transactions GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_transaction_counts FROM rails_transactions WHERE action !~ /.+/ GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.grape_transaction_timings FROM rails_transactions WHERE action !~ /.+/ GROUP BY time(1m) END; ``` ## Import Dashboards @@ -91,21 +99,25 @@ JSON file. Open the dashboard dropdown menu and click 'Import' - + Click 'Choose file' and browse to the location where you downloaded or cloned the dashboard repository. Pick one of the JSON files to import. - + Once the dashboard is imported, be sure to click save icon in the top bar. If you do not save the dashboard after importing it will be removed when you navigate away. - + Repeat this process for each dashboard you wish to import. +Alternatively you can automatically import all the dashboards into your Grafana +instance. See the README of the [Grafana dashboards][grafana-dashboards] +repository for more information on this process. + [grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards --- diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md index 3a2b598b78f5be80b8469adf0c4c6925a3d10b29..63aa03985ef28b02a748e711aa7929301e222dad 100644 --- a/doc/monitoring/performance/influxdb_configuration.md +++ b/doc/monitoring/performance/influxdb_configuration.md @@ -181,6 +181,7 @@ Read more on: - [Introduction to GitLab Performance Monitoring](introduction.md) - [GitLab Configuration](gitlab_configuration.md) - [InfluxDB Schema](influxdb_schema.md) +- [Grafana Install/Configuration](grafana_configuration.md [influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management [influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/ diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md index a5a8aebd2d1381643cb27f0c1375a9e415bfa969..d31b3788f36ab7795ab2c9f7851b5e5290ba9cc4 100644 --- a/doc/monitoring/performance/influxdb_schema.md +++ b/doc/monitoring/performance/influxdb_schema.md @@ -85,3 +85,4 @@ Read more on: - [Introduction to GitLab Performance Monitoring](introduction.md) - [GitLab Configuration](gitlab_configuration.md) - [InfluxDB Configuration](influxdb_configuration.md) +- [Grafana Install/Configuration](grafana_configuration.md diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png index 2b37eda35202160a69048732d759435e074f3b82..c225daa81e18d6c7e2ff26e30d7695fe218b2e1e 100644 Binary files a/doc/project_services/img/jira_service_page.png and b/doc/project_services/img/jira_service_page.png differ diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 27170c1eb1947bc9bba12a1861c5fccb5aaa4c9f..b626c746c793626c9d98c7d59ad277c430467517 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -1,9 +1,9 @@ # GitLab JIRA integration -_**Note:** +>**Note:** Full JIRA integration was previously exclusive to GitLab Enterprise Edition. With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce] -to GitLab Community Edition as well._ +to GitLab Community Edition as well. --- @@ -88,8 +88,9 @@ password as they will be needed when configuring GitLab in the next section. ### Configuring GitLab -_**Note:** The currently supported JIRA versions are v6.x and v7.x. and GitLab -7.8 or higher is required._ +>**Note:** +The currently supported JIRA versions are v6.x and v7.x. and GitLab +7.8 or higher is required. --- @@ -113,13 +114,24 @@ Fill in the required details on the page, as described in the table below. | `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. | | `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | -| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot](img/jira_issues_workflow.png)). By default, this ID is set to `2` | +| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | After saving the configuration, your GitLab project will be able to interact with the linked JIRA project. +For example, given the settings below: + +- the JIRA URL is `https://jira.example.com` +- the project is named `GITLAB` +- the user is named `gitlab` +- the JIRA issue transition is 151 (based on the [JIRA issue transition][trans]) + +the following screenshot shows how the JIRA service settings should look like. +  +[trans]: img/jira_issues_workflow.png + --- ## JIRA issues diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md index 20aa90f0d697e148397fc26be3dfdeca99398249..17bb75ececdaf198a340c826a9ca6995e5325b9b 100644 --- a/doc/public_access/public_access.md +++ b/doc/public_access/public_access.md @@ -58,6 +58,9 @@ you are logged in or not. When visiting the public page of a user, you can only see the projects which you are privileged to. +If the public level is restricted, user profiles are only visible to logged in users. + + ## Restricting the use of public or internal projects In the Admin area under **Settings** (`/admin/application_settings`), you can diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 612376e3a49a5515967f810bf38ab5e5e8acaaf6..c44930a4ceb9f428a71bf753fcec4512bbaf59e1 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -4,6 +4,12 @@ Your GitLab instance can perform HTTP POST requests on the following events: `pr System hooks can be used, e.g. for logging or changing information in a LDAP server. +> **Note:** +> +> We follow the same structure from Webhooks for Push and Tag events, but we never display commits. +> +> Same deprecations from Webhooks are valid here. + ## Hooks request example **Request header**: @@ -240,3 +246,110 @@ X-Gitlab-Event: System Hook "user_id": 41 } ``` + +## Push events + +Triggered when you push to the repository except when pushing tags. + +**Request header**: + +``` +X-Gitlab-Event: System Hook +``` + +**Request body:** + +```json +{ + "event_name": "push", + "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", + "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "ref": "refs/heads/master", + "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "user_id": 4, + "user_name": "John Smith", + "user_email": "john@example.com", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 15, + "project":{ + "name":"Diaspora", + "description":"", + "web_url":"http://example.com/mike/diaspora", + "avatar_url":null, + "git_ssh_url":"git@example.com:mike/diaspora.git", + "git_http_url":"http://example.com/mike/diaspora.git", + "namespace":"Mike", + "visibility_level":0, + "path_with_namespace":"mike/diaspora", + "default_branch":"master", + "homepage":"http://example.com/mike/diaspora", + "url":"git@example.com:mike/diaspora.git", + "ssh_url":"git@example.com:mike/diaspora.git", + "http_url":"http://example.com/mike/diaspora.git" + }, + "repository":{ + "name": "Diaspora", + "url": "git@example.com:mike/diaspora.git", + "description": "", + "homepage": "http://example.com/mike/diaspora", + "git_http_url":"http://example.com/mike/diaspora.git", + "git_ssh_url":"git@example.com:mike/diaspora.git", + "visibility_level":0 + }, + "commits": [], + "total_commits_count": 0 +} +``` + +## Tag events + +Triggered when you create (or delete) tags to the repository. + +**Request header**: + +``` +X-Gitlab-Event: System Hook +``` + +**Request body:** + +```json +{ + "event_name": "tag_push", + "before": "0000000000000000000000000000000000000000", + "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", + "ref": "refs/tags/v1.0.0", + "checkout_sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "user_id": 1, + "user_name": "John Smith", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 1, + "project":{ + "name":"Example", + "description":"", + "web_url":"http://example.com/jsmith/example", + "avatar_url":null, + "git_ssh_url":"git@example.com:jsmith/example.git", + "git_http_url":"http://example.com/jsmith/example.git", + "namespace":"Jsmith", + "visibility_level":0, + "path_with_namespace":"jsmith/example", + "default_branch":"master", + "homepage":"http://example.com/jsmith/example", + "url":"git@example.com:jsmith/example.git", + "ssh_url":"git@example.com:jsmith/example.git", + "http_url":"http://example.com/jsmith/example.git" + }, + "repository":{ + "name": "Example", + "url": "ssh://git@example.com/jsmith/example.git", + "description": "", + "homepage": "http://example.com/jsmith/example", + "git_http_url":"http://example.com/jsmith/example.git", + "git_ssh_url":"git@example.com:jsmith/example.git", + "visibility_level":0 + }, + "commits": [], + "total_commits_count": 0 +} +``` diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md index b9abcbd2c12f61a0f8779ea5b18940c0c240bb8e..6267f14eba4626c53f157da545ac0d519f217a42 100644 --- a/doc/update/8.5-to-8.6.md +++ b/doc/update/8.5-to-8.6.md @@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-6-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all -sudo -u git -H git checkout v2.6.11 +sudo -u git -H git checkout v2.6.12 ``` ### 5. Update gitlab-workhorse diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 22e207b6d321f4ed9320d9e20013e73f313075c9..c1c51302e79be8a2b36ce9e8ff1d0a6f4dfa1eac 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -41,6 +41,7 @@ X-Gitlab-Event: Push Hook "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "ref": "refs/heads/master", + "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "user_id": 4, "user_name": "John Smith", "user_email": "john@example.com", @@ -118,9 +119,10 @@ X-Gitlab-Event: Tag Push Hook ```json { "object_kind": "tag_push", - "ref": "refs/tags/v1.0.0", "before": "0000000000000000000000000000000000000000", "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", + "ref": "refs/tags/v1.0.0", + "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", "user_id": 1, "user_name": "John Smith", "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 25893f948eac1a86d525788a96c7faffb2e86729..9efe41308dcfb303699f4f0c5fc8d0aa24f70878 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -20,6 +20,7 @@ - [Milestones](milestones.md) - [Merge Requests](merge_requests.md) - [Revert changes](revert_changes.md) +- [Cherry-pick changes](cherry_pick_changes.md) - ["Work In Progress" Merge Requests](wip_merge_requests.md) - [Merge When Build Succeeds](merge_when_build_succeeds.md) - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) diff --git a/doc/workflow/cherry_pick_changes.md b/doc/workflow/cherry_pick_changes.md new file mode 100644 index 0000000000000000000000000000000000000000..b0ca0879643a4ee3a910b85d3ec3c9d565d656c5 --- /dev/null +++ b/doc/workflow/cherry_pick_changes.md @@ -0,0 +1,52 @@ +# Cherry-pick changes + +_**Note:** This feature was [introduced][ce-3514] in GitLab 8.7._ + +--- + +GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick] +with introducing a **Cherry-pick** button in Merge Requests and commit details. + +## Cherry-picking a Merge Request + +After the Merge Request has been merged, a **Cherry-pick** button will be available +to cherry-pick the changes introduced by that Merge Request: + + + +--- + +You can cherry-pick the changes directly into the selected branch or you can opt to +create a new Merge Request with the cherry-pick changes: + + + +## Cherry-picking a Commit + +You can cherry-pick a Commit from the Commit details page: + + + +--- + +Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes +directly into the target branch or create a new Merge Request to cherry-pick the +changes: + + + +--- + +Please note that when cherry-picking merge commits, the mainline will always be the +first parent. If you want to use a different mainline then you need to do that +from the command line. + +Here is a quick example to cherry-pick a merge commit using the second parent as the +mainline: + +```bash +git cherry-pick -m 2 7a39eb0 +``` + +[ce-3514]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3514 "Cherry-pick button Merge Request" +[git-cherry-pick]: https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation" diff --git a/doc/workflow/img/cherry_pick_changes_commit.png b/doc/workflow/img/cherry_pick_changes_commit.png new file mode 100644 index 0000000000000000000000000000000000000000..ae91d2cae53c5fe25b791ff415a6b436e33e2e0f Binary files /dev/null and b/doc/workflow/img/cherry_pick_changes_commit.png differ diff --git a/doc/workflow/img/cherry_pick_changes_commit_modal.png b/doc/workflow/img/cherry_pick_changes_commit_modal.png new file mode 100644 index 0000000000000000000000000000000000000000..f502f87677a233f96b2fcfc101404cd01f51f83f Binary files /dev/null and b/doc/workflow/img/cherry_pick_changes_commit_modal.png differ diff --git a/doc/workflow/img/cherry_pick_changes_mr.png b/doc/workflow/img/cherry_pick_changes_mr.png new file mode 100644 index 0000000000000000000000000000000000000000..59c610e620be5f531f6fbc4b2771dc41cb47be7a Binary files /dev/null and b/doc/workflow/img/cherry_pick_changes_mr.png differ diff --git a/doc/workflow/img/cherry_pick_changes_mr_modal.png b/doc/workflow/img/cherry_pick_changes_mr_modal.png new file mode 100644 index 0000000000000000000000000000000000000000..96a80f4726de8be7ef0b78c15029484694738d87 Binary files /dev/null and b/doc/workflow/img/cherry_pick_changes_mr_modal.png differ diff --git a/doc/workflow/img/new_branch_from_issue.png b/doc/workflow/img/new_branch_from_issue.png new file mode 100644 index 0000000000000000000000000000000000000000..394c139e17eadc81e8699aca05c727bf48c6a86d Binary files /dev/null and b/doc/workflow/img/new_branch_from_issue.png differ diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index ba91685a20b835eefc0de954f4958a2bacef595b..31620044b1501d486989bedbdc255e7c844d32bf 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -44,7 +44,7 @@ check it into your Git repository: ```bash git clone git@gitlab.example.com:group/project.git -git lfs init # initialize the Git LFS project project +git lfs install # initialize the Git LFS project project git lfs track "*.iso" # select the file extensions that you want to treat as large files ``` diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md index 4a451d98953d9aaf3cdd79fdd1fa483369b6492f..1832567a34c94dd783b7b3d257e549109c370c4e 100644 --- a/doc/workflow/web_editor.md +++ b/doc/workflow/web_editor.md @@ -66,6 +66,35 @@ the target branch. Click **Create directory** to finish. ## Create a new branch +There are multiple ways to create a branch from GitLab's web interface. + +### Create a new branch from an issue + +>**Note:** +This feature was [introduced][ce-2808] in GitLab 8.6. + +In case your development workflow dictates to have an issue for every merge +request, you can quickly create a branch right on the issue page which will be +tied with the issue itself. You can see a **New Branch** button after the issue +description, unless there is already a branch with the same name or a referenced +merge request. + + + +Once you click it, a new branch will be created that diverges from the default +branch of your project, by default `master`. The branch name will be based on +the title of the issue and as suffix it will have its ID. Thus, the example +screenshot above will yield a branch named +`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`. + +After the branch is created, you can edit files in the repository to fix +the issue. When a merge request is created based on the newly created branch, +the description field will automatically display the [issue closing pattern] +`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the +merge request is merged. + +### Create a new branch from a project's dashboard + If you want to make changes to several files before creating a new merge request, you can create a new branch up front. From a project's files page, choose **New branch** from the dropdown. @@ -118,3 +147,6 @@ appear that is labeled **Start a new merge request with these changes**. After you commit the changes you will be taken to a new merge request form.  + +[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808 +[issue closing pattern]: ../customization/issue_closing.md diff --git a/features/profile/notifications.feature b/features/profile/notifications.feature index 55997d44dec28477bba861b2788c1a893f5a9534..ef8743932f510ab05eeaa0ba7654b7160cedc6c8 100644 --- a/features/profile/notifications.feature +++ b/features/profile/notifications.feature @@ -7,3 +7,9 @@ Feature: Profile Notifications Scenario: I visit notifications tab When I visit profile notifications page Then I should see global notifications settings + + @javascript + Scenario: I edit Project Notifications + Given I visit profile notifications page + When I select Mention setting from dropdown + Then I should see Notification saved message diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature index e07f8053fb7ddffaa80cd6faa6302d5151fa0695..49d7a3b9af248d1888539d7d6fa416f1457f0d71 100644 --- a/features/project/issues/filter_labels.feature +++ b/features/project/issues/filter_labels.feature @@ -12,6 +12,7 @@ Feature: Project Issues Filter Labels @javascript Scenario: I filter by one label Given I click link "bug" + And I click "dropdown close button" Then I should see "Bugfix1" in issues list And I should see "Bugfix2" in issues list And I should not see "Feature1" in issues list diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 1e09dbc4c8ffa7ec48079653b3979c51c9b6c0e1..fdffd71de85f85f826c9d610c1a2e33dc1db92bb 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -123,19 +123,6 @@ Feature: Project Source Browse Files And I am redirected to the fork's new merge request page And I can see the replacement commit message - @javascript - Scenario: I can create file in empty repo - Given I own an empty project - And I visit my empty project page - And I create bare repo - When I click on "add a file" link - And I edit code - And I fill the new file name - And I fill the commit message - And I click on "Commit Changes" - Then I am redirected to the new file - And I should see its new content - @javascript Scenario: If I enter an illegal file name I see an error message Given I click on "New file" link in repo diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index a6e574f12a96002f9569eb4f50c6a862e0333137..2b23df6764b7bf14c7d316ce2950de4b3b1984cf 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -31,7 +31,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps expect(page).to have_content 'Done 0' expect(page).to have_link project.name_with_namespace - should_see_todo(1, "John Doe assigned you merge request !#{merge_request.iid}", merge_request.title) + should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title) should_see_todo(2, "John Doe mentioned you on issue ##{issue.iid}", "#{current_user.to_reference} Wdyt?") should_see_todo(3, "John Doe assigned you issue ##{issue.iid}", issue.title) should_see_todo(4, "Mary Jane mentioned you on issue ##{issue.iid}", issue.title) @@ -45,7 +45,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps page.within('.nav-sidebar') { expect(page).to have_content 'Todos 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.iid}" + should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" end step 'I click on the "Done" tab' do @@ -54,7 +54,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps step 'I should see all todos marked as done' do expect(page).to have_link project.name_with_namespace - should_see_todo(1, "John Doe assigned you merge request !#{merge_request.iid}", merge_request.title, false) + should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false) end step 'I filter by "Enterprise"' do @@ -82,11 +82,11 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps end step 'I should not see todos related to "Merge Requests" in the list' do - should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}" + should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" end step 'I should not see todos related to "Assignments" in the list' do - should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}" + should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" should_not_see_todo "John Doe assigned you issue ##{issue.iid}" end diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb index 4724a32627778377b4d1ec08a4a384004b138330..3b59089a093e0277849370283a35cca8b2898386 100644 --- a/features/steps/profile/active_tab.rb +++ b/features/steps/profile/active_tab.rb @@ -22,4 +22,8 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps step 'the active main tab should be Audit Log' do ensure_active_main_tab('Audit Log') end + + def ensure_active_main_tab(content) + expect(find('.layout-nav li.active')).to have_content(content) + end end diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb index 447ea6d9d10fd5917772b81bee7f5d1bc6b5e986..a96f35ada51e821a43a26188c4652eb0a737af1f 100644 --- a/features/steps/profile/notifications.rb +++ b/features/steps/profile/notifications.rb @@ -9,4 +9,14 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps step 'I should see global notifications settings' do expect(page).to have_content "Notifications" end + + step 'I select Mention setting from dropdown' do + select 'mention', from: 'notification_setting_level' + end + + step 'I should see Notification saved message' do + page.within '.flash-container' do + expect(page).to have_content 'Notification settings saved' + end + end end diff --git a/features/steps/project/commits/user_lookup.rb b/features/steps/project/commits/user_lookup.rb index 40cada6da45017b10fcacafaa5d3b77ce3a1558b..2d43be5a38623d76483cecc4bbcb1f81aa060a0b 100644 --- a/features/steps/project/commits/user_lookup.rb +++ b/features/steps/project/commits/user_lookup.rb @@ -29,8 +29,9 @@ class Spinach::Features::ProjectCommitsUserLookup < Spinach::FeatureSteps def check_author_link(email, user) author_link = find('.commit-author-link') + expect(author_link['href']).to eq user_path(user) - expect(author_link['data-original-title']).to eq email + expect(author_link['title']).to eq email expect(find('.commit-author-name').text).to eq user.name end diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 612bb8fd8b1e5d4f554cbaaedbda44505cb1c50e..0ead83d6937d9660c5316716f1615eee73496181 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -114,7 +114,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps step 'I see the edit page prefilled for "Merge Request On Forked Project"' do expect(current_path).to eq edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - expect(page).to have_content "Edit merge request ##{@merge_request.id}" + expect(page).to have_content "Edit merge request #{@merge_request.to_reference}" expect(find("#merge_request_title").value).to eq "Merge Request On Forked Project" end diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb index 6d50501a722d05a7daa33493ef7782ae752ff654..d82c68569182e2d0d6e03da21d6f91d109abaaee 100644 --- a/features/steps/project/issues/filter_labels.rb +++ b/features/steps/project/issues/filter_labels.rb @@ -32,6 +32,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps page.find('.js-label-select').click sleep 0.5 execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") + end + + step 'I click "dropdown close button"' do + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click sleep 2 end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index aff5ca676beb4e8242010d2bf35d8e730d4f146f..fc12843ea5ca4a3a692388604107aaaed1366e8b 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -20,11 +20,11 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I should see that I am subscribed' do - expect(find('.subscribe-button span')).to have_content 'Unsubscribe' + expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe' end step 'I should see that I am unsubscribed' do - expect(find('.subscribe-button span')).to have_content 'Subscribe' + expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe' end step 'I click link "Closed"' do diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb index 17944527e3a2070e20535ac92e8475efd447bfb8..5bb021890214793501ef766d58516e0c2e370fc2 100644 --- a/features/steps/project/labels.rb +++ b/features/steps/project/labels.rb @@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps private def subscribe_button - first('.subscribe-button span') + first('.label-subscribe-button span') end end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index f0af0d097fa4957ee7662ea7256403f784efbc34..3b1a00f628af07bfdefd70150c0ca291f5f1f981 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -77,11 +77,11 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see that I am subscribed' do - expect(find('.subscribe-button span')).to have_content 'Unsubscribe' + expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe' end step 'I should see that I am unsubscribed' do - expect(find('.subscribe-button span')).to have_content 'Subscribe' + expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe' end step 'I click button "Unsubscribe"' do @@ -519,7 +519,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step '"Bug NS-05" has CI status' do project = merge_request.source_project project.enable_ci - ci_commit = create :ci_commit, project: project, sha: merge_request.last_commit.id + ci_commit = create :ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch create :ci_build, commit: ci_commit end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index e072505e5d7bec13e6807fa9921575e5b6741c8e..c26d7a1521214cd1f52d8e408cd5261a19257a36 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -282,8 +282,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps click_link 'Create empty bare repository' end - step 'I click on "add a file" link' do - click_link 'adding README' + step 'I click on "README" link' do + click_link 'README' # Remove pre-receive hook so we can push without auth FileUtils.rm_f(File.join(@project.repository.path, 'hooks', 'pre-receive')) diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index c4c7672a432fdac103a93310e5687937ad4a15a7..cf30e23b6bd35f4e1b86c8de578295b52ab3957c 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -10,16 +10,16 @@ module SharedBuilds end step 'project has a recent build' do - @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha) + @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha, ref: 'master') @build = create(:ci_build_with_coverage, commit: @ci_commit) end step 'recent build is successful' do - @build.update_column(:status, 'success') + @build.update(status: 'success') end step 'recent build failed' do - @build.update_column(:status, 'failed') + @build.update(status: 'failed') end step 'project has another build that is running' do diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 1448c3f44cc1d191c221ea51263495eaff7716c4..e846c52d474ecdcfddf8d97d00cc92b957a43f60 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -227,7 +227,7 @@ module SharedDiffNote end def click_diff_line(code) - find("button[data-line-code='#{code}']").click + find("button[data-line-code='#{code}']").trigger('click') end def click_parallel_diff_line(code, line_type) diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index b6d70a26c2163076823223c2c454b40cb725ef92..a58b3cb7e160426195aa7e06a21da6e6ddb3e193 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -2,7 +2,7 @@ module SharedIssuable include Spinach::DSL def edit_issuable - find(:css, '.issuable-edit').click + find('.issuable-edit', visible: true).click end step 'project "Community" has "Community issue" open issue' do @@ -71,13 +71,16 @@ module SharedIssuable step 'I should not see any related merge requests' do page.within '.issue-details' do - expect(page).not_to have_content('.merge-requests') + expect(page).not_to have_content('#merge-requests .merge-requests-title') end end step 'I should see the "Enterprise fix" related merge request' do - page.within '.merge-requests' do + page.within '#merge-requests .merge-requests-title' do expect(page).to have_content('1 Related Merge Request') + end + + page.within '#merge-requests ul' do expect(page).to have_content('Enterprise fix') end end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index b13e82f276be493fcd63115b994db75f6b187c20..ea5f9580308e05962a45c852c34de597509a2235 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -230,7 +230,7 @@ module SharedProject step 'project "Shop" has CI build' do project = Project.find_by(name: "Shop") - create :ci_commit, project: project, sha: project.commit.sha + create :ci_commit, project: project, sha: project.commit.sha, ref: 'master', status: 'skipped' end step 'I should see last commit with CI status' do diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json index 18d6e93e0f427dd8c01ffef958ed740eb8f93056..41ca617847eff42a45fce759af898542a0a29f5a 100644 --- a/fixtures/emojis/digests.json +++ b/fixtures/emojis/digests.json @@ -64,21 +64,41 @@ "unicode": "1F6EA", "digest": "fdddc2cd3618ec6661612581b8b93553cb086b0bb197e96aedf1bee8055e7bb4" }, + { + "name": "northeast_pointing_airplane", + "unicode": "1F6EA", + "digest": "fdddc2cd3618ec6661612581b8b93553cb086b0bb197e96aedf1bee8055e7bb4" + }, { "name": "airplane_small", "unicode": "1F6E9", "digest": "f98b44422d6bf505b50330805ecf68013d035341f0b6487c3c05ad913eb5abd3" }, + { + "name": "small_airplane", + "unicode": "1F6E9", + "digest": "f98b44422d6bf505b50330805ecf68013d035341f0b6487c3c05ad913eb5abd3" + }, { "name": "airplane_small_up", "unicode": "1F6E8", "digest": "029752b29a757c087dec60f45ea242e974fc181129e20390d5d4a2f90442091a" }, + { + "name": "up_pointing_small_airplane", + "unicode": "1F6E8", + "digest": "029752b29a757c087dec60f45ea242e974fc181129e20390d5d4a2f90442091a" + }, { "name": "airplane_up", "unicode": "1F6E7", "digest": "ec45d4dbfce1f75dc59339417b1dcf5f1e1359cd9d04ff233babf359a3330e77" }, + { + "name": "up_pointing_airplane", + "unicode": "1F6E7", + "digest": "ec45d4dbfce1f75dc59339417b1dcf5f1e1359cd9d04ff233babf359a3330e77" + }, { "name": "alarm_clock", "unicode": "23F0", @@ -149,11 +169,21 @@ "unicode": "1F5EE", "digest": "f2711991e8b386b2d5b12f296ce20a9b4b00ef91d6d67af2cf4e06abf2faa1dc" }, + { + "name": "left_anger_bubble", + "unicode": "1F5EE", + "digest": "f2711991e8b386b2d5b12f296ce20a9b4b00ef91d6d67af2cf4e06abf2faa1dc" + }, { "name": "anger_right", "unicode": "1F5EF", "digest": "24b572d64c519251a3ae8844e8d66fd6955752aff99aebe7dc20179505a466c4" }, + { + "name": "right_anger_bubble", + "unicode": "1F5EF", + "digest": "24b572d64c519251a3ae8844e8d66fd6955752aff99aebe7dc20179505a466c4" + }, { "name": "angry", "unicode": "1F620", @@ -304,6 +334,11 @@ "unicode": "002A-20E3", "digest": "0b7f27f545b616677c83d40ff957337477b2881459b4d3c839ae55e23797419f" }, + { + "name": "keycap_asterisk", + "unicode": "002A-20E3", + "digest": "0b7f27f545b616677c83d40ff957337477b2881459b4d3c839ae55e23797419f" + }, { "name": "astonished", "unicode": "1F632", @@ -324,6 +359,11 @@ "unicode": "269B", "digest": "cbce1725602efbb77a935cfae5407e4d75489ee988910296c7f6140665afc669" }, + { + "name": "atom_symbol", + "unicode": "269B", + "digest": "cbce1725602efbb77a935cfae5407e4d75489ee988910296c7f6140665afc669" + }, { "name": "b", "unicode": "1F171", @@ -399,11 +439,21 @@ "unicode": "1F5F3", "digest": "0455ea75612efe78354315b4c345953d2d559bb471d5b01c1adc1d6b74ed693a" }, + { + "name": "ballot_box_with_ballot", + "unicode": "1F5F3", + "digest": "0455ea75612efe78354315b4c345953d2d559bb471d5b01c1adc1d6b74ed693a" + }, { "name": "ballot_box_check", "unicode": "1F5F9", "digest": "fc3ba16c009d963a4a0ea20a348ac98eee3c4c18c481df19a5ada0d1de7fcc15" }, + { + "name": "ballot_box_with_bold_check", + "unicode": "1F5F9", + "digest": "fc3ba16c009d963a4a0ea20a348ac98eee3c4c18c481df19a5ada0d1de7fcc15" + }, { "name": "ballot_box_with_check", "unicode": "2611", @@ -414,11 +464,21 @@ "unicode": "1F5F5", "digest": "861dcfc2361298262587b5d0e163fed96a55c44636361f5b4a9ab1d6502b8928" }, + { + "name": "ballot_box_with_script_x", + "unicode": "1F5F5", + "digest": "861dcfc2361298262587b5d0e163fed96a55c44636361f5b4a9ab1d6502b8928" + }, { "name": "ballot_x", "unicode": "1F5F4", "digest": "0b73b89847eb82bcad5664644c8af237e0aef6c3d8c94b7a5df94e05d0ebf4e1" }, + { + "name": "ballot_script_x", + "unicode": "1F5F4", + "digest": "0b73b89847eb82bcad5664644c8af237e0aef6c3d8c94b7a5df94e05d0ebf4e1" + }, { "name": "bamboo", "unicode": "1F38D", @@ -464,31 +524,61 @@ "unicode": "26F9", "digest": "e94beb69f631667479a80095bf313ceb3aa109d6ebb80f182722360a6d2a214e" }, + { + "name": "person_with_ball", + "unicode": "26F9", + "digest": "e94beb69f631667479a80095bf313ceb3aa109d6ebb80f182722360a6d2a214e" + }, { "name": "basketball_player_tone1", "unicode": "26F9-1F3FB", "digest": "6fc77cf2f26ee18e9a3faea500d4277839f77633f31ee618a68c301f1ad32d90" }, + { + "name": "person_with_ball_tone1", + "unicode": "26F9-1F3FB", + "digest": "6fc77cf2f26ee18e9a3faea500d4277839f77633f31ee618a68c301f1ad32d90" + }, { "name": "basketball_player_tone2", "unicode": "26F9-1F3FC", "digest": "6ee9060c24d92708e12a854fb0bdf5c717c90b8c0350d8aa40c278b41bfa12fc" }, + { + "name": "person_with_ball_tone2", + "unicode": "26F9-1F3FC", + "digest": "6ee9060c24d92708e12a854fb0bdf5c717c90b8c0350d8aa40c278b41bfa12fc" + }, { "name": "basketball_player_tone3", "unicode": "26F9-1F3FD", "digest": "752e90dbfa7c7a9ae3f37de924e22f3c3d5a7e54dd41c8e8eb99cabb0dad73cf" }, + { + "name": "person_with_ball_tone3", + "unicode": "26F9-1F3FD", + "digest": "752e90dbfa7c7a9ae3f37de924e22f3c3d5a7e54dd41c8e8eb99cabb0dad73cf" + }, { "name": "basketball_player_tone4", "unicode": "26F9-1F3FE", "digest": "38bedc3074e6243454d568d9b665f5764f1a3d983875651ce7a1cdb53da9f6c8" }, + { + "name": "person_with_ball_tone4", + "unicode": "26F9-1F3FE", + "digest": "38bedc3074e6243454d568d9b665f5764f1a3d983875651ce7a1cdb53da9f6c8" + }, { "name": "basketball_player_tone5", "unicode": "26F9-1F3FF", "digest": "25ee1e84670d3db96d3ad098c859abd6b3448f55f668ce0c195ee2337a215de7" }, + { + "name": "person_with_ball_tone5", + "unicode": "26F9-1F3FF", + "digest": "25ee1e84670d3db96d3ad098c859abd6b3448f55f668ce0c195ee2337a215de7" + }, { "name": "bath", "unicode": "1F6C0", @@ -534,11 +624,21 @@ "unicode": "1F3D6", "digest": "52855d75cfa4476ccc23c58b4afcb76ee48abb22a9a6081210c8accefdf33099" }, + { + "name": "beach_with_umbrella", + "unicode": "1F3D6", + "digest": "52855d75cfa4476ccc23c58b4afcb76ee48abb22a9a6081210c8accefdf33099" + }, { "name": "beach_umbrella", "unicode": "26F1", "digest": "cefe8e195d21d3e0769d3bfe15170db9e57c86db9d31cacb19fcdc8d2191b661" }, + { + "name": "umbrella_on_ground", + "unicode": "26F1", + "digest": "cefe8e195d21d3e0769d3bfe15170db9e57c86db9d31cacb19fcdc8d2191b661" + }, { "name": "bear", "unicode": "1F43B", @@ -584,6 +684,11 @@ "unicode": "1F6CE", "digest": "c15455f1b52ac26404b5c13a0e1070212ed1830026422873f4f6335e26e31259" }, + { + "name": "bellhop_bell", + "unicode": "1F6CE", + "digest": "c15455f1b52ac26404b5c13a0e1070212ed1830026422873f4f6335e26e31259" + }, { "name": "bento", "unicode": "1F371", @@ -634,6 +739,11 @@ "unicode": "2623", "digest": "81f8309318051255ed4dc18855a3cd3f8657a6f3b2d368caa531a57ce0e34235" }, + { + "name": "biohazard_sign", + "unicode": "2623", + "digest": "81f8309318051255ed4dc18855a3cd3f8657a6f3b2d368caa531a57ce0e34235" + }, { "name": "bird", "unicode": "1F426", @@ -769,6 +879,11 @@ "unicode": "1F395", "digest": "1643ec51ff26fc1ac0c67859e202386398650bf2a996c82b68e1b73fa52abf7d" }, + { + "name": "bouquet_of_flowers", + "unicode": "1F395", + "digest": "1643ec51ff26fc1ac0c67859e202386398650bf2a996c82b68e1b73fa52abf7d" + }, { "name": "bow", "unicode": "1F647", @@ -779,6 +894,11 @@ "unicode": "1F3F9", "digest": "1c23469256331ea4ff03c036f89f0e63ad3228c51faecba50129da99b7eaddf3" }, + { + "name": "archery", + "unicode": "1F3F9", + "digest": "1c23469256331ea4ff03c036f89f0e63ad3228c51faecba50129da99b7eaddf3" + }, { "name": "bow_tone1", "unicode": "1F647-1F3FB", @@ -924,6 +1044,11 @@ "unicode": "1F56C", "digest": "92493636cf086205d1e12cc19e613b84152ef10b8cd0215619a0fc813bfc9a7c" }, + { + "name": "bullhorn_with_sound_waves", + "unicode": "1F56C", + "digest": "92493636cf086205d1e12cc19e613b84152ef10b8cd0215619a0fc813bfc9a7c" + }, { "name": "burrito", "unicode": "1F32F", @@ -964,6 +1089,11 @@ "unicode": "1F5A9", "digest": "01b47b5c69c12b65fa4f4c0d580f2a98280d6116f4ad2cf8be378759008bcc3c" }, + { + "name": "pocket calculator", + "unicode": "1F5A9", + "digest": "01b47b5c69c12b65fa4f4c0d580f2a98280d6116f4ad2cf8be378759008bcc3c" + }, { "name": "calendar", "unicode": "1F4C6", @@ -974,6 +1104,11 @@ "unicode": "1F5D3", "digest": "1dd5da98bb435c0c3f632bc0a5c9fdde694de7aee752bf4bb85def086e788a2a" }, + { + "name": "spiral_calendar_pad", + "unicode": "1F5D3", + "digest": "1dd5da98bb435c0c3f632bc0a5c9fdde694de7aee752bf4bb85def086e788a2a" + }, { "name": "calling", "unicode": "1F4F2", @@ -1034,6 +1169,11 @@ "unicode": "1F5C3", "digest": "7d760ae1d44e6f4b2aac00895ca86b5743f8b5ca157ec2bd21ce2665e50ad23a" }, + { + "name": "card_file_box", + "unicode": "1F5C3", + "digest": "7d760ae1d44e6f4b2aac00895ca86b5743f8b5ca157ec2bd21ce2665e50ad23a" + }, { "name": "card_index", "unicode": "1F4C7", @@ -1049,6 +1189,11 @@ "unicode": "1F5AD", "digest": "0b1625eea118060b51a70905c1eb3313ed632e989f70943eca16aa29fe8a34f2" }, + { + "name": "tape_cartridge", + "unicode": "1F5AD", + "digest": "0b1625eea118060b51a70905c1eb3313ed632e989f70943eca16aa29fe8a34f2" + }, { "name": "cat", "unicode": "1F431", @@ -1079,6 +1224,11 @@ "unicode": "1F37E", "digest": "77395d3afe5cc10bfdc381120bae2ae4aefdaa96c529536413873a696c5fa713" }, + { + "name": "bottle_with_popping_cork", + "unicode": "1F37E", + "digest": "77395d3afe5cc10bfdc381120bae2ae4aefdaa96c529536413873a696c5fa713" + }, { "name": "chart", "unicode": "1F4B9", @@ -1104,6 +1254,11 @@ "unicode": "1F9C0", "digest": "5897036ba97b557868bb314fcee83b9d8a609c8447b270a0b3d34a29ce7496d1" }, + { + "name": "cheese_wedge", + "unicode": "1F9C0", + "digest": "5897036ba97b557868bb314fcee83b9d8a609c8447b270a0b3d34a29ce7496d1" + }, { "name": "cherries", "unicode": "1F352", @@ -1169,6 +1324,11 @@ "unicode": "1F307", "digest": "c2530d12204eb518c5a3c8d7deba11170b1412fdf406aea05a69d4c026210d1b" }, + { + "name": "city_sunrise", + "unicode": "1F307", + "digest": "c2530d12204eb518c5a3c8d7deba11170b1412fdf406aea05a69d4c026210d1b" + }, { "name": "cityscape", "unicode": "1F3D9", @@ -1229,6 +1389,11 @@ "unicode": "1F570", "digest": "c48314ccde8bf01acc2b1bc9a6b5aa7d796fc0c8769f80398bc74545fcef31ed" }, + { + "name": "mantlepiece_clock", + "unicode": "1F570", + "digest": "c48314ccde8bf01acc2b1bc9a6b5aa7d796fc0c8769f80398bc74545fcef31ed" + }, { "name": "clock1", "unicode": "1F550", @@ -1354,6 +1519,11 @@ "unicode": "1F5D8", "digest": "67027b7e1a4d800a3ce7d731c21c098d1109d217159a27665eebb7e080fc2622" }, + { + "name": "clockwise_right_and_left_semicircle_arrows", + "unicode": "1F5D8", + "digest": "67027b7e1a4d800a3ce7d731c21c098d1109d217159a27665eebb7e080fc2622" + }, { "name": "closed_book", "unicode": "1F4D5", @@ -1379,21 +1549,41 @@ "unicode": "1F329", "digest": "fc9c85cc95f9c456635692c974f72b6d40e14943824b8129a21c47265c3416f4" }, + { + "name": "cloud_with_lightning", + "unicode": "1F329", + "digest": "fc9c85cc95f9c456635692c974f72b6d40e14943824b8129a21c47265c3416f4" + }, { "name": "cloud_rain", "unicode": "1F327", "digest": "f4406e62ed98f6141ab70736f6d5c540023e805396db0346ee6b7082c3f5e8e2" }, + { + "name": "cloud_with_rain", + "unicode": "1F327", + "digest": "f4406e62ed98f6141ab70736f6d5c540023e805396db0346ee6b7082c3f5e8e2" + }, { "name": "cloud_snow", "unicode": "1F328", "digest": "948990cd13dd927917208c026089519fcf8e258a8a284684ace67c9a2f9a8149" }, + { + "name": "cloud_with_snow", + "unicode": "1F328", + "digest": "948990cd13dd927917208c026089519fcf8e258a8a284684ace67c9a2f9a8149" + }, { "name": "cloud_tornado", "unicode": "1F32A", "digest": "44753516d0bd05d47cfa6eb922aba570ba6a87f805f325772b2cff071460ead1" }, + { + "name": "cloud_with_tornado", + "unicode": "1F32A", + "digest": "44753516d0bd05d47cfa6eb922aba570ba6a87f805f325772b2cff071460ead1" + }, { "name": "clubs", "unicode": "2663", @@ -1439,6 +1629,11 @@ "unicode": "1F5B3", "digest": "b27c30d74f205a8a3bd00a55ca17da7cf6ae3b65ae33e949755a4c6bd69a9fd3" }, + { + "name": "old_personal_computer", + "unicode": "1F5B3", + "digest": "b27c30d74f205a8a3bd00a55ca17da7cf6ae3b65ae33e949755a4c6bd69a9fd3" + }, { "name": "confetti_ball", "unicode": "1F38A", @@ -1509,6 +1704,11 @@ "unicode": "1F3D7", "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9" }, + { + "name": "building_construction", + "unicode": "1F3D7", + "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9" + }, { "name": "convenience_store", "unicode": "1F3EA", @@ -1569,6 +1769,11 @@ "unicode": "1F6CB", "digest": "a93fffed194b404200495abda8772bb35539cfc8499eb0a9bf09c508afad6676" }, + { + "name": "couch_and_lamp", + "unicode": "1F6CB", + "digest": "a93fffed194b404200495abda8772bb35539cfc8499eb0a9bf09c508afad6676" + }, { "name": "couple", "unicode": "1F46B", @@ -1579,6 +1784,11 @@ "unicode": "1F468-2764-1F468", "digest": "3ae6fbf3ba168256ea85c756ac1e7b83fdb8b780d33f06128ed80706ff627eea" }, + { + "name": "couple_with_heart_mm", + "unicode": "1F468-2764-1F468", + "digest": "3ae6fbf3ba168256ea85c756ac1e7b83fdb8b780d33f06128ed80706ff627eea" + }, { "name": "couple_with_heart", "unicode": "1F491", @@ -1589,6 +1799,11 @@ "unicode": "1F469-2764-1F469", "digest": "d2a2ec29c1a1234ea0aa1d9fc6707cf8be8bb36ea8b92523ffa1c3071bcf0b06" }, + { + "name": "couple_with_heart_ww", + "unicode": "1F469-2764-1F469", + "digest": "d2a2ec29c1a1234ea0aa1d9fc6707cf8be8bb36ea8b92523ffa1c3071bcf0b06" + }, { "name": "couplekiss", "unicode": "1F48F", @@ -1614,6 +1829,11 @@ "unicode": "1F58D", "digest": "0f3351c2e68a8d47d27b45a9901be6160de0f9a291bd8680df84d0fc679bcb31" }, + { + "name": "lower_left_crayon", + "unicode": "1F58D", + "digest": "0f3351c2e68a8d47d27b45a9901be6160de0f9a291bd8680df84d0fc679bcb31" + }, { "name": "credit_card", "unicode": "1F4B3", @@ -1629,6 +1849,11 @@ "unicode": "1F3CF", "digest": "00eb11254e887c71db5e8945ad211e9e0280f1e02f4b77a4799b64bba2bbe9b3" }, + { + "name": "cricket_bat_ball", + "unicode": "1F3CF", + "digest": "00eb11254e887c71db5e8945ad211e9e0280f1e02f4b77a4799b64bba2bbe9b3" + }, { "name": "crocodile", "unicode": "1F40A", @@ -1639,21 +1864,41 @@ "unicode": "271D", "digest": "a6e3c345cf6aa2ce690b66454066b53ef5b1dab2ed635e21f1586b1dffc5df42" }, + { + "name": "latin_cross", + "unicode": "271D", + "digest": "a6e3c345cf6aa2ce690b66454066b53ef5b1dab2ed635e21f1586b1dffc5df42" + }, { "name": "cross_heavy", "unicode": "1F547", "digest": "2e37c26b9bad0beb019c7f3e7a3892352d0ad9ca1b90c4333d42e8d56680be70" }, + { + "name": "heavy_latin_cross", + "unicode": "1F547", + "digest": "2e37c26b9bad0beb019c7f3e7a3892352d0ad9ca1b90c4333d42e8d56680be70" + }, { "name": "cross_white", "unicode": "1F546", "digest": "3452e667010d7e49a51d7e1f4ba8ed4f303e33ed43255a051e9a18832a1efba6" }, + { + "name": "white_latin_cross", + "unicode": "1F546", + "digest": "3452e667010d7e49a51d7e1f4ba8ed4f303e33ed43255a051e9a18832a1efba6" + }, { "name": "crossbones", "unicode": "1F571", "digest": "f5e7ce293c1a3282711073e68f033a3876e8428d1218cb2f8294630f9124e584" }, + { + "name": "black_skull_and_crossbones", + "unicode": "1F571", + "digest": "f5e7ce293c1a3282711073e68f033a3876e8428d1218cb2f8294630f9124e584" + }, { "name": "crossed_flags", "unicode": "1F38C", @@ -1674,6 +1919,11 @@ "unicode": "1F6F3", "digest": "90519c46ddfb63e71bc76661953da9041e5f0b97e9f8a7a8696518b4d529f3dd" }, + { + "name": "passenger_ship", + "unicode": "1F6F3", + "digest": "90519c46ddfb63e71bc76661953da9041e5f0b97e9f8a7a8696518b4d529f3dd" + }, { "name": "cry", "unicode": "1F622", @@ -1729,6 +1979,11 @@ "unicode": "1F5E1", "digest": "377060a7ce930566a4732b361be98e8a193a546846dfbba2a00abeeef41d1976" }, + { + "name": "dagger_knife", + "unicode": "1F5E1", + "digest": "377060a7ce930566a4732b361be98e8a193a546846dfbba2a00abeeef41d1976" + }, { "name": "dancer", "unicode": "1F483", @@ -1814,6 +2069,11 @@ "unicode": "1F5A5", "digest": "ba46323e695918e7253f1013cb991efb09790581c74c07c38bc5e10a20b8e8de" }, + { + "name": "desktop_computer", + "unicode": "1F5A5", + "digest": "ba46323e695918e7253f1013cb991efb09790581c74c07c38bc5e10a20b8e8de" + }, { "name": "desktop_window", "unicode": "1F5D4", @@ -1844,6 +2104,11 @@ "unicode": "1F5C2", "digest": "bf4c303452a4c0b4986925041dbec5b7e478060d560630b7c5bc2f997fcad668" }, + { + "name": "card_index_dividers", + "unicode": "1F5C2", + "digest": "bf4c303452a4c0b4986925041dbec5b7e478060d560630b7c5bc2f997fcad668" + }, { "name": "dizzy", "unicode": "1F4AB", @@ -1869,6 +2134,11 @@ "unicode": "1F5B9", "digest": "29407b12409c9673f3d89ef1f86ee50cbc7ed53b1870e33b4a29bbc609017f72" }, + { + "name": "document_with_text", + "unicode": "1F5B9", + "digest": "29407b12409c9673f3d89ef1f86ee50cbc7ed53b1870e33b4a29bbc609017f72" + }, { "name": "dog", "unicode": "1F436", @@ -1909,6 +2179,11 @@ "unicode": "1F54A", "digest": "4e2e9c47e5632efe6ccf945d61dbc2f1155a2e52905e17f307b502a2c951bdb8" }, + { + "name": "dove_of_peace", + "unicode": "1F54A", + "digest": "4e2e9c47e5632efe6ccf945d61dbc2f1155a2e52905e17f307b502a2c951bdb8" + }, { "name": "dragon", "unicode": "1F409", @@ -1944,6 +2219,11 @@ "unicode": "1F4E7", "digest": "12135310cfedc091d120426f5b132df82b538c5fcad458bf6b21588f353c3adb" }, + { + "name": "email", + "unicode": "1F4E7", + "digest": "12135310cfedc091d120426f5b132df82b538c5fcad458bf6b21588f353c3adb" + }, { "name": "ear", "unicode": "1F442", @@ -2044,21 +2324,41 @@ "unicode": "1F582", "digest": "bc60b6d375feee00758a94a05b42eeb165f4084b20eb3e6012b72faa221f7e75" }, + { + "name": "back_of_envelope", + "unicode": "1F582", + "digest": "bc60b6d375feee00758a94a05b42eeb165f4084b20eb3e6012b72faa221f7e75" + }, { "name": "envelope_flying", "unicode": "1F585", "digest": "9d6b6ca4c08006062a6f11948de3e15b13cf5c458967e39a9358665a8e13e214" }, + { + "name": "flying_envelope", + "unicode": "1F585", + "digest": "9d6b6ca4c08006062a6f11948de3e15b13cf5c458967e39a9358665a8e13e214" + }, { "name": "envelope_stamped", "unicode": "1F583", "digest": "f6102aea7283ddc136bfeb09589573420b9279105045fc6b965c1633c1297468" }, + { + "name": "stamped_envelope", + "unicode": "1F583", + "digest": "f6102aea7283ddc136bfeb09589573420b9279105045fc6b965c1633c1297468" + }, { "name": "envelope_stamped_pen", "unicode": "1F586", "digest": "80ea471318d1e04f8e525ff236b3cd4a4c864e66c6246b6aad77d92f56895f33" }, + { + "name": "pen_over_stamped_envelope", + "unicode": "1F586", + "digest": "80ea471318d1e04f8e525ff236b3cd4a4c864e66c6246b6aad77d92f56895f33" + }, { "name": "envelope_with_arrow", "unicode": "1F4E9", @@ -2254,31 +2554,61 @@ "unicode": "1F597", "digest": "0c542ac3141e8f2e74767acd0eb399c2d68c779cb78bf16d437ad3b1f8134ad9" }, + { + "name": "white_down_pointing_left_hand_index", + "unicode": "1F597", + "digest": "0c542ac3141e8f2e74767acd0eb399c2d68c779cb78bf16d437ad3b1f8134ad9" + }, { "name": "finger_pointing_down2", "unicode": "1F59F", "digest": "c5b128a232cbf518544802a2ae1459368274297163721fa05d0103cf95b2b1ee" }, + { + "name": "sideways_white_down_pointing_index", + "unicode": "1F59F", + "digest": "c5b128a232cbf518544802a2ae1459368274297163721fa05d0103cf95b2b1ee" + }, { "name": "finger_pointing_left", "unicode": "1F598", "digest": "d178ece691e2091be08db77fda9cf05462934628557358a8cb6222587b291f7e" }, + { + "name": "sideways_white_left_pointing_index", + "unicode": "1F598", + "digest": "d178ece691e2091be08db77fda9cf05462934628557358a8cb6222587b291f7e" + }, { "name": "finger_pointing_right", "unicode": "1F599", "digest": "a412a47544d8f401f9181f8826c5fa3d6b42a1d76f6926963c2d9cd2a01be06d" }, + { + "name": "sideways_white_right_pointing_index", + "unicode": "1F599", + "digest": "a412a47544d8f401f9181f8826c5fa3d6b42a1d76f6926963c2d9cd2a01be06d" + }, { "name": "finger_pointing_up", "unicode": "1F59E", "digest": "32c2ccab52aa318a47c816d1bcf9c076e667c9ef3e64ce37d7ba7e827238690d" }, + { + "name": "sideways_white_up_pointing_index", + "unicode": "1F59E", + "digest": "32c2ccab52aa318a47c816d1bcf9c076e667c9ef3e64ce37d7ba7e827238690d" + }, { "name": "fire", "unicode": "1F525", "digest": "b44311874681135acbb5e7226febe4365c732da3a9617f10d7074a3b1ade1641" }, + { + "name": "flame", + "unicode": "1F525", + "digest": "b44311874681135acbb5e7226febe4365c732da3a9617f10d7074a3b1ade1641" + }, { "name": "fire_engine", "unicode": "1F692", @@ -2289,6 +2619,11 @@ "unicode": "1F6F1", "digest": "e2482c450136d373f74dfafddf502e0b675eb5d2e1e1c645f163db0e4d15fbb6" }, + { + "name": "oncoming_fire_engine", + "unicode": "1F6F1", + "digest": "e2482c450136d373f74dfafddf502e0b675eb5d2e1e1c645f163db0e4d15fbb6" + }, { "name": "fireworks", "unicode": "1F386", @@ -2359,1188 +2694,2383 @@ "unicode": "1F1E6-1F1E8", "digest": "d9db1edeb709824a1083c2bba79ca5f683ed0edded35918bb167d1ee7396c8da" }, + { + "name": "ac", + "unicode": "1F1E6-1F1E8", + "digest": "d9db1edeb709824a1083c2bba79ca5f683ed0edded35918bb167d1ee7396c8da" + }, { "name": "flag_ad", "unicode": "1F1E6-1F1E9", "digest": "04a8c1745d9b8b20e903302379f2557e8082f72e33878db4cb2cd6b33eb97952" }, + { + "name": "ad", + "unicode": "1F1E6-1F1E9", + "digest": "04a8c1745d9b8b20e903302379f2557e8082f72e33878db4cb2cd6b33eb97952" + }, { "name": "flag_ae", "unicode": "1F1E6-1F1EA", "digest": "868324ac2e7bea1547f5de95f39633b77b8d62f3b3433b3d1a4ee96d169a09cd" }, + { + "name": "ae", + "unicode": "1F1E6-1F1EA", + "digest": "868324ac2e7bea1547f5de95f39633b77b8d62f3b3433b3d1a4ee96d169a09cd" + }, { "name": "flag_af", "unicode": "1F1E6-1F1EB", "digest": "9a94458519e9db5d6cf1557e54fdf62d7e48aaf7de25744a093ec8f284656226" }, + { + "name": "af", + "unicode": "1F1E6-1F1EB", + "digest": "9a94458519e9db5d6cf1557e54fdf62d7e48aaf7de25744a093ec8f284656226" + }, { "name": "flag_ag", "unicode": "1F1E6-1F1EC", "digest": "ea59fabc2bd9024df06a59a34412f52bebfeb03eb6abd73d8fe153e3a68e28f4" }, + { + "name": "ag", + "unicode": "1F1E6-1F1EC", + "digest": "ea59fabc2bd9024df06a59a34412f52bebfeb03eb6abd73d8fe153e3a68e28f4" + }, { "name": "flag_ai", "unicode": "1F1E6-1F1EE", "digest": "75676ded736ad2ebb921e9fd8ebfef49819a35c3dcf005bbc3b7e8c6e75178f2" }, + { + "name": "ai", + "unicode": "1F1E6-1F1EE", + "digest": "75676ded736ad2ebb921e9fd8ebfef49819a35c3dcf005bbc3b7e8c6e75178f2" + }, { "name": "flag_al", "unicode": "1F1E6-1F1F1", "digest": "77b835dcff399b609e2479cbf10f08344c8fc277370ba8e4540165ca15563847" }, + { + "name": "al", + "unicode": "1F1E6-1F1F1", + "digest": "77b835dcff399b609e2479cbf10f08344c8fc277370ba8e4540165ca15563847" + }, { "name": "flag_am", "unicode": "1F1E6-1F1F2", "digest": "3b820c628dd5a93137f7288a43553778f60b0beea4c0a239d063893c0723e73d" }, + { + "name": "am", + "unicode": "1F1E6-1F1F2", + "digest": "3b820c628dd5a93137f7288a43553778f60b0beea4c0a239d063893c0723e73d" + }, { "name": "flag_ao", "unicode": "1F1E6-1F1F4", "digest": "d26439d4ecbe8b67bb1ae9753454505358ebb6b802624f19800471e53ee27187" }, + { + "name": "ao", + "unicode": "1F1E6-1F1F4", + "digest": "d26439d4ecbe8b67bb1ae9753454505358ebb6b802624f19800471e53ee27187" + }, { "name": "flag_aq", "unicode": "1F1E6-1F1F6", "digest": "6b0b4e800d88ab289ae4b6d449bfa115e92543958b477d13ad348468a74e4616" }, + { + "name": "aq", + "unicode": "1F1E6-1F1F6", + "digest": "6b0b4e800d88ab289ae4b6d449bfa115e92543958b477d13ad348468a74e4616" + }, { "name": "flag_ar", "unicode": "1F1E6-1F1F7", "digest": "ca76db601dd3f5794f1caace8ab5641fe3786b86e4ae030706162f0ce07d27b3" }, + { + "name": "ar", + "unicode": "1F1E6-1F1F7", + "digest": "ca76db601dd3f5794f1caace8ab5641fe3786b86e4ae030706162f0ce07d27b3" + }, { "name": "flag_as", "unicode": "1F1E6-1F1F8", "digest": "170e1dde0e3fd2e0f2149de5cc8845e15580cc0412e81a643d61bd387de16141" }, + { + "name": "as", + "unicode": "1F1E6-1F1F8", + "digest": "170e1dde0e3fd2e0f2149de5cc8845e15580cc0412e81a643d61bd387de16141" + }, { "name": "flag_at", "unicode": "1F1E6-1F1F9", "digest": "0ab3675a16b4988e87c81e87453c160d6616c7be76247f54c471dc63aa8b42ba" }, + { + "name": "at", + "unicode": "1F1E6-1F1F9", + "digest": "0ab3675a16b4988e87c81e87453c160d6616c7be76247f54c471dc63aa8b42ba" + }, { "name": "flag_au", "unicode": "1F1E6-1F1FA", "digest": "b6f17d3dfd3547c069a0b6cddd4cf44fb8ce1d1d300e24284fb292ac142537e3" }, + { + "name": "au", + "unicode": "1F1E6-1F1FA", + "digest": "b6f17d3dfd3547c069a0b6cddd4cf44fb8ce1d1d300e24284fb292ac142537e3" + }, { "name": "flag_aw", "unicode": "1F1E6-1F1FC", "digest": "7857bc907f04dfb7ccc4401c05034ad8afb6383a022db77973cfcafa4d6c16c8" }, + { + "name": "aw", + "unicode": "1F1E6-1F1FC", + "digest": "7857bc907f04dfb7ccc4401c05034ad8afb6383a022db77973cfcafa4d6c16c8" + }, { "name": "flag_ax", "unicode": "1F1E6-1F1FD", "digest": "ab8f1fd4af7c220a54d478cec5a9f7f3beb5fc83439c448f3ac9848af8391ac1" }, + { + "name": "ax", + "unicode": "1F1E6-1F1FD", + "digest": "ab8f1fd4af7c220a54d478cec5a9f7f3beb5fc83439c448f3ac9848af8391ac1" + }, { "name": "flag_az", "unicode": "1F1E6-1F1FF", "digest": "187cc7b6d39800c5910a34409db1e6b1d8aac808c72a93e922a419d9b054fd0b" }, + { + "name": "az", + "unicode": "1F1E6-1F1FF", + "digest": "187cc7b6d39800c5910a34409db1e6b1d8aac808c72a93e922a419d9b054fd0b" + }, { "name": "flag_ba", "unicode": "1F1E7-1F1E6", "digest": "cd22c744213087384cf79ed314742026787212c9ceb6999ed166534670f7864a" }, + { + "name": "ba", + "unicode": "1F1E7-1F1E6", + "digest": "cd22c744213087384cf79ed314742026787212c9ceb6999ed166534670f7864a" + }, { "name": "flag_bb", "unicode": "1F1E7-1F1E7", "digest": "44ff0a48ac2d2180374baa58b1b7c64f26d0d151a48811eb08ffa20758104512" }, + { + "name": "bb", + "unicode": "1F1E7-1F1E7", + "digest": "44ff0a48ac2d2180374baa58b1b7c64f26d0d151a48811eb08ffa20758104512" + }, { "name": "flag_bd", "unicode": "1F1E7-1F1E9", "digest": "c18793d2b963458607a0bab94c57e62c8278fce870e96fd8dda78067a8fbde18" }, + { + "name": "bd", + "unicode": "1F1E7-1F1E9", + "digest": "c18793d2b963458607a0bab94c57e62c8278fce870e96fd8dda78067a8fbde18" + }, { "name": "flag_be", "unicode": "1F1E7-1F1EA", "digest": "6e6ccfca064a43b93c8acc04a9425f95af204198022ca20b9ee6c491e99ad950" }, + { + "name": "be", + "unicode": "1F1E7-1F1EA", + "digest": "6e6ccfca064a43b93c8acc04a9425f95af204198022ca20b9ee6c491e99ad950" + }, { "name": "flag_bf", "unicode": "1F1E7-1F1EB", "digest": "d69c0394a1c7cb6323f54f024b7d740c728f229ca5e1b54ac374d5024f5470a5" }, + { + "name": "bf", + "unicode": "1F1E7-1F1EB", + "digest": "d69c0394a1c7cb6323f54f024b7d740c728f229ca5e1b54ac374d5024f5470a5" + }, { "name": "flag_bg", "unicode": "1F1E7-1F1EC", "digest": "413a270caf4a9155e84bdba6c9512277f5642246f6ba8d701383a5eeb02f7e95" }, + { + "name": "bg", + "unicode": "1F1E7-1F1EC", + "digest": "413a270caf4a9155e84bdba6c9512277f5642246f6ba8d701383a5eeb02f7e95" + }, { "name": "flag_bh", "unicode": "1F1E7-1F1ED", "digest": "9243ed65d7f24c824c2a3207335a2d4ad25251258547c16d0b7b7cbb9df6f8de" }, + { + "name": "bh", + "unicode": "1F1E7-1F1ED", + "digest": "9243ed65d7f24c824c2a3207335a2d4ad25251258547c16d0b7b7cbb9df6f8de" + }, { "name": "flag_bi", "unicode": "1F1E7-1F1EE", "digest": "63056519030524b2d2dcd47448267d817205dbd6b98075c97f011a8f1d4d1a4b" }, + { + "name": "bi", + "unicode": "1F1E7-1F1EE", + "digest": "63056519030524b2d2dcd47448267d817205dbd6b98075c97f011a8f1d4d1a4b" + }, { "name": "flag_bj", "unicode": "1F1E7-1F1EF", "digest": "93b245eed85d22260d27d1a8c77f51fb3439309e09b2aeca6cd504dbea77b509" }, + { + "name": "bj", + "unicode": "1F1E7-1F1EF", + "digest": "93b245eed85d22260d27d1a8c77f51fb3439309e09b2aeca6cd504dbea77b509" + }, { "name": "flag_bl", "unicode": "1F1E7-1F1F1", "digest": "5e1e478deaf02bbaa26595e4cefc5f5c9bec6105ce521b7b9ab4fa5e7a452c14" }, + { + "name": "bl", + "unicode": "1F1E7-1F1F1", + "digest": "5e1e478deaf02bbaa26595e4cefc5f5c9bec6105ce521b7b9ab4fa5e7a452c14" + }, { "name": "flag_black", "unicode": "1F3F4", "digest": "df131e5c28e9f51dea53fe7f33551f91d420f7d686b7a62980f0154c6b5357a6" }, + { + "name": "waving_black_flag", + "unicode": "1F3F4", + "digest": "df131e5c28e9f51dea53fe7f33551f91d420f7d686b7a62980f0154c6b5357a6" + }, { "name": "flag_bm", "unicode": "1F1E7-1F1F2", "digest": "9dcd9e60faebe7f93eb19157e99f2ad654a8145c61738de96e6ecd11a246764a" }, + { + "name": "bm", + "unicode": "1F1E7-1F1F2", + "digest": "9dcd9e60faebe7f93eb19157e99f2ad654a8145c61738de96e6ecd11a246764a" + }, { "name": "flag_bn", "unicode": "1F1E7-1F1F3", "digest": "078af6ca481a77871ba005e251a46ce63951c27b1b0cd33b9c1d0d31d349bc1a" }, + { + "name": "bn", + "unicode": "1F1E7-1F1F3", + "digest": "078af6ca481a77871ba005e251a46ce63951c27b1b0cd33b9c1d0d31d349bc1a" + }, { "name": "flag_bo", "unicode": "1F1E7-1F1F4", "digest": "92516d04e922a3bcbabe2e7619194bc972c09ba97576e8155f9829c397a71d8c" }, + { + "name": "bo", + "unicode": "1F1E7-1F1F4", + "digest": "92516d04e922a3bcbabe2e7619194bc972c09ba97576e8155f9829c397a71d8c" + }, { "name": "flag_bq", "unicode": "1F1E7-1F1F6", "digest": "7832df5267a2bb8dddb83aeb11162ce79aeebdb718f2ac0e54adcf3d87936171" }, + { + "name": "bq", + "unicode": "1F1E7-1F1F6", + "digest": "7832df5267a2bb8dddb83aeb11162ce79aeebdb718f2ac0e54adcf3d87936171" + }, { "name": "flag_br", "unicode": "1F1E7-1F1F7", "digest": "aabcc1c082124045ed214f7d9778d8e2ed791ebb8433defea91db458658abeec" }, + { + "name": "br", + "unicode": "1F1E7-1F1F7", + "digest": "aabcc1c082124045ed214f7d9778d8e2ed791ebb8433defea91db458658abeec" + }, { "name": "flag_bs", "unicode": "1F1E7-1F1F8", "digest": "f628f39003608e181696634929522884165e27ccef55270293f92eeef991635f" }, + { + "name": "bs", + "unicode": "1F1E7-1F1F8", + "digest": "f628f39003608e181696634929522884165e27ccef55270293f92eeef991635f" + }, { "name": "flag_bt", "unicode": "1F1E7-1F1F9", "digest": "af24a8ab34815da04c3e5af49a47449e0de93b068957cbda695816d0f830ca12" }, + { + "name": "bt", + "unicode": "1F1E7-1F1F9", + "digest": "af24a8ab34815da04c3e5af49a47449e0de93b068957cbda695816d0f830ca12" + }, { "name": "flag_bv", "unicode": "1F1E7-1F1FB", "digest": "ff0037f6eed95d4bb5f2b502902360e1ff41426e2896daf3e0730cef1f8f7e41" }, + { + "name": "bv", + "unicode": "1F1E7-1F1FB", + "digest": "ff0037f6eed95d4bb5f2b502902360e1ff41426e2896daf3e0730cef1f8f7e41" + }, { "name": "flag_bw", "unicode": "1F1E7-1F1FC", "digest": "3e3241ecb97946cc3e467b083d113a57dd305595e1512d4da18cc403e8689c1d" }, + { + "name": "bw", + "unicode": "1F1E7-1F1FC", + "digest": "3e3241ecb97946cc3e467b083d113a57dd305595e1512d4da18cc403e8689c1d" + }, { "name": "flag_by", "unicode": "1F1E7-1F1FE", "digest": "bdd21885c6fac475241884a44149b887297772e17617ee59dd9fe8518d52cf3d" }, + { + "name": "by", + "unicode": "1F1E7-1F1FE", + "digest": "bdd21885c6fac475241884a44149b887297772e17617ee59dd9fe8518d52cf3d" + }, { "name": "flag_bz", "unicode": "1F1E7-1F1FF", "digest": "21c16e1da641af004576000bf1db44b9a1e0fccfddc775e96022721c2f18eeea" }, + { + "name": "bz", + "unicode": "1F1E7-1F1FF", + "digest": "21c16e1da641af004576000bf1db44b9a1e0fccfddc775e96022721c2f18eeea" + }, { "name": "flag_ca", "unicode": "1F1E8-1F1E6", "digest": "0d00e459084d58d3ea9c60488a9e51bf45f71b77f1600f190225d5ca6ca6c796" }, + { + "name": "ca", + "unicode": "1F1E8-1F1E6", + "digest": "0d00e459084d58d3ea9c60488a9e51bf45f71b77f1600f190225d5ca6ca6c796" + }, { "name": "flag_cc", "unicode": "1F1E8-1F1E8", "digest": "86ab27164603ef0f1f83fe898eda6fbb7bc5709f2518f5577f00817860806a7b" }, + { + "name": "cc", + "unicode": "1F1E8-1F1E8", + "digest": "86ab27164603ef0f1f83fe898eda6fbb7bc5709f2518f5577f00817860806a7b" + }, { "name": "flag_cd", "unicode": "1F1E8-1F1E9", "digest": "fdc2796530ada4bd0bae37ace4bbe707b321b287dcd64568f8e01d3a9df56066" }, + { + "name": "congo", + "unicode": "1F1E8-1F1E9", + "digest": "fdc2796530ada4bd0bae37ace4bbe707b321b287dcd64568f8e01d3a9df56066" + }, { "name": "flag_cf", "unicode": "1F1E8-1F1EB", "digest": "5943bec02bede0931e21e7c34a68f375499f60a34883cc1edf2f21e9834b15ce" }, + { + "name": "cf", + "unicode": "1F1E8-1F1EB", + "digest": "5943bec02bede0931e21e7c34a68f375499f60a34883cc1edf2f21e9834b15ce" + }, { "name": "flag_cg", "unicode": "1F1E8-1F1EC", "digest": "54498482e2772371e148e05cfb7c5eb55f6a22cd528662abdea10bad47d157da" }, + { + "name": "cg", + "unicode": "1F1E8-1F1EC", + "digest": "54498482e2772371e148e05cfb7c5eb55f6a22cd528662abdea10bad47d157da" + }, { "name": "flag_ch", "unicode": "1F1E8-1F1ED", "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386" }, + { + "name": "ch", + "unicode": "1F1E8-1F1ED", + "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386" + }, { "name": "flag_ci", "unicode": "1F1E8-1F1EE", "digest": "3a173a3058a5c0174dc88750852cafec264e901ce82a6c69db122c8c0ea71a3a" }, + { + "name": "ci", + "unicode": "1F1E8-1F1EE", + "digest": "3a173a3058a5c0174dc88750852cafec264e901ce82a6c69db122c8c0ea71a3a" + }, { "name": "flag_ck", "unicode": "1F1E8-1F1F0", "digest": "42f395ff53c618b72b8a224cd4343d1a32f5ad82ced56bf590170a5ff0d5134c" }, + { + "name": "ck", + "unicode": "1F1E8-1F1F0", + "digest": "42f395ff53c618b72b8a224cd4343d1a32f5ad82ced56bf590170a5ff0d5134c" + }, { "name": "flag_cl", "unicode": "1F1E8-1F1F1", "digest": "9d6255feb690596904d800e72d5acdb5cda941c5a741b031ea39a3c7650ac46f" }, + { + "name": "chile", + "unicode": "1F1E8-1F1F1", + "digest": "9d6255feb690596904d800e72d5acdb5cda941c5a741b031ea39a3c7650ac46f" + }, { "name": "flag_cm", "unicode": "1F1E8-1F1F2", "digest": "ffc99d14e0a8b46a980331090ed9f36f31a87f1b0f8dd8c09007a31c6127c69e" }, + { + "name": "cm", + "unicode": "1F1E8-1F1F2", + "digest": "ffc99d14e0a8b46a980331090ed9f36f31a87f1b0f8dd8c09007a31c6127c69e" + }, { "name": "flag_cn", "unicode": "1F1E8-1F1F3", "digest": "869a98c52bdc33591f87e2aab6cb4f13e98bb19136250ff25805d0312a8b7c8a" }, + { + "name": "cn", + "unicode": "1F1E8-1F1F3", + "digest": "869a98c52bdc33591f87e2aab6cb4f13e98bb19136250ff25805d0312a8b7c8a" + }, { "name": "flag_co", "unicode": "1F1E8-1F1F4", "digest": "6aa458440eb2500ad307fea40fd8f1171a1506a6e32af144a4fd51545bb56151" }, + { + "name": "co", + "unicode": "1F1E8-1F1F4", + "digest": "6aa458440eb2500ad307fea40fd8f1171a1506a6e32af144a4fd51545bb56151" + }, { "name": "flag_cp", "unicode": "1F1E8-1F1F5", "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" }, + { + "name": "cp", + "unicode": "1F1E8-1F1F5", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, { "name": "flag_cr", "unicode": "1F1E8-1F1F7", "digest": "0f3b54d8330c5bb136647547dafc598bda755697cfd6b7d872a2443ba7b5cad4" }, + { + "name": "cr", + "unicode": "1F1E8-1F1F7", + "digest": "0f3b54d8330c5bb136647547dafc598bda755697cfd6b7d872a2443ba7b5cad4" + }, { "name": "flag_cu", "unicode": "1F1E8-1F1FA", "digest": "69bc973002475bb3d9b54cb0ba9ec9cb85f144c1cf54689da0ee8f414ebb0d83" }, + { + "name": "cu", + "unicode": "1F1E8-1F1FA", + "digest": "69bc973002475bb3d9b54cb0ba9ec9cb85f144c1cf54689da0ee8f414ebb0d83" + }, { "name": "flag_cv", "unicode": "1F1E8-1F1FB", "digest": "af2e135cf3c1b03a5937c068a75061b5cd332e95902fd0f8dffb2ac2dc89692a" }, + { + "name": "cv", + "unicode": "1F1E8-1F1FB", + "digest": "af2e135cf3c1b03a5937c068a75061b5cd332e95902fd0f8dffb2ac2dc89692a" + }, { "name": "flag_cw", "unicode": "1F1E8-1F1FC", "digest": "df4b2228a82f766c5c64c13c1388482a68549e59dd843671ee0eb43506e33411" }, + { + "name": "cw", + "unicode": "1F1E8-1F1FC", + "digest": "df4b2228a82f766c5c64c13c1388482a68549e59dd843671ee0eb43506e33411" + }, { "name": "flag_cx", "unicode": "1F1E8-1F1FD", "digest": "db12e513345a7be53954167d359ede0b3effbfb292508ee4d726123e3a8f83d7" }, + { + "name": "cx", + "unicode": "1F1E8-1F1FD", + "digest": "db12e513345a7be53954167d359ede0b3effbfb292508ee4d726123e3a8f83d7" + }, { "name": "flag_cy", "unicode": "1F1E8-1F1FE", "digest": "0cea41d4820746e2c6eb408f7ec7419afba9f7396401d92e6c1d77382f721d0b" }, + { + "name": "cy", + "unicode": "1F1E8-1F1FE", + "digest": "0cea41d4820746e2c6eb408f7ec7419afba9f7396401d92e6c1d77382f721d0b" + }, { "name": "flag_cz", "unicode": "1F1E8-1F1FF", "digest": "a1c2405916963be306f761539123486a2845af53716c9dfe94ad5420e14d36c4" }, + { + "name": "cz", + "unicode": "1F1E8-1F1FF", + "digest": "a1c2405916963be306f761539123486a2845af53716c9dfe94ad5420e14d36c4" + }, { "name": "flag_de", "unicode": "1F1E9-1F1EA", "digest": "74a80b64437bc4e31bdd7cbb753ecd2d719bf34c506cbac535db83a644174cce" }, + { + "name": "de", + "unicode": "1F1E9-1F1EA", + "digest": "74a80b64437bc4e31bdd7cbb753ecd2d719bf34c506cbac535db83a644174cce" + }, { "name": "flag_dg", "unicode": "1F1E9-1F1EC", "digest": "13cb5ea872f94a9c3fb579cef417e2d1ed38e8cbe95059576380cacd59bc4b9d" }, + { + "name": "dg", + "unicode": "1F1E9-1F1EC", + "digest": "13cb5ea872f94a9c3fb579cef417e2d1ed38e8cbe95059576380cacd59bc4b9d" + }, { "name": "flag_dj", "unicode": "1F1E9-1F1EF", "digest": "5b479654c28d3eeb70055c5e25dc46ccaba9eeea7537cc45ca9dbb8186b743b6" }, + { + "name": "dj", + "unicode": "1F1E9-1F1EF", + "digest": "5b479654c28d3eeb70055c5e25dc46ccaba9eeea7537cc45ca9dbb8186b743b6" + }, { "name": "flag_dk", "unicode": "1F1E9-1F1F0", "digest": "dee7fa9644a9b447417518a353e7edcbb37b2af8bc7d13a6ed71d7210c43ca3c" }, + { + "name": "dk", + "unicode": "1F1E9-1F1F0", + "digest": "dee7fa9644a9b447417518a353e7edcbb37b2af8bc7d13a6ed71d7210c43ca3c" + }, { "name": "flag_dm", "unicode": "1F1E9-1F1F2", "digest": "2e339190a8a0a238140f42e329f6646af5be75763a787ea268488a2e0440dc4c" }, + { + "name": "dm", + "unicode": "1F1E9-1F1F2", + "digest": "2e339190a8a0a238140f42e329f6646af5be75763a787ea268488a2e0440dc4c" + }, { "name": "flag_do", "unicode": "1F1E9-1F1F4", "digest": "be5dafcd32d7197a96d37299a91835a8009299452f05a66d91c5fdec17448230" }, + { + "name": "do", + "unicode": "1F1E9-1F1F4", + "digest": "be5dafcd32d7197a96d37299a91835a8009299452f05a66d91c5fdec17448230" + }, { "name": "flag_dz", "unicode": "1F1E9-1F1FF", "digest": "cf525d56bac45fe689f92d441274fc0ecbed4f95591d2c066598f72b1ee8d618" }, + { + "name": "dz", + "unicode": "1F1E9-1F1FF", + "digest": "cf525d56bac45fe689f92d441274fc0ecbed4f95591d2c066598f72b1ee8d618" + }, { "name": "flag_ea", "unicode": "1F1EA-1F1E6", "digest": "1acb13950f7c3692f9a36e618d8ec10a73ead5d7fa80fb52b6b2a18e3d456002" }, + { + "name": "ea", + "unicode": "1F1EA-1F1E6", + "digest": "1acb13950f7c3692f9a36e618d8ec10a73ead5d7fa80fb52b6b2a18e3d456002" + }, { "name": "flag_ec", "unicode": "1F1EA-1F1E8", "digest": "4d9d35450efc6026651ccc2278e70fb90b001ca5e5eecd31361b1e4e23253dbd" }, + { + "name": "ec", + "unicode": "1F1EA-1F1E8", + "digest": "4d9d35450efc6026651ccc2278e70fb90b001ca5e5eecd31361b1e4e23253dbd" + }, { "name": "flag_ee", "unicode": "1F1EA-1F1EA", "digest": "86ec7b2f618fe71dddec3d5a621b56b878d683780f1e0ad446f965326d42df48" }, + { + "name": "ee", + "unicode": "1F1EA-1F1EA", + "digest": "86ec7b2f618fe71dddec3d5a621b56b878d683780f1e0ad446f965326d42df48" + }, { "name": "flag_eg", "unicode": "1F1EA-1F1EC", "digest": "f06d36a6fec15af4c1a76de30e8469847dde2728bb5a48956b4e466098b778a4" }, + { + "name": "eg", + "unicode": "1F1EA-1F1EC", + "digest": "f06d36a6fec15af4c1a76de30e8469847dde2728bb5a48956b4e466098b778a4" + }, { "name": "flag_eh", "unicode": "1F1EA-1F1ED", "digest": "eb63f5b92c62c98dc008dfa7ad8830aa17fa23964f812a28055bd8b6f5960c5b" }, + { + "name": "eh", + "unicode": "1F1EA-1F1ED", + "digest": "eb63f5b92c62c98dc008dfa7ad8830aa17fa23964f812a28055bd8b6f5960c5b" + }, { "name": "flag_er", "unicode": "1F1EA-1F1F7", "digest": "e901195f7b37b22a6872d36713de0ec176f6424c209e261e5c849ce318c772f6" }, + { + "name": "er", + "unicode": "1F1EA-1F1F7", + "digest": "e901195f7b37b22a6872d36713de0ec176f6424c209e261e5c849ce318c772f6" + }, { "name": "flag_es", "unicode": "1F1EA-1F1F8", "digest": "27ab5cc6c2e9f26ccdfa632887533eebcd9b514f80cec9e721cf8e5e2544339c" }, + { + "name": "es", + "unicode": "1F1EA-1F1F8", + "digest": "27ab5cc6c2e9f26ccdfa632887533eebcd9b514f80cec9e721cf8e5e2544339c" + }, { "name": "flag_et", "unicode": "1F1EA-1F1F9", "digest": "6cdb3718c9b3ec713258dd36781db58b7da53f3017445056c1a76233e3b4a7de" }, + { + "name": "et", + "unicode": "1F1EA-1F1F9", + "digest": "6cdb3718c9b3ec713258dd36781db58b7da53f3017445056c1a76233e3b4a7de" + }, { "name": "flag_eu", "unicode": "1F1EA-1F1FA", "digest": "363f60e8a747166d5cec8d70bfdf266411eec2ff07933b6187975075caadfd74" }, + { + "name": "eu", + "unicode": "1F1EA-1F1FA", + "digest": "363f60e8a747166d5cec8d70bfdf266411eec2ff07933b6187975075caadfd74" + }, { "name": "flag_fi", "unicode": "1F1EB-1F1EE", "digest": "1a1959cb551a0e8bdaee8c04657fb7387a4d83173f7759f89468da12e1818a9e" }, + { + "name": "fi", + "unicode": "1F1EB-1F1EE", + "digest": "1a1959cb551a0e8bdaee8c04657fb7387a4d83173f7759f89468da12e1818a9e" + }, { "name": "flag_fj", "unicode": "1F1EB-1F1EF", "digest": "f26dc36ea9c1f32d9bb54874ea384e7118b6e2585be69245fdd73acd8304ae78" }, + { + "name": "fj", + "unicode": "1F1EB-1F1EF", + "digest": "f26dc36ea9c1f32d9bb54874ea384e7118b6e2585be69245fdd73acd8304ae78" + }, { "name": "flag_fk", "unicode": "1F1EB-1F1F0", "digest": "0479e233499b704f91a9b13d083e66296efe2f28ed917ab1496b223bfb09adb8" }, + { + "name": "fk", + "unicode": "1F1EB-1F1F0", + "digest": "0479e233499b704f91a9b13d083e66296efe2f28ed917ab1496b223bfb09adb8" + }, { "name": "flag_fm", "unicode": "1F1EB-1F1F2", "digest": "142ea7b4b4a7004329925b495da43ab82351cbaac383c8da6e614b39ba58d05e" }, + { + "name": "fm", + "unicode": "1F1EB-1F1F2", + "digest": "142ea7b4b4a7004329925b495da43ab82351cbaac383c8da6e614b39ba58d05e" + }, { "name": "flag_fo", "unicode": "1F1EB-1F1F4", "digest": "f1c800d4f4d39e2aead9a11ed500f16108d6bc48bd24bd2a1af7b966d8e76752" }, + { + "name": "fo", + "unicode": "1F1EB-1F1F4", + "digest": "f1c800d4f4d39e2aead9a11ed500f16108d6bc48bd24bd2a1af7b966d8e76752" + }, { "name": "flag_fr", "unicode": "1F1EB-1F1F7", "digest": "6f52f36b5199c65ab1cad13ff4e77d2d8b48a8ff79b92166976674ffdc7829ee" }, + { + "name": "fr", + "unicode": "1F1EB-1F1F7", + "digest": "6f52f36b5199c65ab1cad13ff4e77d2d8b48a8ff79b92166976674ffdc7829ee" + }, { "name": "flag_ga", "unicode": "1F1EC-1F1E6", "digest": "50a0d5a07466e419b74a4d532738f7958de9baa37df6191be4f3755dccc3b326" }, + { + "name": "ga", + "unicode": "1F1EC-1F1E6", + "digest": "50a0d5a07466e419b74a4d532738f7958de9baa37df6191be4f3755dccc3b326" + }, { "name": "flag_gb", "unicode": "1F1EC-1F1E7", "digest": "220f7da6d5a231b766c79f2e1b7d3fdb74ec0c0c17558cc00a8a8ccdf2afc2e0" }, + { + "name": "gb", + "unicode": "1F1EC-1F1E7", + "digest": "220f7da6d5a231b766c79f2e1b7d3fdb74ec0c0c17558cc00a8a8ccdf2afc2e0" + }, { "name": "flag_gd", "unicode": "1F1EC-1F1E9", "digest": "3e162b0d13f4ceea7f663b1d425f13863d104e80df75a640f526e276bcd04081" }, + { + "name": "gd", + "unicode": "1F1EC-1F1E9", + "digest": "3e162b0d13f4ceea7f663b1d425f13863d104e80df75a640f526e276bcd04081" + }, { "name": "flag_ge", "unicode": "1F1EC-1F1EA", "digest": "35897f8254675d2efe9e3070c88af9ef214f08440e6ee75ebe81d28cdb57ea2b" }, + { + "name": "ge", + "unicode": "1F1EC-1F1EA", + "digest": "35897f8254675d2efe9e3070c88af9ef214f08440e6ee75ebe81d28cdb57ea2b" + }, { "name": "flag_gf", "unicode": "1F1EC-1F1EB", "digest": "3a34df321635f71a0f2cc4e1eda58d85c29230c77456362345196351bf56533d" }, + { + "name": "gf", + "unicode": "1F1EC-1F1EB", + "digest": "3a34df321635f71a0f2cc4e1eda58d85c29230c77456362345196351bf56533d" + }, { "name": "flag_gg", "unicode": "1F1EC-1F1EC", "digest": "c972f8d190b4e9ca8890df41503d202ffd73981833d3f3750f563302167bcd66" }, + { + "name": "gg", + "unicode": "1F1EC-1F1EC", + "digest": "c972f8d190b4e9ca8890df41503d202ffd73981833d3f3750f563302167bcd66" + }, { "name": "flag_gh", "unicode": "1F1EC-1F1ED", "digest": "9c3d3569bd411389fa0af7c6938d4325cedeb9c0e8f059dc1d5a74c6b8d6d01b" }, + { + "name": "gh", + "unicode": "1F1EC-1F1ED", + "digest": "9c3d3569bd411389fa0af7c6938d4325cedeb9c0e8f059dc1d5a74c6b8d6d01b" + }, { "name": "flag_gi", "unicode": "1F1EC-1F1EE", "digest": "ede638bc6fedc30a01821025d87ec19297500da9c04a7a155984fca186118649" }, + { + "name": "gi", + "unicode": "1F1EC-1F1EE", + "digest": "ede638bc6fedc30a01821025d87ec19297500da9c04a7a155984fca186118649" + }, { "name": "flag_gl", "unicode": "1F1EC-1F1F1", "digest": "a2ce3371eff1da8331671925f707232aa593ac7400d59555c9ca689729ce24ec" }, + { + "name": "gl", + "unicode": "1F1EC-1F1F1", + "digest": "a2ce3371eff1da8331671925f707232aa593ac7400d59555c9ca689729ce24ec" + }, { "name": "flag_gm", "unicode": "1F1EC-1F1F2", "digest": "932bf6eb75ddd4278268dd2f09d8fffcfef89f8fd6b6e86a08a414cd3ceec94d" }, + { + "name": "gm", + "unicode": "1F1EC-1F1F2", + "digest": "932bf6eb75ddd4278268dd2f09d8fffcfef89f8fd6b6e86a08a414cd3ceec94d" + }, { "name": "flag_gn", "unicode": "1F1EC-1F1F3", "digest": "ebf543713895adaa09d64897f24bd461191191b8fcbbcede52bdaf4bd2dc67a8" }, + { + "name": "gn", + "unicode": "1F1EC-1F1F3", + "digest": "ebf543713895adaa09d64897f24bd461191191b8fcbbcede52bdaf4bd2dc67a8" + }, { "name": "flag_gp", "unicode": "1F1EC-1F1F5", "digest": "2e6c48d80c571b34f31fa9b3622dcc51e1707c0118e991e9c177742ff02a8a96" }, + { + "name": "gp", + "unicode": "1F1EC-1F1F5", + "digest": "2e6c48d80c571b34f31fa9b3622dcc51e1707c0118e991e9c177742ff02a8a96" + }, { "name": "flag_gq", "unicode": "1F1EC-1F1F6", "digest": "b0f5810180d12fc48faf75e73f882dc59072d7bf957f8455bf7e1e336539dc41" }, + { + "name": "gq", + "unicode": "1F1EC-1F1F6", + "digest": "b0f5810180d12fc48faf75e73f882dc59072d7bf957f8455bf7e1e336539dc41" + }, { "name": "flag_gr", "unicode": "1F1EC-1F1F7", "digest": "8d60d6f8910f5179d851dbea0798b56a492c6be85f3d55e1a1126cd1d6663a3b" }, + { + "name": "gr", + "unicode": "1F1EC-1F1F7", + "digest": "8d60d6f8910f5179d851dbea0798b56a492c6be85f3d55e1a1126cd1d6663a3b" + }, { "name": "flag_gs", "unicode": "1F1EC-1F1F8", "digest": "7b07915af0e2364ebc386a162d44846f3a7986fdd24e20ad2bc56d64a103fe9c" }, + { + "name": "gs", + "unicode": "1F1EC-1F1F8", + "digest": "7b07915af0e2364ebc386a162d44846f3a7986fdd24e20ad2bc56d64a103fe9c" + }, { "name": "flag_gt", "unicode": "1F1EC-1F1F9", "digest": "0c78108ede45bf34917b409a0867f5ec8253c74b694beda083f3e8d04d7a10d8" }, + { + "name": "gt", + "unicode": "1F1EC-1F1F9", + "digest": "0c78108ede45bf34917b409a0867f5ec8253c74b694beda083f3e8d04d7a10d8" + }, { "name": "flag_gu", "unicode": "1F1EC-1F1FA", "digest": "909f1bc98fa1507adb787eb3875503b21ea937d6ae8bb152153916c2da5e13bb" }, + { + "name": "gu", + "unicode": "1F1EC-1F1FA", + "digest": "909f1bc98fa1507adb787eb3875503b21ea937d6ae8bb152153916c2da5e13bb" + }, { "name": "flag_gw", "unicode": "1F1EC-1F1FC", "digest": "f5f34410c7b22d5ed9994b47d0e7a9d9a6a1f05c4d3142f7fef3e4409725f5e6" }, + { + "name": "gw", + "unicode": "1F1EC-1F1FC", + "digest": "f5f34410c7b22d5ed9994b47d0e7a9d9a6a1f05c4d3142f7fef3e4409725f5e6" + }, { "name": "flag_gy", "unicode": "1F1EC-1F1FE", "digest": "4939cf52ab34a924a31032b42668960a2c7d8d4f998b16b065c247110df334be" }, + { + "name": "gy", + "unicode": "1F1EC-1F1FE", + "digest": "4939cf52ab34a924a31032b42668960a2c7d8d4f998b16b065c247110df334be" + }, { "name": "flag_hk", "unicode": "1F1ED-1F1F0", "digest": "bde0916df6d62f6b1cf8f85a8a39526c97fc6ef6fedb0b0cae2adb127a08eafe" }, + { + "name": "hk", + "unicode": "1F1ED-1F1F0", + "digest": "bde0916df6d62f6b1cf8f85a8a39526c97fc6ef6fedb0b0cae2adb127a08eafe" + }, { "name": "flag_hm", "unicode": "1F1ED-1F1F2", "digest": "603e6c9bff9a0dc941970a313fe98fbf53ff5a57028f1a2766420be4211711cc" }, + { + "name": "hm", + "unicode": "1F1ED-1F1F2", + "digest": "603e6c9bff9a0dc941970a313fe98fbf53ff5a57028f1a2766420be4211711cc" + }, { "name": "flag_hn", "unicode": "1F1ED-1F1F3", "digest": "2953ad0909bc32c02615f6ad5a4e5f331ba794a41632b1f0fc366e1c640cc2b9" }, + { + "name": "hn", + "unicode": "1F1ED-1F1F3", + "digest": "2953ad0909bc32c02615f6ad5a4e5f331ba794a41632b1f0fc366e1c640cc2b9" + }, { "name": "flag_hr", "unicode": "1F1ED-1F1F7", "digest": "41c9ffc4f0faaa2d77e5cffb781329e7d2489ce879bd8eb9c503621e834abc50" }, + { + "name": "hr", + "unicode": "1F1ED-1F1F7", + "digest": "41c9ffc4f0faaa2d77e5cffb781329e7d2489ce879bd8eb9c503621e834abc50" + }, { "name": "flag_ht", "unicode": "1F1ED-1F1F9", "digest": "6a56c3d71b4f858e1774aa2134a9f5584087fec968e9ee8bb1046d2ec93bf059" }, + { + "name": "ht", + "unicode": "1F1ED-1F1F9", + "digest": "6a56c3d71b4f858e1774aa2134a9f5584087fec968e9ee8bb1046d2ec93bf059" + }, { "name": "flag_hu", "unicode": "1F1ED-1F1FA", "digest": "72f5809818d4cab8c0cee73df7f67b820fb8471eea4199911a5917ac099795e8" }, + { + "name": "hu", + "unicode": "1F1ED-1F1FA", + "digest": "72f5809818d4cab8c0cee73df7f67b820fb8471eea4199911a5917ac099795e8" + }, { "name": "flag_ic", "unicode": "1F1EE-1F1E8", "digest": "7e2a7667fcd05f927af47e64c5790c104a9956dd9f1a45f03cb0fdcc85d866d3" }, + { + "name": "ic", + "unicode": "1F1EE-1F1E8", + "digest": "7e2a7667fcd05f927af47e64c5790c104a9956dd9f1a45f03cb0fdcc85d866d3" + }, { "name": "flag_id", "unicode": "1F1EE-1F1E9", "digest": "4721f616fae2e443e52f1e9cc96e4835bddca16a2d75d7d5afea57cdee866b7f" }, + { + "name": "indonesia", + "unicode": "1F1EE-1F1E9", + "digest": "4721f616fae2e443e52f1e9cc96e4835bddca16a2d75d7d5afea57cdee866b7f" + }, { "name": "flag_ie", "unicode": "1F1EE-1F1EA", "digest": "84b19833e6c9fb43187f8a28d85045a3df58816f20a07edab90474323174b1f3" }, + { + "name": "ie", + "unicode": "1F1EE-1F1EA", + "digest": "84b19833e6c9fb43187f8a28d85045a3df58816f20a07edab90474323174b1f3" + }, { "name": "flag_il", "unicode": "1F1EE-1F1F1", "digest": "c99d4bd8c2541cf3a7392c4faf4477d96bc47065dd1423b9e06450483e69b34f" }, + { + "name": "il", + "unicode": "1F1EE-1F1F1", + "digest": "c99d4bd8c2541cf3a7392c4faf4477d96bc47065dd1423b9e06450483e69b34f" + }, { "name": "flag_im", "unicode": "1F1EE-1F1F2", "digest": "5eeb12c0315b527ce61649a38b64d76af726a73b2d381d1a1ddd1366bafb1bfc" }, + { + "name": "im", + "unicode": "1F1EE-1F1F2", + "digest": "5eeb12c0315b527ce61649a38b64d76af726a73b2d381d1a1ddd1366bafb1bfc" + }, { "name": "flag_in", "unicode": "1F1EE-1F1F3", "digest": "ecc3cfcff3368fe0875a51a8be9f4dfd449a187e5beb41a2b34241736247f73b" }, + { + "name": "in", + "unicode": "1F1EE-1F1F3", + "digest": "ecc3cfcff3368fe0875a51a8be9f4dfd449a187e5beb41a2b34241736247f73b" + }, { "name": "flag_io", "unicode": "1F1EE-1F1F4", "digest": "26243d60e04ba3bc9eb8f008bfc77b2a64bcf1a3d0073eb0449a8c8121618c9c" }, + { + "name": "io", + "unicode": "1F1EE-1F1F4", + "digest": "26243d60e04ba3bc9eb8f008bfc77b2a64bcf1a3d0073eb0449a8c8121618c9c" + }, { "name": "flag_iq", "unicode": "1F1EE-1F1F6", "digest": "a1fb5e59575081920b3be5290f654d57a9be099deb56d4ed69eba81a2b531cb3" }, + { + "name": "iq", + "unicode": "1F1EE-1F1F6", + "digest": "a1fb5e59575081920b3be5290f654d57a9be099deb56d4ed69eba81a2b531cb3" + }, { "name": "flag_ir", "unicode": "1F1EE-1F1F7", "digest": "ab89488b934af1d4bdae7ed16dfc74fffe658bb8e95d5161b48cdd06de44ae85" }, + { + "name": "ir", + "unicode": "1F1EE-1F1F7", + "digest": "ab89488b934af1d4bdae7ed16dfc74fffe658bb8e95d5161b48cdd06de44ae85" + }, { "name": "flag_is", "unicode": "1F1EE-1F1F8", "digest": "55db1fc9e6c56d4c9bcb9a46e5e4300cf2a0c32fa91dc24b487a1d56c8097268" }, + { + "name": "is", + "unicode": "1F1EE-1F1F8", + "digest": "55db1fc9e6c56d4c9bcb9a46e5e4300cf2a0c32fa91dc24b487a1d56c8097268" + }, { "name": "flag_it", "unicode": "1F1EE-1F1F9", "digest": "36fc993fb00ab607578a4d0e573e988e17b9459a68a000a48de905a8238589d0" }, + { + "name": "it", + "unicode": "1F1EE-1F1F9", + "digest": "36fc993fb00ab607578a4d0e573e988e17b9459a68a000a48de905a8238589d0" + }, { "name": "flag_je", "unicode": "1F1EF-1F1EA", "digest": "c608dbfd1259330e2f8c40dc5d12ffd0489396f4fc5f3ca57bcb2f0d9d05c20c" }, + { + "name": "je", + "unicode": "1F1EF-1F1EA", + "digest": "c608dbfd1259330e2f8c40dc5d12ffd0489396f4fc5f3ca57bcb2f0d9d05c20c" + }, { "name": "flag_jm", "unicode": "1F1EF-1F1F2", "digest": "a8224b68b2d324f848d75e4376875ef76a8174e6ba32790d9ca622fe1eabfd5f" }, + { + "name": "jm", + "unicode": "1F1EF-1F1F2", + "digest": "a8224b68b2d324f848d75e4376875ef76a8174e6ba32790d9ca622fe1eabfd5f" + }, { "name": "flag_jo", "unicode": "1F1EF-1F1F4", "digest": "2403563dc2ab4ed0e7e3a0761cc09f96801550bba6b177b54d651d8804ad987d" }, + { + "name": "jo", + "unicode": "1F1EF-1F1F4", + "digest": "2403563dc2ab4ed0e7e3a0761cc09f96801550bba6b177b54d651d8804ad987d" + }, { "name": "flag_jp", "unicode": "1F1EF-1F1F5", "digest": "aea8eebd0a0139818cb7629d9c9a8e55160b458eb8ffeee2f36c5cff4b507fd3" }, + { + "name": "jp", + "unicode": "1F1EF-1F1F5", + "digest": "aea8eebd0a0139818cb7629d9c9a8e55160b458eb8ffeee2f36c5cff4b507fd3" + }, { "name": "flag_ke", "unicode": "1F1F0-1F1EA", "digest": "9c8365f74858743bcdce4a9cf6a6f4110faf2dc6433e5dc7d98c24bb3b32a36d" }, + { + "name": "ke", + "unicode": "1F1F0-1F1EA", + "digest": "9c8365f74858743bcdce4a9cf6a6f4110faf2dc6433e5dc7d98c24bb3b32a36d" + }, { "name": "flag_kg", "unicode": "1F1F0-1F1EC", "digest": "0c72bdb1d64b1e3be3d9516a50655a6162d8501851d2cf2fadb8c6ef7740df4e" }, + { + "name": "kg", + "unicode": "1F1F0-1F1EC", + "digest": "0c72bdb1d64b1e3be3d9516a50655a6162d8501851d2cf2fadb8c6ef7740df4e" + }, { "name": "flag_kh", "unicode": "1F1F0-1F1ED", "digest": "49e41e488732d789e395091e144cd6215c6818ba2073e5e22ea21203a737d03c" }, + { + "name": "kh", + "unicode": "1F1F0-1F1ED", + "digest": "49e41e488732d789e395091e144cd6215c6818ba2073e5e22ea21203a737d03c" + }, { "name": "flag_ki", "unicode": "1F1F0-1F1EE", "digest": "9d7f168adbcf5f4cfe28470addfdb0a8b231438d593edb70f633981bfa4c7638" }, + { + "name": "ki", + "unicode": "1F1F0-1F1EE", + "digest": "9d7f168adbcf5f4cfe28470addfdb0a8b231438d593edb70f633981bfa4c7638" + }, { "name": "flag_km", "unicode": "1F1F0-1F1F2", "digest": "9318c28957fa7a19eba5ec452c1cbce01a5a83d41d29d081614d3abb0585d478" }, + { + "name": "km", + "unicode": "1F1F0-1F1F2", + "digest": "9318c28957fa7a19eba5ec452c1cbce01a5a83d41d29d081614d3abb0585d478" + }, { "name": "flag_kn", "unicode": "1F1F0-1F1F3", "digest": "eac7e7d0f023dee5c0c8559bc2c9a96273adda54ce47598025120b30d8d6ebc1" }, + { + "name": "kn", + "unicode": "1F1F0-1F1F3", + "digest": "eac7e7d0f023dee5c0c8559bc2c9a96273adda54ce47598025120b30d8d6ebc1" + }, { "name": "flag_kp", "unicode": "1F1F0-1F1F5", "digest": "d4d53db6f8363174de6db864c056267ba8a7d7e87b5527f2f42bb9b8ac3f362b" }, + { + "name": "kp", + "unicode": "1F1F0-1F1F5", + "digest": "d4d53db6f8363174de6db864c056267ba8a7d7e87b5527f2f42bb9b8ac3f362b" + }, { "name": "flag_kr", "unicode": "1F1F0-1F1F7", "digest": "5c7e61ab4a2aae70cbe51f0ca4718516002bc943b35d870bd853a0c98c4e0ed5" }, + { + "name": "kr", + "unicode": "1F1F0-1F1F7", + "digest": "5c7e61ab4a2aae70cbe51f0ca4718516002bc943b35d870bd853a0c98c4e0ed5" + }, { "name": "flag_kw", "unicode": "1F1F0-1F1FC", "digest": "5d229cd99d25f4285bd30d98cfcc3cd8346648897476e2905a1811ceeef48d37" }, + { + "name": "kw", + "unicode": "1F1F0-1F1FC", + "digest": "5d229cd99d25f4285bd30d98cfcc3cd8346648897476e2905a1811ceeef48d37" + }, { "name": "flag_ky", "unicode": "1F1F0-1F1FE", "digest": "9ce3d8dfc273d3a400960876c434b702f93df92c6c00682dbed2ec8e3966d8a8" }, + { + "name": "ky", + "unicode": "1F1F0-1F1FE", + "digest": "9ce3d8dfc273d3a400960876c434b702f93df92c6c00682dbed2ec8e3966d8a8" + }, { "name": "flag_kz", "unicode": "1F1F0-1F1FF", "digest": "a6f0be0a767fa4824495d568d9fc2bd8d4c1a26f363873d3b65362e9383e2a50" }, + { + "name": "kz", + "unicode": "1F1F0-1F1FF", + "digest": "a6f0be0a767fa4824495d568d9fc2bd8d4c1a26f363873d3b65362e9383e2a50" + }, { "name": "flag_la", "unicode": "1F1F1-1F1E6", "digest": "ab2ae96da87f7b53ab212f8dcd897a591cff9ea6666270097a8e739ee0b8f8cb" }, + { + "name": "la", + "unicode": "1F1F1-1F1E6", + "digest": "ab2ae96da87f7b53ab212f8dcd897a591cff9ea6666270097a8e739ee0b8f8cb" + }, { "name": "flag_lb", "unicode": "1F1F1-1F1E7", "digest": "0c3fcab22e9fae1c78658290aff97de785d0b6adb5e3702d00073ce774b7ed54" }, + { + "name": "lb", + "unicode": "1F1F1-1F1E7", + "digest": "0c3fcab22e9fae1c78658290aff97de785d0b6adb5e3702d00073ce774b7ed54" + }, { "name": "flag_lc", "unicode": "1F1F1-1F1E8", "digest": "e154b0b3a1635a36e0d9ad518c0ea12259320e5f1ebbda982248486492065d28" }, + { + "name": "lc", + "unicode": "1F1F1-1F1E8", + "digest": "e154b0b3a1635a36e0d9ad518c0ea12259320e5f1ebbda982248486492065d28" + }, { "name": "flag_li", "unicode": "1F1F1-1F1EE", "digest": "bbc393a89e73cc8c29a0a9297428d07aa1d4717ea9b7d4dd9d69f21ac7d0605d" }, + { + "name": "li", + "unicode": "1F1F1-1F1EE", + "digest": "bbc393a89e73cc8c29a0a9297428d07aa1d4717ea9b7d4dd9d69f21ac7d0605d" + }, { "name": "flag_lk", "unicode": "1F1F1-1F1F0", "digest": "376bd501d113a844971ca1006ab31aa086cd55d74842ea5f3dedaba997b58693" }, + { + "name": "lk", + "unicode": "1F1F1-1F1F0", + "digest": "376bd501d113a844971ca1006ab31aa086cd55d74842ea5f3dedaba997b58693" + }, { "name": "flag_lr", "unicode": "1F1F1-1F1F7", "digest": "9a6ebe1c9d9a53079ee77292a5ad0965f96409b0417f92876a1c3bd463d6a9bc" }, + { + "name": "lr", + "unicode": "1F1F1-1F1F7", + "digest": "9a6ebe1c9d9a53079ee77292a5ad0965f96409b0417f92876a1c3bd463d6a9bc" + }, { "name": "flag_ls", "unicode": "1F1F1-1F1F8", "digest": "e2f4b05414f6e0c3d629a92b0534d4145475f0214a83a62c902fe0884c833c89" }, + { + "name": "ls", + "unicode": "1F1F1-1F1F8", + "digest": "e2f4b05414f6e0c3d629a92b0534d4145475f0214a83a62c902fe0884c833c89" + }, { "name": "flag_lt", "unicode": "1F1F1-1F1F9", "digest": "d5e2f8b2ffa820a33ea6d612fccd61e32467d25154342f5be134d3520e48387f" }, + { + "name": "lt", + "unicode": "1F1F1-1F1F9", + "digest": "d5e2f8b2ffa820a33ea6d612fccd61e32467d25154342f5be134d3520e48387f" + }, { "name": "flag_lu", "unicode": "1F1F1-1F1FA", "digest": "f43277103292195b51981d08e2dde68eab660a65c7875f510e09a8b2370f1b5c" }, + { + "name": "lu", + "unicode": "1F1F1-1F1FA", + "digest": "f43277103292195b51981d08e2dde68eab660a65c7875f510e09a8b2370f1b5c" + }, { "name": "flag_lv", "unicode": "1F1F1-1F1FB", "digest": "e1288ac5c80d6e9d577d652e34be247ca39bf9d3d7cfc8a6cae13c1f9ac9dc47" }, + { + "name": "lv", + "unicode": "1F1F1-1F1FB", + "digest": "e1288ac5c80d6e9d577d652e34be247ca39bf9d3d7cfc8a6cae13c1f9ac9dc47" + }, { "name": "flag_ly", "unicode": "1F1F1-1F1FE", "digest": "5122294b769a174e3b6e3d238bb846b3e760929f5bb3c1a708d8a429f3f32f68" }, + { + "name": "ly", + "unicode": "1F1F1-1F1FE", + "digest": "5122294b769a174e3b6e3d238bb846b3e760929f5bb3c1a708d8a429f3f32f68" + }, { "name": "flag_ma", "unicode": "1F1F2-1F1E6", "digest": "615a6447ff284de7689b4fd7b04fdda308f65dbbec958cfb96d2977514981d16" }, + { + "name": "ma", + "unicode": "1F1F2-1F1E6", + "digest": "615a6447ff284de7689b4fd7b04fdda308f65dbbec958cfb96d2977514981d16" + }, { "name": "flag_mc", "unicode": "1F1F2-1F1E8", "digest": "08b48b28938acbfc0fbc15c25ee14dbad7164c5165d03df2eee370755ee7b4cf" }, + { + "name": "mc", + "unicode": "1F1F2-1F1E8", + "digest": "08b48b28938acbfc0fbc15c25ee14dbad7164c5165d03df2eee370755ee7b4cf" + }, { "name": "flag_md", "unicode": "1F1F2-1F1E9", "digest": "93d61de68f821e1e08b30e63d91e8b4a657766475128538894cf9da9a3b4e3c0" }, + { + "name": "md", + "unicode": "1F1F2-1F1E9", + "digest": "93d61de68f821e1e08b30e63d91e8b4a657766475128538894cf9da9a3b4e3c0" + }, { "name": "flag_me", "unicode": "1F1F2-1F1EA", "digest": "ee55c0eb78241aec2baf1822a47fa46d63209ceae3db7617ae886b823ae229ff" }, + { + "name": "me", + "unicode": "1F1F2-1F1EA", + "digest": "ee55c0eb78241aec2baf1822a47fa46d63209ceae3db7617ae886b823ae229ff" + }, { "name": "flag_mf", "unicode": "1F1F2-1F1EB", "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" }, + { + "name": "mf", + "unicode": "1F1F2-1F1EB", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, { "name": "flag_mg", "unicode": "1F1F2-1F1EC", "digest": "86ec8140e2c4854f52cff74757baf0cbb75a4aacca8be6af8c8f9c939a7b866c" }, + { + "name": "mg", + "unicode": "1F1F2-1F1EC", + "digest": "86ec8140e2c4854f52cff74757baf0cbb75a4aacca8be6af8c8f9c939a7b866c" + }, { "name": "flag_mh", "unicode": "1F1F2-1F1ED", "digest": "8311ea3422c9d5e94b55e19b03bedd6fe6e2a191b7657e15ac75a48932958a5b" }, + { + "name": "mh", + "unicode": "1F1F2-1F1ED", + "digest": "8311ea3422c9d5e94b55e19b03bedd6fe6e2a191b7657e15ac75a48932958a5b" + }, { "name": "flag_mk", "unicode": "1F1F2-1F1F0", "digest": "5c6f504f88c5a875c06ac8b26fa6e81a9d79c42a1c7d1fad9a5d4c8ad06ca502" }, + { + "name": "mk", + "unicode": "1F1F2-1F1F0", + "digest": "5c6f504f88c5a875c06ac8b26fa6e81a9d79c42a1c7d1fad9a5d4c8ad06ca502" + }, { "name": "flag_ml", "unicode": "1F1F2-1F1F1", "digest": "d08a4973db40cf28e58ca3c80e8bd4e50d68ba1080b31917aeefdb0e210b5c50" }, + { + "name": "ml", + "unicode": "1F1F2-1F1F1", + "digest": "d08a4973db40cf28e58ca3c80e8bd4e50d68ba1080b31917aeefdb0e210b5c50" + }, { "name": "flag_mm", "unicode": "1F1F2-1F1F2", "digest": "5e95089514ca09bb93afb481b317477c9d053adcf450e0b711d78ed1078c7470" }, + { + "name": "mm", + "unicode": "1F1F2-1F1F2", + "digest": "5e95089514ca09bb93afb481b317477c9d053adcf450e0b711d78ed1078c7470" + }, { "name": "flag_mn", "unicode": "1F1F2-1F1F3", "digest": "7a0ca72715dd2a36eeeed2f8c888497cb752f0000af8f07d6930743caf6e4273" }, + { + "name": "mn", + "unicode": "1F1F2-1F1F3", + "digest": "7a0ca72715dd2a36eeeed2f8c888497cb752f0000af8f07d6930743caf6e4273" + }, { "name": "flag_mo", "unicode": "1F1F2-1F1F4", "digest": "d2c7c2191bc1bc83d85f2270968cb4de5cf26a11f70e166a8b32c108287ef729" }, + { + "name": "mo", + "unicode": "1F1F2-1F1F4", + "digest": "d2c7c2191bc1bc83d85f2270968cb4de5cf26a11f70e166a8b32c108287ef729" + }, { "name": "flag_mp", "unicode": "1F1F2-1F1F5", "digest": "89ad06121fd7981338fe188464491bea371f85125bfb4fc01fb5cad606613b1e" }, + { + "name": "mp", + "unicode": "1F1F2-1F1F5", + "digest": "89ad06121fd7981338fe188464491bea371f85125bfb4fc01fb5cad606613b1e" + }, { "name": "flag_mq", "unicode": "1F1F2-1F1F6", "digest": "98176f3af823b26a3657a17c5073ee22367898b40bd3973de76329aa87ca5a2e" }, + { + "name": "mq", + "unicode": "1F1F2-1F1F6", + "digest": "98176f3af823b26a3657a17c5073ee22367898b40bd3973de76329aa87ca5a2e" + }, { "name": "flag_mr", "unicode": "1F1F2-1F1F7", "digest": "cc3e705ad84f83fe2d544385c39564743024dab26595d62469b35fdb791f6015" }, + { + "name": "mr", + "unicode": "1F1F2-1F1F7", + "digest": "cc3e705ad84f83fe2d544385c39564743024dab26595d62469b35fdb791f6015" + }, { "name": "flag_ms", "unicode": "1F1F2-1F1F8", "digest": "465e3d5700b557f2589bd6e34a0c6b12c634a6ed4dcfbee3c1c841c5de3413f0" }, + { + "name": "ms", + "unicode": "1F1F2-1F1F8", + "digest": "465e3d5700b557f2589bd6e34a0c6b12c634a6ed4dcfbee3c1c841c5de3413f0" + }, { "name": "flag_mt", "unicode": "1F1F2-1F1F9", "digest": "e610ba22d8d8ad750ed10dff8e1b4d89bc34f066c3424bfa77dbdc1a5d79743a" }, + { + "name": "mt", + "unicode": "1F1F2-1F1F9", + "digest": "e610ba22d8d8ad750ed10dff8e1b4d89bc34f066c3424bfa77dbdc1a5d79743a" + }, { "name": "flag_mu", "unicode": "1F1F2-1F1FA", "digest": "3daf015d3b95218677dafbb282b7804686aa68875a6bd1d70c165b7b149e19cb" }, + { + "name": "mu", + "unicode": "1F1F2-1F1FA", + "digest": "3daf015d3b95218677dafbb282b7804686aa68875a6bd1d70c165b7b149e19cb" + }, { "name": "flag_mv", "unicode": "1F1F2-1F1FB", "digest": "d30e4bfd04f08177de92f3c175600aaafa89b9668bbe2b83f35f07a74382065c" }, + { + "name": "mv", + "unicode": "1F1F2-1F1FB", + "digest": "d30e4bfd04f08177de92f3c175600aaafa89b9668bbe2b83f35f07a74382065c" + }, { "name": "flag_mw", "unicode": "1F1F2-1F1FC", "digest": "f364b1c8bfda3f86b5e26422eedc571ba11e312dcc634197631a6840cb22aede" }, + { + "name": "mw", + "unicode": "1F1F2-1F1FC", + "digest": "f364b1c8bfda3f86b5e26422eedc571ba11e312dcc634197631a6840cb22aede" + }, { "name": "flag_mx", "unicode": "1F1F2-1F1FD", "digest": "eafb02ec0be9cefab7cef7c426c7d860d98e4947f4da04054154dc86d8f487c4" }, + { + "name": "mx", + "unicode": "1F1F2-1F1FD", + "digest": "eafb02ec0be9cefab7cef7c426c7d860d98e4947f4da04054154dc86d8f487c4" + }, { "name": "flag_my", "unicode": "1F1F2-1F1FE", "digest": "9a690b357bc6b970781bd122c1e546ade3ccb73d930c2af1008b82027e36c7cf" }, + { + "name": "my", + "unicode": "1F1F2-1F1FE", + "digest": "9a690b357bc6b970781bd122c1e546ade3ccb73d930c2af1008b82027e36c7cf" + }, { "name": "flag_mz", "unicode": "1F1F2-1F1FF", "digest": "36d0548ebfef9e0443ec1d0597ebfa6e95c25b997381f30c8c74008820743bb9" }, + { + "name": "mz", + "unicode": "1F1F2-1F1FF", + "digest": "36d0548ebfef9e0443ec1d0597ebfa6e95c25b997381f30c8c74008820743bb9" + }, { "name": "flag_na", "unicode": "1F1F3-1F1E6", "digest": "4989dc9452b0bdfa101cfd3b7c83ef1195a7e45128b9ed00193fe712a6d02fca" }, + { + "name": "na", + "unicode": "1F1F3-1F1E6", + "digest": "4989dc9452b0bdfa101cfd3b7c83ef1195a7e45128b9ed00193fe712a6d02fca" + }, { "name": "flag_nc", "unicode": "1F1F3-1F1E8", "digest": "7fc9d865eebf729d5496c4cd7576476ec599f65b379d4a6df66b4e399553c2eb" }, + { + "name": "nc", + "unicode": "1F1F3-1F1E8", + "digest": "7fc9d865eebf729d5496c4cd7576476ec599f65b379d4a6df66b4e399553c2eb" + }, { "name": "flag_ne", "unicode": "1F1F3-1F1EA", "digest": "d3f10fb44ec44a04112bc66d05f0a44c6ec46dae73cfd3fe26cdc8b32ec06713" }, + { + "name": "ne", + "unicode": "1F1F3-1F1EA", + "digest": "d3f10fb44ec44a04112bc66d05f0a44c6ec46dae73cfd3fe26cdc8b32ec06713" + }, { "name": "flag_nf", "unicode": "1F1F3-1F1EB", "digest": "d390e0d52215a025380af221ba9e955e5886edbb4c9f4b124f2fb60a8e019e42" }, + { + "name": "nf", + "unicode": "1F1F3-1F1EB", + "digest": "d390e0d52215a025380af221ba9e955e5886edbb4c9f4b124f2fb60a8e019e42" + }, { "name": "flag_ng", "unicode": "1F1F3-1F1EC", "digest": "e69d1bb8f1db4a0c295c90dda23d8f97c2dea59f9a2da2ecb0e9a1dc4dbea101" }, + { + "name": "nigeria", + "unicode": "1F1F3-1F1EC", + "digest": "e69d1bb8f1db4a0c295c90dda23d8f97c2dea59f9a2da2ecb0e9a1dc4dbea101" + }, { "name": "flag_ni", "unicode": "1F1F3-1F1EE", "digest": "dbaccc942637469b0ee75bd5f956958c3c5a89d8f69b69c96f02ab6594124894" }, + { + "name": "ni", + "unicode": "1F1F3-1F1EE", + "digest": "dbaccc942637469b0ee75bd5f956958c3c5a89d8f69b69c96f02ab6594124894" + }, { "name": "flag_nl", "unicode": "1F1F3-1F1F1", "digest": "bda2eb0315763c3c19d37c664dab1ee4280f20888a0ca57677fd33cfa4240910" }, + { + "name": "nl", + "unicode": "1F1F3-1F1F1", + "digest": "bda2eb0315763c3c19d37c664dab1ee4280f20888a0ca57677fd33cfa4240910" + }, { "name": "flag_no", "unicode": "1F1F3-1F1F4", "digest": "42b49dec756a220781ea271ca8fbcaba524dc3b38d5d8f999bfaa40ef9ebd302" }, + { + "name": "no", + "unicode": "1F1F3-1F1F4", + "digest": "42b49dec756a220781ea271ca8fbcaba524dc3b38d5d8f999bfaa40ef9ebd302" + }, { "name": "flag_np", "unicode": "1F1F3-1F1F5", "digest": "b5259257db079235310d5d9537d2b5b61ae0326bc8920ba13084b009844e2957" }, + { + "name": "np", + "unicode": "1F1F3-1F1F5", + "digest": "b5259257db079235310d5d9537d2b5b61ae0326bc8920ba13084b009844e2957" + }, { "name": "flag_nr", "unicode": "1F1F3-1F1F7", "digest": "1bd7d1fe2c3a5e98cfd4dff6e8d6dd6d3c74f0051ad615587d77d2291a9784cc" }, + { + "name": "nr", + "unicode": "1F1F3-1F1F7", + "digest": "1bd7d1fe2c3a5e98cfd4dff6e8d6dd6d3c74f0051ad615587d77d2291a9784cc" + }, { "name": "flag_nu", "unicode": "1F1F3-1F1FA", "digest": "e2a7a398e07d2232147cc0917d72d18b519246d3d314e9f6f03dcf98d312d4ce" }, + { + "name": "nu", + "unicode": "1F1F3-1F1FA", + "digest": "e2a7a398e07d2232147cc0917d72d18b519246d3d314e9f6f03dcf98d312d4ce" + }, { "name": "flag_nz", "unicode": "1F1F3-1F1FF", "digest": "ce8b1cb87dae3a3ec865575b57a0b4987a7f4bd3f170e7b210dd764fc2588cd4" }, + { + "name": "nz", + "unicode": "1F1F3-1F1FF", + "digest": "ce8b1cb87dae3a3ec865575b57a0b4987a7f4bd3f170e7b210dd764fc2588cd4" + }, { "name": "flag_om", "unicode": "1F1F4-1F1F2", "digest": "29da72505a276a8a372a00c197388ebc5098c221cab26b3ff755bd62b10f740f" }, + { + "name": "om", + "unicode": "1F1F4-1F1F2", + "digest": "29da72505a276a8a372a00c197388ebc5098c221cab26b3ff755bd62b10f740f" + }, { "name": "flag_pa", "unicode": "1F1F5-1F1E6", "digest": "180b673c9aceea43a8b55823a82d80600257e4982d0757d129860e3d8a14f458" }, + { + "name": "pa", + "unicode": "1F1F5-1F1E6", + "digest": "180b673c9aceea43a8b55823a82d80600257e4982d0757d129860e3d8a14f458" + }, { "name": "flag_pe", "unicode": "1F1F5-1F1EA", "digest": "b61823ea2cd91e371e40832df5764558b81d44fac41030827a3f6d2564643c00" }, + { + "name": "pe", + "unicode": "1F1F5-1F1EA", + "digest": "b61823ea2cd91e371e40832df5764558b81d44fac41030827a3f6d2564643c00" + }, { "name": "flag_pf", "unicode": "1F1F5-1F1EB", "digest": "e560421911f4af90c73a0dbdf8f42e69316003799304c9394fb127e3b83326fa" }, + { + "name": "pf", + "unicode": "1F1F5-1F1EB", + "digest": "e560421911f4af90c73a0dbdf8f42e69316003799304c9394fb127e3b83326fa" + }, { "name": "flag_pg", "unicode": "1F1F5-1F1EC", "digest": "880e87db2ce0eac38db037683a5db46fd6ce30623cf56ae4a93a747103570044" }, + { + "name": "pg", + "unicode": "1F1F5-1F1EC", + "digest": "880e87db2ce0eac38db037683a5db46fd6ce30623cf56ae4a93a747103570044" + }, { "name": "flag_ph", "unicode": "1F1F5-1F1ED", "digest": "49aae2f56bfd1385741dc76857aa1f1459778b2d39a1c955e469c5367585bfd5" }, + { + "name": "ph", + "unicode": "1F1F5-1F1ED", + "digest": "49aae2f56bfd1385741dc76857aa1f1459778b2d39a1c955e469c5367585bfd5" + }, { "name": "flag_pk", "unicode": "1F1F5-1F1F0", "digest": "64379dbfc932df3a07935b5cfa11ca151f761d3728939e982604e12c663cd646" }, + { + "name": "pk", + "unicode": "1F1F5-1F1F0", + "digest": "64379dbfc932df3a07935b5cfa11ca151f761d3728939e982604e12c663cd646" + }, { "name": "flag_pl", "unicode": "1F1F5-1F1F1", "digest": "3b688b074c2735d3dea0b7ab74b80eba243ce50cb05d68e585c9d701c1f14617" }, + { + "name": "pl", + "unicode": "1F1F5-1F1F1", + "digest": "3b688b074c2735d3dea0b7ab74b80eba243ce50cb05d68e585c9d701c1f14617" + }, { "name": "flag_pm", "unicode": "1F1F5-1F1F2", "digest": "a13a69ee3131501dd8138173cfb669a35ee8039d84aa665e69dd7f0d0aa3e717" }, + { + "name": "pm", + "unicode": "1F1F5-1F1F2", + "digest": "a13a69ee3131501dd8138173cfb669a35ee8039d84aa665e69dd7f0d0aa3e717" + }, { "name": "flag_pn", "unicode": "1F1F5-1F1F3", "digest": "d7ae3985cf66024e4a3001e79a8efbb3e75571f2b0abbd0fb87fc1efc795a2b3" }, + { + "name": "pn", + "unicode": "1F1F5-1F1F3", + "digest": "d7ae3985cf66024e4a3001e79a8efbb3e75571f2b0abbd0fb87fc1efc795a2b3" + }, { "name": "flag_pr", "unicode": "1F1F5-1F1F7", "digest": "4910dc984bc908158506b770f28af56150cbb4509a4291947dfa2479b9e4b308" }, + { + "name": "pr", + "unicode": "1F1F5-1F1F7", + "digest": "4910dc984bc908158506b770f28af56150cbb4509a4291947dfa2479b9e4b308" + }, { "name": "flag_ps", "unicode": "1F1F5-1F1F8", "digest": "b2bca7619fced25de94d7bd398537857460348a552e7d73d189aef3f428e6a13" }, + { + "name": "ps", + "unicode": "1F1F5-1F1F8", + "digest": "b2bca7619fced25de94d7bd398537857460348a552e7d73d189aef3f428e6a13" + }, { "name": "flag_pt", "unicode": "1F1F5-1F1F9", "digest": "177282613b4b8b4d9551f1da6a1c3f66f1b96cf67c71c7d164213b26b3237395" }, + { + "name": "pt", + "unicode": "1F1F5-1F1F9", + "digest": "177282613b4b8b4d9551f1da6a1c3f66f1b96cf67c71c7d164213b26b3237395" + }, { "name": "flag_pw", "unicode": "1F1F5-1F1FC", "digest": "2ff42a14bdc7df76b5f989dca381f94765032b26ae47d47b97844abde458cefe" }, + { + "name": "pw", + "unicode": "1F1F5-1F1FC", + "digest": "2ff42a14bdc7df76b5f989dca381f94765032b26ae47d47b97844abde458cefe" + }, { "name": "flag_py", "unicode": "1F1F5-1F1FE", "digest": "80169b69a46c4c67d0090dc2c6bf05d1a14f133ac7ae56f811547e8e8f70d81b" }, + { + "name": "py", + "unicode": "1F1F5-1F1FE", + "digest": "80169b69a46c4c67d0090dc2c6bf05d1a14f133ac7ae56f811547e8e8f70d81b" + }, { "name": "flag_qa", "unicode": "1F1F6-1F1E6", "digest": "589b44b975aa97426afb8db7f8b355491fca246b693903485824bf0f5a6953a2" }, + { + "name": "qa", + "unicode": "1F1F6-1F1E6", + "digest": "589b44b975aa97426afb8db7f8b355491fca246b693903485824bf0f5a6953a2" + }, { "name": "flag_re", "unicode": "1F1F7-1F1EA", "digest": "77d242261742831a142c9ec74cd17d76b1e6d1af751ff3c6a356646744bc798a" }, + { + "name": "re", + "unicode": "1F1F7-1F1EA", + "digest": "77d242261742831a142c9ec74cd17d76b1e6d1af751ff3c6a356646744bc798a" + }, { "name": "flag_ro", "unicode": "1F1F7-1F1F4", "digest": "d7d17026ea81f27456983722540f9a23343a3a1b22e7697c4fba118ce8b4719e" }, + { + "name": "ro", + "unicode": "1F1F7-1F1F4", + "digest": "d7d17026ea81f27456983722540f9a23343a3a1b22e7697c4fba118ce8b4719e" + }, { "name": "flag_rs", "unicode": "1F1F7-1F1F8", "digest": "e466a18cc0368e623d3fe33a036c1e88db91ae24f7510e17caacc85c41f1bac8" }, + { + "name": "rs", + "unicode": "1F1F7-1F1F8", + "digest": "e466a18cc0368e623d3fe33a036c1e88db91ae24f7510e17caacc85c41f1bac8" + }, { "name": "flag_ru", "unicode": "1F1F7-1F1FA", "digest": "86bf53a62dfc4c434d910f43df70f430fc67c0070fe3fc466c4fbfd6a5d8e646" }, + { + "name": "ru", + "unicode": "1F1F7-1F1FA", + "digest": "86bf53a62dfc4c434d910f43df70f430fc67c0070fe3fc466c4fbfd6a5d8e646" + }, { "name": "flag_rw", "unicode": "1F1F7-1F1FC", "digest": "38ec5a01896c9747a8dbf865d5e8584770e587253b7af3d3b9c36cd993f67518" }, + { + "name": "rw", + "unicode": "1F1F7-1F1FC", + "digest": "38ec5a01896c9747a8dbf865d5e8584770e587253b7af3d3b9c36cd993f67518" + }, { "name": "flag_sa", "unicode": "1F1F8-1F1E6", "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0" }, + { + "name": "saudiarabia", + "unicode": "1F1F8-1F1E6", + "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0" + }, + { + "name": "saudi", + "unicode": "1F1F8-1F1E6", + "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0" + }, { "name": "flag_sb", "unicode": "1F1F8-1F1E7", "digest": "8ffa24c5cb92be4dbe43f6cd85b61b9608a3101bd78ebccff4fe99c209b3e241" }, + { + "name": "sb", + "unicode": "1F1F8-1F1E7", + "digest": "8ffa24c5cb92be4dbe43f6cd85b61b9608a3101bd78ebccff4fe99c209b3e241" + }, { "name": "flag_sc", "unicode": "1F1F8-1F1E8", "digest": "227d090ac2cbf317e594567b6114b5063a13cfe33abf990d37b200debcfadabb" }, + { + "name": "sc", + "unicode": "1F1F8-1F1E8", + "digest": "227d090ac2cbf317e594567b6114b5063a13cfe33abf990d37b200debcfadabb" + }, { "name": "flag_sd", "unicode": "1F1F8-1F1E9", "digest": "350f3332e8ea1138e54facc870dd0fea5f2ab7d3fd4baa02ed8627ae79642f6c" }, + { + "name": "sd", + "unicode": "1F1F8-1F1E9", + "digest": "350f3332e8ea1138e54facc870dd0fea5f2ab7d3fd4baa02ed8627ae79642f6c" + }, { "name": "flag_se", "unicode": "1F1F8-1F1EA", "digest": "c1b09f36c263727de83b54376f05e083a17a61941af9a1640b826629256a280d" }, + { + "name": "se", + "unicode": "1F1F8-1F1EA", + "digest": "c1b09f36c263727de83b54376f05e083a17a61941af9a1640b826629256a280d" + }, { "name": "flag_sg", "unicode": "1F1F8-1F1EC", "digest": "e6fc26920dfc07e4fd3c8d897de9c607e0bf48a3b64a13630c858d707a8e7660" }, + { + "name": "sg", + "unicode": "1F1F8-1F1EC", + "digest": "e6fc26920dfc07e4fd3c8d897de9c607e0bf48a3b64a13630c858d707a8e7660" + }, { "name": "flag_sh", "unicode": "1F1F8-1F1ED", "digest": "f2c22ab0eb49e3104c35f1c0268b1e63c3a67f41b0cfa9861b189525988e53b6" }, + { + "name": "sh", + "unicode": "1F1F8-1F1ED", + "digest": "f2c22ab0eb49e3104c35f1c0268b1e63c3a67f41b0cfa9861b189525988e53b6" + }, { "name": "flag_si", "unicode": "1F1F8-1F1EE", "digest": "1ef0b10e498f71591322f9d8ec122d39838f479370cf7ee922560986ef6c4f2e" }, + { + "name": "si", + "unicode": "1F1F8-1F1EE", + "digest": "1ef0b10e498f71591322f9d8ec122d39838f479370cf7ee922560986ef6c4f2e" + }, { "name": "flag_sj", "unicode": "1F1F8-1F1EF", "digest": "ce913b007f84a9cba2add8d754aa791901624c60e4200de426dfa25271cb0f78" }, + { + "name": "sj", + "unicode": "1F1F8-1F1EF", + "digest": "ce913b007f84a9cba2add8d754aa791901624c60e4200de426dfa25271cb0f78" + }, { "name": "flag_sk", "unicode": "1F1F8-1F1F0", "digest": "d8f8fc4024c82f906effe98facbef9d543fb3708b1134dc502c74dc4a442b30a" }, + { + "name": "sk", + "unicode": "1F1F8-1F1F0", + "digest": "d8f8fc4024c82f906effe98facbef9d543fb3708b1134dc502c74dc4a442b30a" + }, { "name": "flag_sl", "unicode": "1F1F8-1F1F1", "digest": "dd7fd0452498d8d1c894cf0d5a662ddff9c5bcc02148bdc3dc7e6f25d0bb586e" }, + { + "name": "sl", + "unicode": "1F1F8-1F1F1", + "digest": "dd7fd0452498d8d1c894cf0d5a662ddff9c5bcc02148bdc3dc7e6f25d0bb586e" + }, { "name": "flag_sm", "unicode": "1F1F8-1F1F2", "digest": "2b499606aee2b5cbf4037338753c80a4c8f75f4abcef2c8657bd9337e602bbd3" }, + { + "name": "sm", + "unicode": "1F1F8-1F1F2", + "digest": "2b499606aee2b5cbf4037338753c80a4c8f75f4abcef2c8657bd9337e602bbd3" + }, { "name": "flag_sn", "unicode": "1F1F8-1F1F3", "digest": "03b46a9d8b129da13f60c23b820b04fba52050ca58a41b859ad57d5c3cc2515d" }, + { + "name": "sn", + "unicode": "1F1F8-1F1F3", + "digest": "03b46a9d8b129da13f60c23b820b04fba52050ca58a41b859ad57d5c3cc2515d" + }, { "name": "flag_so", "unicode": "1F1F8-1F1F4", "digest": "ea416b6a05ddc5b16291ebe5101735360b08c834d55ac82c663ac1dd3e459048" }, + { + "name": "so", + "unicode": "1F1F8-1F1F4", + "digest": "ea416b6a05ddc5b16291ebe5101735360b08c834d55ac82c663ac1dd3e459048" + }, { "name": "flag_sr", "unicode": "1F1F8-1F1F7", "digest": "012179fbcbcb7343e7b09d33e283fb63c7964a6eca35ccb9407d468e495a9874" }, + { + "name": "sr", + "unicode": "1F1F8-1F1F7", + "digest": "012179fbcbcb7343e7b09d33e283fb63c7964a6eca35ccb9407d468e495a9874" + }, { "name": "flag_ss", "unicode": "1F1F8-1F1F8", "digest": "6723150482c640643c9dd7e33ea749f4a8b46aceacbd4f5e11aa33b3ee13aab7" }, + { + "name": "ss", + "unicode": "1F1F8-1F1F8", + "digest": "6723150482c640643c9dd7e33ea749f4a8b46aceacbd4f5e11aa33b3ee13aab7" + }, { "name": "flag_st", "unicode": "1F1F8-1F1F9", "digest": "0947fcec2e3cb1b0e9943c3d00891e8ee226e8d0532e9b1fe807ddf2e8fbc49d" }, + { + "name": "st", + "unicode": "1F1F8-1F1F9", + "digest": "0947fcec2e3cb1b0e9943c3d00891e8ee226e8d0532e9b1fe807ddf2e8fbc49d" + }, { "name": "flag_sv", "unicode": "1F1F8-1F1FB", "digest": "ce7e583db833c4b10e2f7a2d09b97bb522c02e96ea0b3f3a48a955f7d8f970d8" }, + { + "name": "sv", + "unicode": "1F1F8-1F1FB", + "digest": "ce7e583db833c4b10e2f7a2d09b97bb522c02e96ea0b3f3a48a955f7d8f970d8" + }, { "name": "flag_sx", "unicode": "1F1F8-1F1FD", "digest": "c01fb238c7ba439f24a5ef821b6457f2a0fd0b99a1b2d02395bed87f0a4a88e5" }, + { + "name": "sx", + "unicode": "1F1F8-1F1FD", + "digest": "c01fb238c7ba439f24a5ef821b6457f2a0fd0b99a1b2d02395bed87f0a4a88e5" + }, { "name": "flag_sy", "unicode": "1F1F8-1F1FE", "digest": "a77d87ef98c96140c59998d10d94837e2a056dd3ac5c7522e89e5c62eac69e69" }, + { + "name": "sy", + "unicode": "1F1F8-1F1FE", + "digest": "a77d87ef98c96140c59998d10d94837e2a056dd3ac5c7522e89e5c62eac69e69" + }, { "name": "flag_sz", "unicode": "1F1F8-1F1FF", "digest": "2904ad01040a9107ad556ec4c2561781d96746005cca250babb1127b8ba21050" }, + { + "name": "sz", + "unicode": "1F1F8-1F1FF", + "digest": "2904ad01040a9107ad556ec4c2561781d96746005cca250babb1127b8ba21050" + }, { "name": "flag_ta", "unicode": "1F1F9-1F1E6", "digest": "eda84db90e1a8854e8ff3c15b3b38ee65f7d6532b76970a6fbac304c30d8c959" }, + { + "name": "ta", + "unicode": "1F1F9-1F1E6", + "digest": "eda84db90e1a8854e8ff3c15b3b38ee65f7d6532b76970a6fbac304c30d8c959" + }, { "name": "flag_tc", "unicode": "1F1F9-1F1E8", "digest": "4628fdf6dc598a2846beefe97f7d4c6812f4961394cec132924b44bbe79b3322" }, + { + "name": "tc", + "unicode": "1F1F9-1F1E8", + "digest": "4628fdf6dc598a2846beefe97f7d4c6812f4961394cec132924b44bbe79b3322" + }, { "name": "flag_td", "unicode": "1F1F9-1F1E9", "digest": "125ff31e4285cb2a5493a52a2703ebe8e7138b918ec4dae3d0f8693632372df6" }, + { + "name": "td", + "unicode": "1F1F9-1F1E9", + "digest": "125ff31e4285cb2a5493a52a2703ebe8e7138b918ec4dae3d0f8693632372df6" + }, { "name": "flag_tf", "unicode": "1F1F9-1F1EB", "digest": "489d591e11764ac341f2234020f7879db782b8f673fc9aae425fd713e4082334" }, + { + "name": "tf", + "unicode": "1F1F9-1F1EB", + "digest": "489d591e11764ac341f2234020f7879db782b8f673fc9aae425fd713e4082334" + }, { "name": "flag_tg", "unicode": "1F1F9-1F1EC", "digest": "4ceedfcfcc22cd14d9add9d86d6748447995f19f7095fa4be883e21eb1aa86bc" }, + { + "name": "tg", + "unicode": "1F1F9-1F1EC", + "digest": "4ceedfcfcc22cd14d9add9d86d6748447995f19f7095fa4be883e21eb1aa86bc" + }, { "name": "flag_th", "unicode": "1F1F9-1F1ED", "digest": "2798cc660af1c5dc4891c30aded3a53d7cfa0af128cc495df8141907b165902d" }, + { + "name": "th", + "unicode": "1F1F9-1F1ED", + "digest": "2798cc660af1c5dc4891c30aded3a53d7cfa0af128cc495df8141907b165902d" + }, { "name": "flag_tj", "unicode": "1F1F9-1F1EF", "digest": "0483506fc5b5f2d4fc18ea3cd2f8a5da985d68fe4bf90bd3fd05e67e38f32398" }, + { + "name": "tj", + "unicode": "1F1F9-1F1EF", + "digest": "0483506fc5b5f2d4fc18ea3cd2f8a5da985d68fe4bf90bd3fd05e67e38f32398" + }, { "name": "flag_tk", "unicode": "1F1F9-1F1F0", "digest": "d5d4a8c6ce3207731b7c154a9d8d8fa2af055a48f03b3cbbcfd3317d3b8a75f2" }, + { + "name": "tk", + "unicode": "1F1F9-1F1F0", + "digest": "d5d4a8c6ce3207731b7c154a9d8d8fa2af055a48f03b3cbbcfd3317d3b8a75f2" + }, { "name": "flag_tl", "unicode": "1F1F9-1F1F1", "digest": "7a2ba8f91a6b627c60c88244223a9b9d0c12707f50b174f9c2eca07dd3440df7" }, + { + "name": "tl", + "unicode": "1F1F9-1F1F1", + "digest": "7a2ba8f91a6b627c60c88244223a9b9d0c12707f50b174f9c2eca07dd3440df7" + }, { "name": "flag_tm", "unicode": "1F1F9-1F1F2", "digest": "adcf5f23adcf983ce626b44559482f8728251eab34b3ff5d8b125112f3a1010f" }, + { + "name": "turkmenistan", + "unicode": "1F1F9-1F1F2", + "digest": "adcf5f23adcf983ce626b44559482f8728251eab34b3ff5d8b125112f3a1010f" + }, { "name": "flag_tn", "unicode": "1F1F9-1F1F3", "digest": "5ee690ee1f3c3c0cba9b36efdef902894ec59cefbc60c4baa341efd3d7bb9ba2" }, + { + "name": "tn", + "unicode": "1F1F9-1F1F3", + "digest": "5ee690ee1f3c3c0cba9b36efdef902894ec59cefbc60c4baa341efd3d7bb9ba2" + }, { "name": "flag_to", "unicode": "1F1F9-1F1F4", "digest": "cde8672ca25b0e3a423865283fab9bc3ab10f472e04979b3b2f8032b71e96300" }, + { + "name": "to", + "unicode": "1F1F9-1F1F4", + "digest": "cde8672ca25b0e3a423865283fab9bc3ab10f472e04979b3b2f8032b71e96300" + }, { "name": "flag_tr", "unicode": "1F1F9-1F1F7", "digest": "3d83c03ed084cfc81fa633310382acd7213e1eaa19d0ed97d142e7824032b55d" }, + { + "name": "tr", + "unicode": "1F1F9-1F1F7", + "digest": "3d83c03ed084cfc81fa633310382acd7213e1eaa19d0ed97d142e7824032b55d" + }, { "name": "flag_tt", "unicode": "1F1F9-1F1F9", "digest": "d66d272ac27e2b398289d6b60128ccd3508aeb1f4a00a3920c5e6a21bfe357ed" }, + { + "name": "tt", + "unicode": "1F1F9-1F1F9", + "digest": "d66d272ac27e2b398289d6b60128ccd3508aeb1f4a00a3920c5e6a21bfe357ed" + }, { "name": "flag_tv", "unicode": "1F1F9-1F1FB", "digest": "8716527383854cf1569f737d0f0f9ad77b46747255f24e02f5b2fbc850c2e35c" }, + { + "name": "tuvalu", + "unicode": "1F1F9-1F1FB", + "digest": "8716527383854cf1569f737d0f0f9ad77b46747255f24e02f5b2fbc850c2e35c" + }, { "name": "flag_tw", "unicode": "1F1F9-1F1FC", "digest": "fb17b97e18e4423c5f60d60ec3ec60b917be579fc4dd9b5b23236786dcb35108" }, + { + "name": "tw", + "unicode": "1F1F9-1F1FC", + "digest": "fb17b97e18e4423c5f60d60ec3ec60b917be579fc4dd9b5b23236786dcb35108" + }, { "name": "flag_tz", "unicode": "1F1F9-1F1FF", "digest": "a8a8cf57ae5227cb54620bf31d2d6e154d2067d6d049b8db64bc4e538222948b" }, { - "name": "flag_ua", + "name": "tz", + "unicode": "1F1F9-1F1FF", + "digest": "a8a8cf57ae5227cb54620bf31d2d6e154d2067d6d049b8db64bc4e538222948b" + }, + { + "name": "flag_ua", + "unicode": "1F1FA-1F1E6", + "digest": "03aca4b3ffd60d944a5793eb7530f8d8ae527782f642f6606194e46ee314b12c" + }, + { + "name": "ua", "unicode": "1F1FA-1F1E6", "digest": "03aca4b3ffd60d944a5793eb7530f8d8ae527782f642f6606194e46ee314b12c" }, @@ -3549,106 +5079,211 @@ "unicode": "1F1FA-1F1EC", "digest": "70226a1585e88390b3b815b8b79a0ddb36d2961c6b465c4ff72aa444abfe982e" }, + { + "name": "ug", + "unicode": "1F1FA-1F1EC", + "digest": "70226a1585e88390b3b815b8b79a0ddb36d2961c6b465c4ff72aa444abfe982e" + }, { "name": "flag_um", "unicode": "1F1FA-1F1F2", "digest": "aa83bf051149acf907140a860de5de1700710e4164ae5549ad1040b24d0a142b" }, + { + "name": "um", + "unicode": "1F1FA-1F1F2", + "digest": "aa83bf051149acf907140a860de5de1700710e4164ae5549ad1040b24d0a142b" + }, { "name": "flag_us", "unicode": "1F1FA-1F1F8", "digest": "32ba2aa09a30514247e91d60762791b582f547a37d9151f98b700dff50f355ea" }, + { + "name": "us", + "unicode": "1F1FA-1F1F8", + "digest": "32ba2aa09a30514247e91d60762791b582f547a37d9151f98b700dff50f355ea" + }, { "name": "flag_uy", "unicode": "1F1FA-1F1FE", "digest": "0e01b3f1df4bdf6d616dacc9c5825151b941bf074be750e8b24a07ea5d5bcacb" }, + { + "name": "uy", + "unicode": "1F1FA-1F1FE", + "digest": "0e01b3f1df4bdf6d616dacc9c5825151b941bf074be750e8b24a07ea5d5bcacb" + }, { "name": "flag_uz", "unicode": "1F1FA-1F1FF", "digest": "903029ce83812a2134f24b65db35b183443a440ea5fecaa6ef7dcaaf65b2519c" }, + { + "name": "uz", + "unicode": "1F1FA-1F1FF", + "digest": "903029ce83812a2134f24b65db35b183443a440ea5fecaa6ef7dcaaf65b2519c" + }, { "name": "flag_va", "unicode": "1F1FB-1F1E6", "digest": "fd3c1c5d0ac030e838f807288912c98a3e258f87901e252e46942a4dab9f8cb7" }, + { + "name": "va", + "unicode": "1F1FB-1F1E6", + "digest": "fd3c1c5d0ac030e838f807288912c98a3e258f87901e252e46942a4dab9f8cb7" + }, { "name": "flag_vc", "unicode": "1F1FB-1F1E8", "digest": "7cd554ea8ca817b5366701160274587ab44167ae5a89c430bbaf237ea18b7421" }, + { + "name": "vc", + "unicode": "1F1FB-1F1E8", + "digest": "7cd554ea8ca817b5366701160274587ab44167ae5a89c430bbaf237ea18b7421" + }, { "name": "flag_ve", "unicode": "1F1FB-1F1EA", "digest": "72930094fb088c1facabea07616035ec4771374358a90c3045219d087b350dd8" }, + { + "name": "ve", + "unicode": "1F1FB-1F1EA", + "digest": "72930094fb088c1facabea07616035ec4771374358a90c3045219d087b350dd8" + }, { "name": "flag_vg", "unicode": "1F1FB-1F1EC", "digest": "78a59afd368b7a8312bfdb2f49927ff09e6b8f46aab0136c0453e3319e81df49" }, + { + "name": "vg", + "unicode": "1F1FB-1F1EC", + "digest": "78a59afd368b7a8312bfdb2f49927ff09e6b8f46aab0136c0453e3319e81df49" + }, { "name": "flag_vi", "unicode": "1F1FB-1F1EE", "digest": "e070879f9605a9bae66bb84f2abf5a40c8b264baee65cd4f7a6720b826739f29" }, + { + "name": "vi", + "unicode": "1F1FB-1F1EE", + "digest": "e070879f9605a9bae66bb84f2abf5a40c8b264baee65cd4f7a6720b826739f29" + }, { "name": "flag_vn", "unicode": "1F1FB-1F1F3", "digest": "100ddf06e0f239b170f4d6cb459450bf4945281ee818f7d3c061828b80562219" }, + { + "name": "vn", + "unicode": "1F1FB-1F1F3", + "digest": "100ddf06e0f239b170f4d6cb459450bf4945281ee818f7d3c061828b80562219" + }, { "name": "flag_vu", "unicode": "1F1FB-1F1FA", "digest": "59fc9d16818295bba4f7f551598f85378cd07f2bd7e31a4eef2589aaa3847563" }, + { + "name": "vu", + "unicode": "1F1FB-1F1FA", + "digest": "59fc9d16818295bba4f7f551598f85378cd07f2bd7e31a4eef2589aaa3847563" + }, { "name": "flag_wf", "unicode": "1F1FC-1F1EB", "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" }, + { + "name": "wf", + "unicode": "1F1FC-1F1EB", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, { "name": "flag_white", "unicode": "1F3F3", "digest": "96307e3a28e92d1e7147a06f154ffc291ee3cd1765cf8b7bfb06412294112559" }, + { + "name": "waving_white_flag", + "unicode": "1F3F3", + "digest": "96307e3a28e92d1e7147a06f154ffc291ee3cd1765cf8b7bfb06412294112559" + }, { "name": "flag_ws", "unicode": "1F1FC-1F1F8", "digest": "0c95271d0f4b23f0d215ee0fba05cf08ecb70665d4c028e17463ecda2754b164" }, + { + "name": "ws", + "unicode": "1F1FC-1F1F8", + "digest": "0c95271d0f4b23f0d215ee0fba05cf08ecb70665d4c028e17463ecda2754b164" + }, { "name": "flag_xk", "unicode": "1F1FD-1F1F0", "digest": "713aa7d228e96f4a06d58d1fb8c2a55296c3e56842f8177ca936f3e09f50da1e" }, + { + "name": "xk", + "unicode": "1F1FD-1F1F0", + "digest": "713aa7d228e96f4a06d58d1fb8c2a55296c3e56842f8177ca936f3e09f50da1e" + }, { "name": "flag_ye", "unicode": "1F1FE-1F1EA", "digest": "3bb65bae9c913357bcae8b8b5878efc9e194ca308442ab69639c29716b49f078" }, + { + "name": "ye", + "unicode": "1F1FE-1F1EA", + "digest": "3bb65bae9c913357bcae8b8b5878efc9e194ca308442ab69639c29716b49f078" + }, { "name": "flag_yt", "unicode": "1F1FE-1F1F9", "digest": "f86c86f4c194610a3af78971fcf221ad97b9499d08f6d64476e417a2f52a611e" }, + { + "name": "yt", + "unicode": "1F1FE-1F1F9", + "digest": "f86c86f4c194610a3af78971fcf221ad97b9499d08f6d64476e417a2f52a611e" + }, { "name": "flag_za", "unicode": "1F1FF-1F1E6", "digest": "4dd4fa49a01fdcfc7c1c099a7869e0e9acba83a6a3debf6c8505ada4c796b872" }, + { + "name": "za", + "unicode": "1F1FF-1F1E6", + "digest": "4dd4fa49a01fdcfc7c1c099a7869e0e9acba83a6a3debf6c8505ada4c796b872" + }, { "name": "flag_zm", "unicode": "1F1FF-1F1F2", "digest": "ab6790d89875447de3d1c7f4713b102761bc3e9afdd714b818689e175ca03011" }, + { + "name": "zm", + "unicode": "1F1FF-1F1F2", + "digest": "ab6790d89875447de3d1c7f4713b102761bc3e9afdd714b818689e175ca03011" + }, { "name": "flag_zw", "unicode": "1F1FF-1F1FC", "digest": "9d39b934fe922174b2250f2cd1b174a548d2904091d3298f35b7cc59fbceb181" }, + { + "name": "zw", + "unicode": "1F1FF-1F1FC", + "digest": "9d39b934fe922174b2250f2cd1b174a548d2904091d3298f35b7cc59fbceb181" + }, { "name": "flags", "unicode": "1F38F", @@ -3669,11 +5304,21 @@ "unicode": "1F581", "digest": "be59efba4bc0759af5a726c06619090ef5071bf2541611d71691dedecee6c697" }, + { + "name": "clamshell_mobile_phone", + "unicode": "1F581", + "digest": "be59efba4bc0759af5a726c06619090ef5071bf2541611d71691dedecee6c697" + }, { "name": "floppy_black", "unicode": "1F5AA", "digest": "9022f51bb09c5130c6d46bb2accb159bed6f54d6fbffda6ecad62965ebc958ea" }, + { + "name": "black_hard_shell_floppy_disk", + "unicode": "1F5AA", + "digest": "9022f51bb09c5130c6d46bb2accb159bed6f54d6fbffda6ecad62965ebc958ea" + }, { "name": "floppy_disk", "unicode": "1F4BE", @@ -3684,6 +5329,11 @@ "unicode": "1F5AB", "digest": "ec79c400117c4506ef8cf3eebef6c42dd37e60b3079d3e98b6ccd06e517e2af0" }, + { + "name": "white_hard_shell_floppy_disk", + "unicode": "1F5AB", + "digest": "ec79c400117c4506ef8cf3eebef6c42dd37e60b3079d3e98b6ccd06e517e2af0" + }, { "name": "flower_playing_cards", "unicode": "1F3B4", @@ -3714,6 +5364,11 @@ "unicode": "1F5C1", "digest": "74f3b484771c3d6ef61cf003de25c1a59b875afa46c057b5b1d92d9f99460685" }, + { + "name": "open_folder", + "unicode": "1F5C1", + "digest": "74f3b484771c3d6ef61cf003de25c1a59b875afa46c057b5b1d92d9f99460685" + }, { "name": "football", "unicode": "1F3C8", @@ -3734,6 +5389,11 @@ "unicode": "1F37D", "digest": "b4081b9edea6cdab5112fdd17535051ba17710953013f5020c7c40f84a1e3247" }, + { + "name": "fork_and_knife_with_plate", + "unicode": "1F37D", + "digest": "b4081b9edea6cdab5112fdd17535051ba17710953013f5020c7c40f84a1e3247" + }, { "name": "fountain", "unicode": "26F2", @@ -3754,16 +5414,31 @@ "unicode": "1F5BC", "digest": "6ff21063063989c6ae7dd69f4d6a781c676f9dba380d8e6f1dbac5d53b24f349" }, + { + "name": "frame_with_picture", + "unicode": "1F5BC", + "digest": "6ff21063063989c6ae7dd69f4d6a781c676f9dba380d8e6f1dbac5d53b24f349" + }, { "name": "frame_tiles", "unicode": "1F5BD", "digest": "34a5bb044b4b3ad94b116ad106f7b6747fb8612dc0e9f8ccd4313c2920508df0" }, + { + "name": "frame_with_tiles", + "unicode": "1F5BD", + "digest": "34a5bb044b4b3ad94b116ad106f7b6747fb8612dc0e9f8ccd4313c2920508df0" + }, { "name": "frame_x", "unicode": "1F5BE", "digest": "2e427688fd70361c8c59787d0722ad68abe1c3f968258ee99c0c77ce4b8a8e15" }, + { + "name": "frame_with_an_x", + "unicode": "1F5BE", + "digest": "2e427688fd70361c8c59787d0722ad68abe1c3f968258ee99c0c77ce4b8a8e15" + }, { "name": "free", "unicode": "1F193", @@ -3789,11 +5464,21 @@ "unicode": "1F626", "digest": "fb39f5c2aea98054adb02a3a0ac34a2e38d83f32cd590e9d2449e06a9702f2f5" }, + { + "name": "anguished", + "unicode": "1F626", + "digest": "fb39f5c2aea98054adb02a3a0ac34a2e38d83f32cd590e9d2449e06a9702f2f5" + }, { "name": "frowning2", "unicode": "2639", "digest": "7bb6c682a6c9f98bf3a5ae986e317fd26d1af497c857500deec2f06b6a3af5da" }, + { + "name": "white_frowning_face", + "unicode": "2639", + "digest": "7bb6c682a6c9f98bf3a5ae986e317fd26d1af497c857500deec2f06b6a3af5da" + }, { "name": "fuelpump", "unicode": "26FD", @@ -4029,6 +5714,11 @@ "unicode": "2692", "digest": "2e4fe33406ca03fbb0df1596d63e903d8ee6bd78ecc3ec38a67dd2cecbc584e2" }, + { + "name": "hammer_and_pick", + "unicode": "2692", + "digest": "2e4fe33406ca03fbb0df1596d63e903d8ee6bd78ecc3ec38a67dd2cecbc584e2" + }, { "name": "hamster", "unicode": "1F439", @@ -4039,41 +5729,81 @@ "unicode": "1F590", "digest": "a43e52f7cdec5e9d51497888b0988d7bbd42846ad7e492b196293fbce576d197" }, + { + "name": "raised_hand_with_fingers_splayed", + "unicode": "1F590", + "digest": "a43e52f7cdec5e9d51497888b0988d7bbd42846ad7e492b196293fbce576d197" + }, { "name": "hand_splayed_reverse", "unicode": "1F591", "digest": "ff0af0fe9def7388adca6836e5958492282b1afae99f1b6e1e65d11ba68b96db" }, + { + "name": "reversed_raised_hand_with_fingers_splayed", + "unicode": "1F591", + "digest": "ff0af0fe9def7388adca6836e5958492282b1afae99f1b6e1e65d11ba68b96db" + }, { "name": "hand_splayed_tone1", "unicode": "1F590-1F3FB", "digest": "73cceec7117280d330f8a149979190f0f355dd8d0a92821be89fb70344bb8dfe" }, + { + "name": "raised_hand_with_fingers_splayed_tone1", + "unicode": "1F590-1F3FB", + "digest": "73cceec7117280d330f8a149979190f0f355dd8d0a92821be89fb70344bb8dfe" + }, { "name": "hand_splayed_tone2", "unicode": "1F590-1F3FC", "digest": "b06fac698128f4c3a7b8ea56e8bc4de088bb5461aa0f9c84553f16b43d347145" }, + { + "name": "raised_hand_with_fingers_splayed_tone2", + "unicode": "1F590-1F3FC", + "digest": "b06fac698128f4c3a7b8ea56e8bc4de088bb5461aa0f9c84553f16b43d347145" + }, { "name": "hand_splayed_tone3", "unicode": "1F590-1F3FD", "digest": "a94ee9a2f8cdec6d2f7dd6887d1c7b8e064fcad63030c2c7c001742d72b5603e" }, + { + "name": "raised_hand_with_fingers_splayed_tone3", + "unicode": "1F590-1F3FD", + "digest": "a94ee9a2f8cdec6d2f7dd6887d1c7b8e064fcad63030c2c7c001742d72b5603e" + }, { "name": "hand_splayed_tone4", "unicode": "1F590-1F3FE", "digest": "501792b4126c6f32e755accee0fc8b4d1915e1d36c4ceaa40f3bd0066efe76c3" }, + { + "name": "raised_hand_with_fingers_splayed_tone4", + "unicode": "1F590-1F3FE", + "digest": "501792b4126c6f32e755accee0fc8b4d1915e1d36c4ceaa40f3bd0066efe76c3" + }, { "name": "hand_splayed_tone5", "unicode": "1F590-1F3FF", "digest": "22ed533d587cf44f286e2d6ad77be20b4b5f133c422af4ca51e9af86a75002d8" }, + { + "name": "raised_hand_with_fingers_splayed_tone5", + "unicode": "1F590-1F3FF", + "digest": "22ed533d587cf44f286e2d6ad77be20b4b5f133c422af4ca51e9af86a75002d8" + }, { "name": "hand_victory", "unicode": "1F594", "digest": "2d512ced4e8a438f2a346aed67310d3080f9828c748ade1be95943c32ba1c735" }, + { + "name": "reversed_victory_hand", + "unicode": "1F594", + "digest": "2d512ced4e8a438f2a346aed67310d3080f9828c748ade1be95943c32ba1c735" + }, { "name": "handbag", "unicode": "1F45C", @@ -4104,6 +5834,11 @@ "unicode": "1F915", "digest": "d690b740ff4f58e89dfc764c6411a4e84cfedffd7694eb5efa839a642dbabd08" }, + { + "name": "face_with_head_bandage", + "unicode": "1F915", + "digest": "d690b740ff4f58e89dfc764c6411a4e84cfedffd7694eb5efa839a642dbabd08" + }, { "name": "headphones", "unicode": "1F3A7", @@ -4129,6 +5864,11 @@ "unicode": "2763", "digest": "9751c89dcf10805f2011949ff3ddcb6bcb13de8c32ae5de9e03955e8a4235df2" }, + { + "name": "heavy_heart_exclamation_mark_ornament", + "unicode": "2763", + "digest": "9751c89dcf10805f2011949ff3ddcb6bcb13de8c32ae5de9e03955e8a4235df2" + }, { "name": "heart_eyes", "unicode": "1F60D", @@ -4144,6 +5884,11 @@ "unicode": "1F394", "digest": "2178829e2c85accda55d2f685544587f6de5c8398a127ae1e08ff1c4ab282204" }, + { + "name": "heart_with_tip_on_the_left", + "unicode": "1F394", + "digest": "2178829e2c85accda55d2f685544587f6de5c8398a127ae1e08ff1c4ab282204" + }, { "name": "heartbeat", "unicode": "1F493", @@ -4199,6 +5944,11 @@ "unicode": "26D1", "digest": "affbe9dd87b87ff9235b4858c59c2a73e9ed30dd5221e5b666b8d7747378a9c4" }, + { + "name": "helmet_with_white_cross", + "unicode": "26D1", + "digest": "affbe9dd87b87ff9235b4858c59c2a73e9ed30dd5221e5b666b8d7747378a9c4" + }, { "name": "herb", "unicode": "1F33F", @@ -4234,6 +5984,11 @@ "unicode": "1F3D8", "digest": "9980d6dd6cbd23b820747ecac4cb10974dd24b0c94b4acfe21fa87793ad065c9" }, + { + "name": "house_buildings", + "unicode": "1F3D8", + "digest": "9980d6dd6cbd23b820747ecac4cb10974dd24b0c94b4acfe21fa87793ad065c9" + }, { "name": "honey_pot", "unicode": "1F36F", @@ -4289,6 +6044,11 @@ "unicode": "1F32D", "digest": "58b829e26b5c4642942898d9c7873cb08e048fd7deaacba8292899d5d895cb2b" }, + { + "name": "hot_dog", + "unicode": "1F32D", + "digest": "58b829e26b5c4642942898d9c7873cb08e048fd7deaacba8292899d5d895cb2b" + }, { "name": "hotel", "unicode": "1F3E8", @@ -4319,6 +6079,11 @@ "unicode": "1F3DA", "digest": "e404631e3a296bdeae3de7510da8934c32327bc0fa0f7ae4e676b61932165668" }, + { + "name": "derelict_house_building", + "unicode": "1F3DA", + "digest": "e404631e3a296bdeae3de7510da8934c32327bc0fa0f7ae4e676b61932165668" + }, { "name": "house_with_garden", "unicode": "1F3E1", @@ -4329,6 +6094,11 @@ "unicode": "1F917", "digest": "68ed6c4e0eae9071cf67770a39e07a2290b4f7763170f765b3cd3ac67ae43240" }, + { + "name": "hugging_face", + "unicode": "1F917", + "digest": "68ed6c4e0eae9071cf67770a39e07a2290b4f7763170f765b3cd3ac67ae43240" + }, { "name": "hushed", "unicode": "1F62F", @@ -4379,6 +6149,11 @@ "unicode": "1F6C8", "digest": "59c35e77d5ee663c5d56f7d8af845ce8aeb9935e526ae4a06e02ae70e71212ca" }, + { + "name": "circled_information_source", + "unicode": "1F6C8", + "digest": "59c35e77d5ee663c5d56f7d8af845ce8aeb9935e526ae4a06e02ae70e71212ca" + }, { "name": "information_desk_person", "unicode": "1F481", @@ -4434,6 +6209,11 @@ "unicode": "1F3DD", "digest": "17f02b309b62ed9542b1d8943168302846040e420f413e56d799bb5fba7064fa" }, + { + "name": "desert_island", + "unicode": "1F3DD", + "digest": "17f02b309b62ed9542b1d8943168302846040e420f413e56d799bb5fba7064fa" + }, { "name": "izakaya_lantern", "unicode": "1F3EE", @@ -4474,6 +6254,11 @@ "unicode": "1F6E6", "digest": "3708e5e034b1c64d1268d66527e13c369aa0f8903bce9172bef773b2d1940948" }, + { + "name": "up_pointing_military_airplane", + "unicode": "1F6E6", + "digest": "3708e5e034b1c64d1268d66527e13c369aa0f8903bce9172bef773b2d1940948" + }, { "name": "joy", "unicode": "1F602", @@ -4504,21 +6289,41 @@ "unicode": "1F5DD", "digest": "87a7d42531d7a11dcb11b0d6d1be611ee8cec35b5d22226a8ac6083fedef4f5d" }, + { + "name": "old_key", + "unicode": "1F5DD", + "digest": "87a7d42531d7a11dcb11b0d6d1be611ee8cec35b5d22226a8ac6083fedef4f5d" + }, { "name": "keyboard", "unicode": "1F5AE", "digest": "3b254cbf19946df3af05e501d11653d89fcda91684b7248d86186f842b83bf16" }, + { + "name": "wired_keyboard", + "unicode": "1F5AE", + "digest": "3b254cbf19946df3af05e501d11653d89fcda91684b7248d86186f842b83bf16" + }, { "name": "keyboard_mouse", "unicode": "1F5A6", "digest": "95b523e55d8afeaeb06442bbe20e47f49643bb0c32d89a8cdbbccdead20532b3" }, + { + "name": "keyboard_and_mouse", + "unicode": "1F5A6", + "digest": "95b523e55d8afeaeb06442bbe20e47f49643bb0c32d89a8cdbbccdead20532b3" + }, { "name": "keyboard_with_jacks", "unicode": "1F398", "digest": "e29a0d0b8018d13458469edca13c60a882a2817957c1aa11b050684c995a47ee" }, + { + "name": "musical_keyboard_with_jacks", + "unicode": "1F398", + "digest": "e29a0d0b8018d13458469edca13c60a882a2817957c1aa11b050684c995a47ee" + }, { "name": "keycap_ten", "unicode": "1F51F", @@ -4539,11 +6344,21 @@ "unicode": "1F468-2764-1F48B-1F468", "digest": "381364ad988ec07cc3708fd60f71838092224009088fff587069b4e8ab01ee63" }, + { + "name": "couplekiss_mm", + "unicode": "1F468-2764-1F48B-1F468", + "digest": "381364ad988ec07cc3708fd60f71838092224009088fff587069b4e8ab01ee63" + }, { "name": "kiss_ww", "unicode": "1F469-2764-1F48B-1F469", "digest": "7705ca707b73f44c856ea324bdfe30ed05244c8d192d1111f6e1d62ab3f2f8a5" }, + { + "name": "couplekiss_ww", + "unicode": "1F469-2764-1F48B-1F469", + "digest": "7705ca707b73f44c856ea324bdfe30ed05244c8d192d1111f6e1d62ab3f2f8a5" + }, { "name": "kissing", "unicode": "1F617", @@ -4619,6 +6434,11 @@ "unicode": "1F606", "digest": "f22d3be77f1daf058d04c3cbc1fd7f76b4dc069d2d300b45e63e768b08d269c5" }, + { + "name": "satisfied", + "unicode": "1F606", + "digest": "f22d3be77f1daf058d04c3cbc1fd7f76b4dc069d2d300b45e63e768b08d269c5" + }, { "name": "leaves", "unicode": "1F343", @@ -4639,6 +6459,11 @@ "unicode": "1F57B", "digest": "8052e44951afee04c87296128744b5019ec783c9ed1a231f659af6c8ddaa50f3" }, + { + "name": "left_hand_telephone_receiver", + "unicode": "1F57B", + "digest": "8052e44951afee04c87296128744b5019ec783c9ed1a231f659af6c8ddaa50f3" + }, { "name": "left_right_arrow", "unicode": "2194", @@ -4674,6 +6499,11 @@ "unicode": "1F574", "digest": "3e4e9a5ac6a8dbd7909c58a9d915f16f1a0fc59cc019714ae5935f18e4704044" }, + { + "name": "man_in_business_suit_levitating", + "unicode": "1F574", + "digest": "3e4e9a5ac6a8dbd7909c58a9d915f16f1a0fc59cc019714ae5935f18e4704044" + }, { "name": "libra", "unicode": "264E", @@ -4684,36 +6514,71 @@ "unicode": "1F3CB", "digest": "f64db037fd21e5918e5de35d6a561ef4b44668e307ed351338de00fcf3e771e3" }, + { + "name": "weight_lifter", + "unicode": "1F3CB", + "digest": "f64db037fd21e5918e5de35d6a561ef4b44668e307ed351338de00fcf3e771e3" + }, { "name": "lifter_tone1", "unicode": "1F3CB-1F3FB", "digest": "f9e0d161b12c4908ac3409b11c1a77ee38f33ba018f12416545876214bfb7c01" }, + { + "name": "weight_lifter_tone1", + "unicode": "1F3CB-1F3FB", + "digest": "f9e0d161b12c4908ac3409b11c1a77ee38f33ba018f12416545876214bfb7c01" + }, { "name": "lifter_tone2", "unicode": "1F3CB-1F3FC", "digest": "631eb6ed5bd147dc6f1f8b94149abe44d62a0f78e7809e37a4bfe127c40ed98f" }, + { + "name": "weight_lifter_tone2", + "unicode": "1F3CB-1F3FC", + "digest": "631eb6ed5bd147dc6f1f8b94149abe44d62a0f78e7809e37a4bfe127c40ed98f" + }, { "name": "lifter_tone3", "unicode": "1F3CB-1F3FD", "digest": "406b5707a47d9066f016acf0b64fa695e3505acc2453758a0428de21efd7eb6d" }, + { + "name": "weight_lifter_tone3", + "unicode": "1F3CB-1F3FD", + "digest": "406b5707a47d9066f016acf0b64fa695e3505acc2453758a0428de21efd7eb6d" + }, { "name": "lifter_tone4", "unicode": "1F3CB-1F3FE", "digest": "d917164ed8c4bb1ffcc887ca256ec329e7fa1b9516eaf8c159f8b43fdb071ed6" }, + { + "name": "weight_lifter_tone4", + "unicode": "1F3CB-1F3FE", + "digest": "d917164ed8c4bb1ffcc887ca256ec329e7fa1b9516eaf8c159f8b43fdb071ed6" + }, { "name": "lifter_tone5", "unicode": "1F3CB-1F3FF", "digest": "f79ea93e8a40b3c895b693bf49eb4ce6e7b3f4413595e5881ea44839fd7fe8e5" }, + { + "name": "weight_lifter_tone5", + "unicode": "1F3CB-1F3FF", + "digest": "f79ea93e8a40b3c895b693bf49eb4ce6e7b3f4413595e5881ea44839fd7fe8e5" + }, { "name": "light_check_mark", "unicode": "1F5F8", "digest": "7842b0df8c2b6703bed0cce5d2790d394eec7120b2a245a76f375528f2729a7b" }, + { + "name": "light_mark", + "unicode": "1F5F8", + "digest": "7842b0df8c2b6703bed0cce5d2790d394eec7120b2a245a76f375528f2729a7b" + }, { "name": "light_rail", "unicode": "1F688", @@ -4729,6 +6594,11 @@ "unicode": "1F981", "digest": "935b1076815f51fafcd860a395d0a03c536acfcea61ffcf542a377da046fa7d9" }, + { + "name": "lion", + "unicode": "1F981", + "digest": "935b1076815f51fafcd860a395d0a03c536acfcea61ffcf542a377da046fa7d9" + }, { "name": "lips", "unicode": "1F444", @@ -4929,6 +6799,11 @@ "unicode": "1F5FA", "digest": "f56116d09996d6d08fb5cdfb46622b545253f2649008170fc2011a9713fa875b" }, + { + "name": "world_map", + "unicode": "1F5FA", + "digest": "f56116d09996d6d08fb5cdfb46622b545253f2649008170fc2011a9713fa875b" + }, { "name": "maple_leaf", "unicode": "1F341", @@ -4979,6 +6854,11 @@ "unicode": "1F3C5", "digest": "270d438b6e2155e944dc734ea3e4d02409e51f59db2db636398fbf96e5edb0e6" }, + { + "name": "sports_medal", + "unicode": "1F3C5", + "digest": "270d438b6e2155e944dc734ea3e4d02409e51f59db2db636398fbf96e5edb0e6" + }, { "name": "mega", "unicode": "1F4E3", @@ -5004,31 +6884,61 @@ "unicode": "1F918", "digest": "45e5fac0b9b019cf217dcfd1380cafb0d03063454612178278dac1ca5f8476a6" }, + { + "name": "sign_of_the_horns", + "unicode": "1F918", + "digest": "45e5fac0b9b019cf217dcfd1380cafb0d03063454612178278dac1ca5f8476a6" + }, { "name": "metal_tone1", "unicode": "1F918-1F3FB", "digest": "9b3596fe7c063df838f0a43fb680ce10fb88e2b73c5c3324abfa357a224c17aa" }, + { + "name": "sign_of_the_horns_tone1", + "unicode": "1F918-1F3FB", + "digest": "9b3596fe7c063df838f0a43fb680ce10fb88e2b73c5c3324abfa357a224c17aa" + }, { "name": "metal_tone2", "unicode": "1F918-1F3FC", "digest": "e15a4898a0efca4354ac48d6b01ff0618ce8b110b1246a4f5d78e19b54658be6" }, + { + "name": "sign_of_the_horns_tone2", + "unicode": "1F918-1F3FC", + "digest": "e15a4898a0efca4354ac48d6b01ff0618ce8b110b1246a4f5d78e19b54658be6" + }, { "name": "metal_tone3", "unicode": "1F918-1F3FD", "digest": "c159e8179cb1907c246b432d87c5253b914fd7cebb6ac05292c4e38eff4815b0" }, + { + "name": "sign_of_the_horns_tone3", + "unicode": "1F918-1F3FD", + "digest": "c159e8179cb1907c246b432d87c5253b914fd7cebb6ac05292c4e38eff4815b0" + }, { "name": "metal_tone4", "unicode": "1F918-1F3FE", "digest": "a8a43a88028c97074321e3da56df1045db41ede58bf286c21d7ae90f222f2011" }, + { + "name": "sign_of_the_horns_tone4", + "unicode": "1F918-1F3FE", + "digest": "a8a43a88028c97074321e3da56df1045db41ede58bf286c21d7ae90f222f2011" + }, { "name": "metal_tone5", "unicode": "1F918-1F3FF", "digest": "e6611e826e867e2c73a8cadb138e4aa6365e3583dd229ff24b3e8f161904bf56" }, + { + "name": "sign_of_the_horns_tone5", + "unicode": "1F918-1F3FF", + "digest": "e6611e826e867e2c73a8cadb138e4aa6365e3583dd229ff24b3e8f161904bf56" + }, { "name": "metro", "unicode": "1F687", @@ -5044,6 +6954,11 @@ "unicode": "1F399", "digest": "f9df32cd207808f67a895d3460a215d1ecc42e377907bcd64731c02b697d4f32" }, + { + "name": "studio_microphone", + "unicode": "1F399", + "digest": "f9df32cd207808f67a895d3460a215d1ecc42e377907bcd64731c02b697d4f32" + }, { "name": "microscope", "unicode": "1F52C", @@ -5054,31 +6969,61 @@ "unicode": "1F595", "digest": "c6320b236a4a9593aeade511b52dd3114207e947458cb3b818c78737a505fdf6" }, + { + "name": "reversed_hand_with_middle_finger_extended", + "unicode": "1F595", + "digest": "c6320b236a4a9593aeade511b52dd3114207e947458cb3b818c78737a505fdf6" + }, { "name": "middle_finger_tone1", "unicode": "1F595-1F3FB", "digest": "93c7aa994856185519d576cb779bdcff3a33f7077eef98e70968125f92f02448" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone1", + "unicode": "1F595-1F3FB", + "digest": "93c7aa994856185519d576cb779bdcff3a33f7077eef98e70968125f92f02448" + }, { "name": "middle_finger_tone2", "unicode": "1F595-1F3FC", "digest": "a0de802294717b80e08d9d30f5fd64eacb90b5b3b9d7a0c27d6226a22822597f" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone2", + "unicode": "1F595-1F3FC", + "digest": "a0de802294717b80e08d9d30f5fd64eacb90b5b3b9d7a0c27d6226a22822597f" + }, { "name": "middle_finger_tone3", "unicode": "1F595-1F3FD", "digest": "8bbbab07c838257416bbf8377904362c07019fca9d5abf9fd048ccf6370178da" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone3", + "unicode": "1F595-1F3FD", + "digest": "8bbbab07c838257416bbf8377904362c07019fca9d5abf9fd048ccf6370178da" + }, { "name": "middle_finger_tone4", "unicode": "1F595-1F3FE", "digest": "d9eed8db540fdb669c6ae5ef168b77659660589f5ddd9b66062274d335a3ef04" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone4", + "unicode": "1F595-1F3FE", + "digest": "d9eed8db540fdb669c6ae5ef168b77659660589f5ddd9b66062274d335a3ef04" + }, { "name": "middle_finger_tone5", "unicode": "1F595-1F3FF", "digest": "0519c3298040e57db202294476df239edb9b23b44848bab296bc45eda7cf8664" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone5", + "unicode": "1F595-1F3FF", + "digest": "0519c3298040e57db202294476df239edb9b23b44848bab296bc45eda7cf8664" + }, { "name": "military_medal", "unicode": "1F396", @@ -5109,6 +7054,11 @@ "unicode": "1F911", "digest": "3ac2f9b5409e1426eef6966938ca04cf78aeffefd43f44b6c86af4af7836e22f" }, + { + "name": "money_mouth_face", + "unicode": "1F911", + "digest": "3ac2f9b5409e1426eef6966938ca04cf78aeffefd43f44b6c86af4af7836e22f" + }, { "name": "money_with_wings", "unicode": "1F4B8", @@ -5144,11 +7094,21 @@ "unicode": "1F5F1", "digest": "4af3e4e53eaa328b0d20542ab31705a74bf9fd368cd0673b706838ce1681d3c9" }, + { + "name": "lightning_mood_bubble", + "unicode": "1F5F1", + "digest": "4af3e4e53eaa328b0d20542ab31705a74bf9fd368cd0673b706838ce1681d3c9" + }, { "name": "mood_lightning", "unicode": "1F5F2", "digest": "6784635e81ec722fd50a1c2a23b0f9679e4bf1b5ae2b5a01eeb995bc1f7a426f" }, + { + "name": "lightning_mood", + "unicode": "1F5F2", + "digest": "6784635e81ec722fd50a1c2a23b0f9679e4bf1b5ae2b5a01eeb995bc1f7a426f" + }, { "name": "mortar_board", "unicode": "1F393", @@ -5169,6 +7129,11 @@ "unicode": "1F3CD", "digest": "8429fb6dfeb873abdffcc179c32d4f23e91c9e6b27b06cd204fd2e83cc11189e" }, + { + "name": "racing_motorcycle", + "unicode": "1F3CD", + "digest": "8429fb6dfeb873abdffcc179c32d4f23e91c9e6b27b06cd204fd2e83cc11189e" + }, { "name": "motorway", "unicode": "1F6E3", @@ -5229,6 +7194,11 @@ "unicode": "1F3D4", "digest": "9939aade3d4d972ba3af16fcc6cc2454978f5426e4c92838734a44db065ce0ff" }, + { + "name": "snow_capped_mountain", + "unicode": "1F3D4", + "digest": "9939aade3d4d972ba3af16fcc6cc2454978f5426e4c92838734a44db065ce0ff" + }, { "name": "mouse", "unicode": "1F42D", @@ -5244,11 +7214,21 @@ "unicode": "1F5AF", "digest": "e0d2055ccba489d24e0c0b6d2f22793efe48a734b0fd50f5af88f721b40665c0" }, + { + "name": "one_button_mouse", + "unicode": "1F5AF", + "digest": "e0d2055ccba489d24e0c0b6d2f22793efe48a734b0fd50f5af88f721b40665c0" + }, { "name": "mouse_three_button", "unicode": "1F5B1", "digest": "6a5629fee01145211cc8f4e8f59c5f1e61affed38c650502213d76c7d8861b01" }, + { + "name": "three_button_mouse", + "unicode": "1F5B1", + "digest": "6a5629fee01145211cc8f4e8f59c5f1e61affed38c650502213d76c7d8861b01" + }, { "name": "movie_camera", "unicode": "1F3A5", @@ -5364,11 +7344,21 @@ "unicode": "1F913", "digest": "94efd551700aae8909b8dd7a78a54a33e070d24b2e0a10534353645084614e98" }, + { + "name": "nerd_face", + "unicode": "1F913", + "digest": "94efd551700aae8909b8dd7a78a54a33e070d24b2e0a10534353645084614e98" + }, { "name": "network", "unicode": "1F5A7", "digest": "1dbaa54deeb2328fd8a3f044e450c97ac3ff39627c598bb2f4312d677482ee06" }, + { + "name": "three_networked_computers", + "unicode": "1F5A7", + "digest": "1dbaa54deeb2328fd8a3f044e450c97ac3ff39627c598bb2f4312d677482ee06" + }, { "name": "neutral_face", "unicode": "1F610", @@ -5399,6 +7389,11 @@ "unicode": "1F5DE", "digest": "0ca6b5850091f23295c970815a8e64a52e3c3dae492029ecb1e0726c2693f9bf" }, + { + "name": "rolled_up_newspaper", + "unicode": "1F5DE", + "digest": "0ca6b5850091f23295c970815a8e64a52e3c3dae492029ecb1e0726c2693f9bf" + }, { "name": "ng", "unicode": "1F196", @@ -5524,11 +7519,21 @@ "unicode": "1F5C9", "digest": "073660fdaa02ecf98d04f61f8d65d6cc447ccae3825fccaff19a2c99ebba52af" }, + { + "name": "note_page", + "unicode": "1F5C9", + "digest": "073660fdaa02ecf98d04f61f8d65d6cc447ccae3825fccaff19a2c99ebba52af" + }, { "name": "note_empty", "unicode": "1F5C6", "digest": "06b56eeaca6349bbcf1020bea98f937450a7e086db65cd5d7497748e0fb607be" }, + { + "name": "empty_note_page", + "unicode": "1F5C6", + "digest": "06b56eeaca6349bbcf1020bea98f937450a7e086db65cd5d7497748e0fb607be" + }, { "name": "notebook", "unicode": "1F4D3", @@ -5544,16 +7549,31 @@ "unicode": "1F5CA", "digest": "85069e2d13540886457368a57295072aec44c7137d9223bfcf908ce1f0e5124e" }, + { + "name": "note_pad", + "unicode": "1F5CA", + "digest": "85069e2d13540886457368a57295072aec44c7137d9223bfcf908ce1f0e5124e" + }, { "name": "notepad_empty", "unicode": "1F5C7", "digest": "8be5053e74c13d8220917c5aee1f4afdecb001612886438f283b0c2a0fecf6af" }, + { + "name": "empty_note_pad", + "unicode": "1F5C7", + "digest": "8be5053e74c13d8220917c5aee1f4afdecb001612886438f283b0c2a0fecf6af" + }, { "name": "notepad_spiral", "unicode": "1F5D2", "digest": "c181b6c1cc6063ec1848e46cbbf1d8b890c53b59cdc5218311ce06889570e727" }, + { + "name": "spiral_note_pad", + "unicode": "1F5D2", + "digest": "c181b6c1cc6063ec1848e46cbbf1d8b890c53b59cdc5218311ce06889570e727" + }, { "name": "notes", "unicode": "1F3B6", @@ -5599,6 +7619,11 @@ "unicode": "1F6E2", "digest": "f8b7626cb09e229203105b9c8c7f3fbb38c0650021092fc50115ad517248644a" }, + { + "name": "oil_drum", + "unicode": "1F6E2", + "digest": "f8b7626cb09e229203105b9c8c7f3fbb38c0650021092fc50115ad517248644a" + }, { "name": "ok", "unicode": "1F197", @@ -5699,31 +7724,61 @@ "unicode": "1F475", "digest": "3ed599443eed25399aac999fc234c9e97f8fb6ec567e37a553c26e01021b097c" }, + { + "name": "grandma", + "unicode": "1F475", + "digest": "3ed599443eed25399aac999fc234c9e97f8fb6ec567e37a553c26e01021b097c" + }, { "name": "older_woman_tone1", "unicode": "1F475-1F3FB", "digest": "7421c5dba67cfd1eeabb2fa8faf4aa0d615d23f191cf7d7c0ad9c1fa884edfda" }, + { + "name": "grandma_tone1", + "unicode": "1F475-1F3FB", + "digest": "7421c5dba67cfd1eeabb2fa8faf4aa0d615d23f191cf7d7c0ad9c1fa884edfda" + }, { "name": "older_woman_tone2", "unicode": "1F475-1F3FC", "digest": "65edeef25648ac7f8be535df06af1286441691fa15176e99a6e83fc779aa2cde" }, + { + "name": "grandma_tone2", + "unicode": "1F475-1F3FC", + "digest": "65edeef25648ac7f8be535df06af1286441691fa15176e99a6e83fc779aa2cde" + }, { "name": "older_woman_tone3", "unicode": "1F475-1F3FD", "digest": "5d27bbcc5796227a9caec1c7612d3f691055655b96f7303e420839463d76c269" }, + { + "name": "grandma_tone3", + "unicode": "1F475-1F3FD", + "digest": "5d27bbcc5796227a9caec1c7612d3f691055655b96f7303e420839463d76c269" + }, { "name": "older_woman_tone4", "unicode": "1F475-1F3FE", "digest": "75b858e910175fc0233503d672120fd43ac035ba3fd2052fbb44df39f6e3695c" }, + { + "name": "grandma_tone4", + "unicode": "1F475-1F3FE", + "digest": "75b858e910175fc0233503d672120fd43ac035ba3fd2052fbb44df39f6e3695c" + }, { "name": "older_woman_tone5", "unicode": "1F475-1F3FF", "digest": "9da1cf10a605c470877d7f4a840f99344b1ec2e7b1ec7db61e930cde77025e3b" }, + { + "name": "grandma_tone5", + "unicode": "1F475-1F3FF", + "digest": "9da1cf10a605c470877d7f4a840f99344b1ec2e7b1ec7db61e930cde77025e3b" + }, { "name": "om_symbol", "unicode": "1F549", @@ -5809,6 +7864,11 @@ "unicode": "1F5B8", "digest": "df8c10028d29d65f144a6b789d1c3294e7b3293554c4c30d28d72dc7ba8d9a5d" }, + { + "name": "optical_disc_icon", + "unicode": "1F5B8", + "digest": "df8c10028d29d65f144a6b789d1c3294e7b3293554c4c30d28d72dc7ba8d9a5d" + }, { "name": "orange_book", "unicode": "1F4D9", @@ -5864,6 +7924,11 @@ "unicode": "1F58C", "digest": "73eb33184f5f495d6c2699fafc1a8680069f82a70fbe519290c3a2ce30d1aee9" }, + { + "name": "lower_left_paintbrush", + "unicode": "1F58C", + "digest": "73eb33184f5f495d6c2699fafc1a8680069f82a70fbe519290c3a2ce30d1aee9" + }, { "name": "palm_tree", "unicode": "1F334", @@ -5884,11 +7949,21 @@ "unicode": "1F587", "digest": "7071e031f4a100c3cb3573fbfa375360043f0276289a0818f2ffaf71b3580040" }, + { + "name": "linked_paperclips", + "unicode": "1F587", + "digest": "7071e031f4a100c3cb3573fbfa375360043f0276289a0818f2ffaf71b3580040" + }, { "name": "park", "unicode": "1F3DE", "digest": "d257f0f1b1a0134573f80ba1a5f522a91c320ee7f93a1cb64877c077e7e19b50" }, + { + "name": "national_park", + "unicode": "1F3DE", + "digest": "d257f0f1b1a0134573f80ba1a5f522a91c320ee7f93a1cb64877c077e7e19b50" + }, { "name": "parking", "unicode": "1F17F", @@ -5914,11 +7989,21 @@ "unicode": "23F8", "digest": "edd605ffaa39a7905ed0958b7cc69f00f5b271e579198d2df1746ad1b3648272" }, + { + "name": "double_vertical_bar", + "unicode": "23F8", + "digest": "edd605ffaa39a7905ed0958b7cc69f00f5b271e579198d2df1746ad1b3648272" + }, { "name": "peace", "unicode": "262E", "digest": "e0ee8a5c9fb18d5db6841b21527ed8fd955abdff9ffdb7b2684dca22107015fc" }, + { + "name": "peace_symbol", + "unicode": "262E", + "digest": "e0ee8a5c9fb18d5db6841b21527ed8fd955abdff9ffdb7b2684dca22107015fc" + }, { "name": "peach", "unicode": "1F351", @@ -5934,16 +8019,31 @@ "unicode": "1F58A", "digest": "6becdc6f622c774bb09b7e7592bba2123ecccc9de32a35f0b18b50d7d54109cb" }, + { + "name": "lower_left_ballpoint_pen", + "unicode": "1F58A", + "digest": "6becdc6f622c774bb09b7e7592bba2123ecccc9de32a35f0b18b50d7d54109cb" + }, { "name": "pen_fountain", "unicode": "1F58B", "digest": "8c78cf0c2bd1d5e309d2d3356ff207e3fc76ca18dd6b90762cb62f6afbc95c6a" }, + { + "name": "lower_left_fountain_pen", + "unicode": "1F58B", + "digest": "8c78cf0c2bd1d5e309d2d3356ff207e3fc76ca18dd6b90762cb62f6afbc95c6a" + }, { "name": "pencil", "unicode": "1F4DD", "digest": "62b7ee5d9352114d09ee6f2c9a4c5e8b79f775a6c509e82ddfcdd61e13716249" }, + { + "name": "memo", + "unicode": "1F4DD", + "digest": "62b7ee5d9352114d09ee6f2c9a4c5e8b79f775a6c509e82ddfcdd61e13716249" + }, { "name": "pencil2", "unicode": "270F", @@ -5954,6 +8054,11 @@ "unicode": "1F589", "digest": "52c1ba1228917eb491ac1745a495e0fdafba6b985a81caba250f71d1f94c725c" }, + { + "name": "lower_left_pencil", + "unicode": "1F589", + "digest": "52c1ba1228917eb491ac1745a495e0fdafba6b985a81caba250f71d1f94c725c" + }, { "name": "penguin", "unicode": "1F427", @@ -5964,11 +8069,21 @@ "unicode": "1F3F2", "digest": "cd3c33bfc3c7fbe84b98d2d481d56a7bf5488ff94afadd8b5a0e454768b80269" }, + { + "name": "black_pennant", + "unicode": "1F3F2", + "digest": "cd3c33bfc3c7fbe84b98d2d481d56a7bf5488ff94afadd8b5a0e454768b80269" + }, { "name": "pennant_white", "unicode": "1F3F1", "digest": "818b1be73540f2cfeb1c514e1ee75d18715af317f0db817d9ae081b9ea33d4b0" }, + { + "name": "white_pennant", + "unicode": "1F3F1", + "digest": "818b1be73540f2cfeb1c514e1ee75d18715af317f0db817d9ae081b9ea33d4b0" + }, { "name": "pensive", "unicode": "1F614", @@ -6109,11 +8224,21 @@ "unicode": "1F3D3", "digest": "dd2a84716c93410a285ff759bfbc2dc31a10f90b203c7a657b908e5949e89a39" }, + { + "name": "table_tennis", + "unicode": "1F3D3", + "digest": "dd2a84716c93410a285ff759bfbc2dc31a10f90b203c7a657b908e5949e89a39" + }, { "name": "piracy", "unicode": "1F572", "digest": "f42955ba75c598392e5e258be49968d858c876e0d6e7aa9dc795f7e8cff42be9" }, + { + "name": "no_piracy", + "unicode": "1F572", + "digest": "f42955ba75c598392e5e258be49968d858c876e0d6e7aa9dc795f7e8cff42be9" + }, { "name": "pisces", "unicode": "2653", @@ -6129,6 +8254,11 @@ "unicode": "1F6D0", "digest": "4fabc307b7e35f94288f6d53985485662a4814b11a9a382f0a3873d41b1290d3" }, + { + "name": "worship_symbol", + "unicode": "1F6D0", + "digest": "4fabc307b7e35f94288f6d53985485662a4814b11a9a382f0a3873d41b1290d3" + }, { "name": "play_pause", "unicode": "23EF", @@ -6299,6 +8429,21 @@ "unicode": "1F4A9", "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" }, + { + "name": "shit", + "unicode": "1F4A9", + "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" + }, + { + "name": "hankey", + "unicode": "1F4A9", + "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" + }, + { + "name": "poo", + "unicode": "1F4A9", + "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" + }, { "name": "popcorn", "unicode": "1F37F", @@ -6419,11 +8564,21 @@ "unicode": "1F6C7", "digest": "bc6cdea2269a0ec39576d98dc4cda2bd9efa4dc330dde870148c6a85ad9cc63f" }, + { + "name": "prohibited_sign", + "unicode": "1F6C7", + "digest": "bc6cdea2269a0ec39576d98dc4cda2bd9efa4dc330dde870148c6a85ad9cc63f" + }, { "name": "projector", "unicode": "1F4FD", "digest": "fc361282f367926254c08150b02cb8fda7fa8d2c9c939d9360c78bf19a4f982e" }, + { + "name": "film_projector", + "unicode": "1F4FD", + "digest": "fc361282f367926254c08150b02cb8fda7fa8d2c9c939d9360c78bf19a4f982e" + }, { "name": "punch", "unicode": "1F44A", @@ -6499,6 +8654,11 @@ "unicode": "1F3CE", "digest": "2e9828e3884c79ad7e9e1173d3470790f3f56cfa08ef4e38deff45db0728c66c" }, + { + "name": "racing_car", + "unicode": "1F3CE", + "digest": "2e9828e3884c79ad7e9e1173d3470790f3f56cfa08ef4e38deff45db0728c66c" + }, { "name": "racehorse", "unicode": "1F40E", @@ -6519,6 +8679,11 @@ "unicode": "2622", "digest": "5ad8e8594617c0153672a76421deb836e05c6098020c33af3f975f8fcfe216e4" }, + { + "name": "radioactive_sign", + "unicode": "2622", + "digest": "5ad8e8594617c0153672a76421deb836e05c6098020c33af3f975f8fcfe216e4" + }, { "name": "rage", "unicode": "1F621", @@ -6534,6 +8699,11 @@ "unicode": "1F6E4", "digest": "63ee881cc775d5b2711082b6c96ab44d5204c5d390afd6d8ee97e52aeeaa5e5e" }, + { + "name": "railroad_track", + "unicode": "1F6E4", + "digest": "63ee881cc775d5b2711082b6c96ab44d5204c5d390afd6d8ee97e52aeeaa5e5e" + }, { "name": "rainbow", "unicode": "1F308", @@ -6744,11 +8914,21 @@ "unicode": "1F569", "digest": "5b92daa87bdf6ee15e798bec382a2ee885f4e6e77a68a3f626adcfe4c782b375" }, + { + "name": "right_speaker_with_one_sound_wave", + "unicode": "1F569", + "digest": "5b92daa87bdf6ee15e798bec382a2ee885f4e6e77a68a3f626adcfe4c782b375" + }, { "name": "right_speaker_three", "unicode": "1F56A", "digest": "4d00b720a65bd0f4c3682b290b1976ec2388d6ae61225398f4e70556ae9e5f80" }, + { + "name": "right_speaker_with_three_sound_waves", + "unicode": "1F56A", + "digest": "4d00b720a65bd0f4c3682b290b1976ec2388d6ae61225398f4e70556ae9e5f80" + }, { "name": "ring", "unicode": "1F48D", @@ -6764,6 +8944,11 @@ "unicode": "1F916", "digest": "cc0e363774b86e21a5b2cea7f7af85bca9e92c124ebcd39c6067c125048baa60" }, + { + "name": "robot_face", + "unicode": "1F916", + "digest": "cc0e363774b86e21a5b2cea7f7af85bca9e92c124ebcd39c6067c125048baa60" + }, { "name": "rocket", "unicode": "1F680", @@ -6779,6 +8964,11 @@ "unicode": "1F644", "digest": "f596f203030b6c9bd743848512aa3fc7919447020d35ae5c2bf13ccb16fa2dbe" }, + { + "name": "face_with_rolling_eyes", + "unicode": "1F644", + "digest": "f596f203030b6c9bd743848512aa3fc7919447020d35ae5c2bf13ccb16fa2dbe" + }, { "name": "rooster", "unicode": "1F413", @@ -7099,11 +9289,21 @@ "unicode": "1F480", "digest": "dfd169764b192ac7c6e5101277dd9f1e010e86bdd32ad37e00ed4499fc0a5dd6" }, + { + "name": "skeleton", + "unicode": "1F480", + "digest": "dfd169764b192ac7c6e5101277dd9f1e010e86bdd32ad37e00ed4499fc0a5dd6" + }, { "name": "skull_crossbones", "unicode": "2620", "digest": "e2acf0f36b6a6800c1829a1c6551b5d0eb6dcdef4b7f02070cf69570aeab608c" }, + { + "name": "skull_and_crossbones", + "unicode": "2620", + "digest": "e2acf0f36b6a6800c1829a1c6551b5d0eb6dcdef4b7f02070cf69570aeab608c" + }, { "name": "sleeping", "unicode": "1F634", @@ -7124,11 +9324,21 @@ "unicode": "1F641", "digest": "3ae82b38b58ffa50eddebd87153428d880ca181f4f4178a9ca3bd813ea15ccbc" }, + { + "name": "slightly_frowning_face", + "unicode": "1F641", + "digest": "3ae82b38b58ffa50eddebd87153428d880ca181f4f4178a9ca3bd813ea15ccbc" + }, { "name": "slight_smile", "unicode": "1F642", "digest": "5eee09f634a4e2031927d008a6530a258a00e611ead0c386dd5b7ebb5e75a306" }, + { + "name": "slightly_smiling_face", + "unicode": "1F642", + "digest": "5eee09f634a4e2031927d008a6530a258a00e611ead0c386dd5b7ebb5e75a306" + }, { "name": "slot_machine", "unicode": "1F3B0", @@ -7299,6 +9509,11 @@ "unicode": "1F5E3", "digest": "d92cfe1200887300b2f05f9576448a2f2a79d0accd51f323a65ce3db0aa5639b" }, + { + "name": "speaking_head_in_silhouette", + "unicode": "1F5E3", + "digest": "d92cfe1200887300b2f05f9576448a2f2a79d0accd51f323a65ce3db0aa5639b" + }, { "name": "speech_balloon", "unicode": "1F4AC", @@ -7309,21 +9524,41 @@ "unicode": "1F5E8", "digest": "478b0b07460a9f54b7d0050f886da59fde5e428daa11e899fc31477fda1707ed" }, + { + "name": "left_speech_bubble", + "unicode": "1F5E8", + "digest": "478b0b07460a9f54b7d0050f886da59fde5e428daa11e899fc31477fda1707ed" + }, { "name": "speech_right", "unicode": "1F5E9", "digest": "8439b13779163c15e678a78b08ebeeb7d131632df21d2a7868de7fed38ca9d8a" }, + { + "name": "right_speech_bubble", + "unicode": "1F5E9", + "digest": "8439b13779163c15e678a78b08ebeeb7d131632df21d2a7868de7fed38ca9d8a" + }, { "name": "speech_three", "unicode": "1F5EB", "digest": "55a934f3659b6e75fdce0d0c4e2ea56dd34a43892c85a6666bd1882a0bfb92a9" }, + { + "name": "three_speech_bubbles", + "unicode": "1F5EB", + "digest": "55a934f3659b6e75fdce0d0c4e2ea56dd34a43892c85a6666bd1882a0bfb92a9" + }, { "name": "speech_two", "unicode": "1F5EA", "digest": "0563ef0591da243673cf877462acc5d8e1d980a56e81668ac627de74d0c33983" }, + { + "name": "two_speech_bubbles", + "unicode": "1F5EA", + "digest": "0563ef0591da243673cf877462acc5d8e1d980a56e81668ac627de74d0c33983" + }, { "name": "speedboat", "unicode": "1F6A4", @@ -7344,31 +9579,61 @@ "unicode": "1F575", "digest": "eaa570a36d83119d0a596228e74affe84d7355714ff6901d88a89410d26dec2a" }, + { + "name": "sleuth_or_spy", + "unicode": "1F575", + "digest": "eaa570a36d83119d0a596228e74affe84d7355714ff6901d88a89410d26dec2a" + }, { "name": "spy_tone1", "unicode": "1F575-1F3FB", "digest": "abdc066d4cad6a17047faf7806c45feb43ae1e2056cf500536f08f4173dbfa94" }, + { + "name": "sleuth_or_spy_tone1", + "unicode": "1F575-1F3FB", + "digest": "abdc066d4cad6a17047faf7806c45feb43ae1e2056cf500536f08f4173dbfa94" + }, { "name": "spy_tone2", "unicode": "1F575-1F3FC", "digest": "72a3313ef12364105e764cc3deabd47eb6bd086f261c435682ae1cd29dc8230b" }, + { + "name": "sleuth_or_spy_tone2", + "unicode": "1F575-1F3FC", + "digest": "72a3313ef12364105e764cc3deabd47eb6bd086f261c435682ae1cd29dc8230b" + }, { "name": "spy_tone3", "unicode": "1F575-1F3FD", "digest": "2a1108d3d2e778f88aa5b3ae36705c877b84d0bf6b421409582ba748aeb2aee7" }, + { + "name": "sleuth_or_spy_tone3", + "unicode": "1F575-1F3FD", + "digest": "2a1108d3d2e778f88aa5b3ae36705c877b84d0bf6b421409582ba748aeb2aee7" + }, { "name": "spy_tone4", "unicode": "1F575-1F3FE", "digest": "1d4fe62912384bc0d687bcf4565752caf0ed6146c903a156d1c6ba6ea239b154" }, + { + "name": "sleuth_or_spy_tone4", + "unicode": "1F575-1F3FE", + "digest": "1d4fe62912384bc0d687bcf4565752caf0ed6146c903a156d1c6ba6ea239b154" + }, { "name": "spy_tone5", "unicode": "1F575-1F3FF", "digest": "69c1baac73783edb9e2d0c951f922dc7dddac34d0a9c818fee8d1021bc17db0d" }, + { + "name": "sleuth_or_spy_tone5", + "unicode": "1F575-1F3FF", + "digest": "69c1baac73783edb9e2d0c951f922dc7dddac34d0a9c818fee8d1021bc17db0d" + }, { "name": "stadium", "unicode": "1F3DF", @@ -7419,6 +9684,11 @@ "unicode": "1F4FE", "digest": "1ce1f9a83867514b8351ad4fd80c46bba04ad67dfb9874e63d7296e1a21161a5" }, + { + "name": "portable_stereo", + "unicode": "1F4FE", + "digest": "1ce1f9a83867514b8351ad4fd80c46bba04ad67dfb9874e63d7296e1a21161a5" + }, { "name": "stew", "unicode": "1F372", @@ -7644,6 +9914,11 @@ "unicode": "1F57F", "digest": "c3a42a653a91d90c6b668f678419d5438f2e546050914b841623e57107e805db" }, + { + "name": "black_touchtone_telephone", + "unicode": "1F57F", + "digest": "c3a42a653a91d90c6b668f678419d5438f2e546050914b841623e57107e805db" + }, { "name": "telephone_receiver", "unicode": "1F4DE", @@ -7654,6 +9929,11 @@ "unicode": "1F57E", "digest": "62a7e0e50c53e9f85eba51a92882e6064be05997910d3f7700e1e957dbaf0581" }, + { + "name": "white_touchtone_telephone", + "unicode": "1F57E", + "digest": "62a7e0e50c53e9f85eba51a92882e6064be05997910d3f7700e1e957dbaf0581" + }, { "name": "telescope", "unicode": "1F52D", @@ -7684,11 +9964,21 @@ "unicode": "1F912", "digest": "f19c489d89dd2d39770a6c8725a20f3e98f9e5216774af60c0665fd6a03a7687" }, + { + "name": "face_with_thermometer", + "unicode": "1F912", + "digest": "f19c489d89dd2d39770a6c8725a20f3e98f9e5216774af60c0665fd6a03a7687" + }, { "name": "thinking", "unicode": "1F914", "digest": "f64a9a18dca4c502b46f933838753a818b604a9d0268aa32eda26cbd31abc58c" }, + { + "name": "thinking_face", + "unicode": "1F914", + "digest": "f64a9a18dca4c502b46f933838753a818b604a9d0268aa32eda26cbd31abc58c" + }, { "name": "thought_balloon", "unicode": "1F4AD", @@ -7699,11 +9989,21 @@ "unicode": "1F5EC", "digest": "4fd591bf4318df73d1b17f434a449d8e95f49cca53a3d8f4d1ca983f3809ef46" }, + { + "name": "left_thought_bubble", + "unicode": "1F5EC", + "digest": "4fd591bf4318df73d1b17f434a449d8e95f49cca53a3d8f4d1ca983f3809ef46" + }, { "name": "thought_right", "unicode": "1F5ED", "digest": "0e8c0ce26e2d0e30894f5394b0736456e8268f775e0e7eda4c7dc3c2ff9231ae" }, + { + "name": "right_thought_bubble", + "unicode": "1F5ED", + "digest": "0e8c0ce26e2d0e30894f5394b0736456e8268f775e0e7eda4c7dc3c2ff9231ae" + }, { "name": "three", "unicode": "0033-20E3", @@ -7714,76 +10014,151 @@ "unicode": "1F593", "digest": "a8b561e389bc4e4b07fba70994f6445e5ddc6afe68922fcb6e9e7282d19ad958" }, + { + "name": "reversed_thumbs_down_sign", + "unicode": "1F593", + "digest": "a8b561e389bc4e4b07fba70994f6445e5ddc6afe68922fcb6e9e7282d19ad958" + }, { "name": "thumbs_up_reverse", "unicode": "1F592", "digest": "b6e52715c5ce590bfd08f6e05058ec3765ea2da341b11f9825d100608b173837" }, + { + "name": "reversed_thumbs_up_sign", + "unicode": "1F592", + "digest": "b6e52715c5ce590bfd08f6e05058ec3765ea2da341b11f9825d100608b173837" + }, { "name": "thumbsdown", "unicode": "1F44E", "digest": "a98f742c9773e0d95c0de5e1c10d1ab373fa761378a205f27d095e85debe69a3" }, + { + "name": "-1", + "unicode": "1F44E", + "digest": "a98f742c9773e0d95c0de5e1c10d1ab373fa761378a205f27d095e85debe69a3" + }, { "name": "thumbsdown_tone1", "unicode": "1F44E-1F3FB", "digest": "5d0a7c63d52eafe6267c552168c5557a66622009d565c3cf7b5378c1f6e84bce" }, + { + "name": "-1_tone1", + "unicode": "1F44E-1F3FB", + "digest": "5d0a7c63d52eafe6267c552168c5557a66622009d565c3cf7b5378c1f6e84bce" + }, { "name": "thumbsdown_tone2", "unicode": "1F44E-1F3FC", "digest": "ca5c15dc516660b2989a1c717bf3745fdfb6964c7acf3b938285ff6c7caf2ca2" }, + { + "name": "-1_tone2", + "unicode": "1F44E-1F3FC", + "digest": "ca5c15dc516660b2989a1c717bf3745fdfb6964c7acf3b938285ff6c7caf2ca2" + }, { "name": "thumbsdown_tone3", "unicode": "1F44E-1F3FD", "digest": "05740e3568795270674dac9134198bf75b1b778c11daa71649c88c231859ec16" }, + { + "name": "-1_tone3", + "unicode": "1F44E-1F3FD", + "digest": "05740e3568795270674dac9134198bf75b1b778c11daa71649c88c231859ec16" + }, { "name": "thumbsdown_tone4", "unicode": "1F44E-1F3FE", "digest": "5ee93bcc2f515806462a7b303064beade2b22a3f43a8162e39fd65d15d772e27" }, + { + "name": "-1_tone4", + "unicode": "1F44E-1F3FE", + "digest": "5ee93bcc2f515806462a7b303064beade2b22a3f43a8162e39fd65d15d772e27" + }, { "name": "thumbsdown_tone5", "unicode": "1F44E-1F3FF", "digest": "5c9ef8d53cf6f755668ab6dabfbfcdfd4b95fd59db3b3dd60290efefe9c33994" }, + { + "name": "-1_tone5", + "unicode": "1F44E-1F3FF", + "digest": "5c9ef8d53cf6f755668ab6dabfbfcdfd4b95fd59db3b3dd60290efefe9c33994" + }, { "name": "thumbsup", "unicode": "1F44D", "digest": "28b31df963773ba42a1a089f43cd89d0ce1ab0981e5410f41242e9a125fc1aee" }, + { + "name": "+1", + "unicode": "1F44D", + "digest": "28b31df963773ba42a1a089f43cd89d0ce1ab0981e5410f41242e9a125fc1aee" + }, { "name": "thumbsup_tone1", "unicode": "1F44D-1F3FB", "digest": "f6365942738d2128b6959d6672b3d295757dc8240703cb84a2b014ad78d67de3" }, + { + "name": "+1_tone1", + "unicode": "1F44D-1F3FB", + "digest": "f6365942738d2128b6959d6672b3d295757dc8240703cb84a2b014ad78d67de3" + }, { "name": "thumbsup_tone2", "unicode": "1F44D-1F3FC", "digest": "771d30146e4dc947a69057b05d32c765c8457ab02b5342889c5489acf27ef356" }, + { + "name": "+1_tone2", + "unicode": "1F44D-1F3FC", + "digest": "771d30146e4dc947a69057b05d32c765c8457ab02b5342889c5489acf27ef356" + }, { "name": "thumbsup_tone3", "unicode": "1F44D-1F3FD", "digest": "0bb7bbfb654c6139260e1786e7ffa5a33f31e19410c1d4d15737fdf5dd4c721d" }, + { + "name": "+1_tone3", + "unicode": "1F44D-1F3FD", + "digest": "0bb7bbfb654c6139260e1786e7ffa5a33f31e19410c1d4d15737fdf5dd4c721d" + }, { "name": "thumbsup_tone4", "unicode": "1F44D-1F3FE", "digest": "df0927c5342f0075fbf4ea83b724e6f70c0466c54769c9ce4a5c2deb602b28aa" }, + { + "name": "+1_tone4", + "unicode": "1F44D-1F3FE", + "digest": "df0927c5342f0075fbf4ea83b724e6f70c0466c54769c9ce4a5c2deb602b28aa" + }, { "name": "thumbsup_tone5", "unicode": "1F44D-1F3FF", "digest": "0683ae08c50aaf186c6406680a60617679c7b4bccd0817f24b15911dbb06866f" }, + { + "name": "+1_tone5", + "unicode": "1F44D-1F3FF", + "digest": "0683ae08c50aaf186c6406680a60617679c7b4bccd0817f24b15911dbb06866f" + }, { "name": "thunder_cloud_rain", "unicode": "26C8", "digest": "dd836f06b41a10d6ed9bcbdae291d2886847ff66dc3ede2427382e469f60674c" }, + { + "name": "thunder_cloud_and_rain", + "unicode": "26C8", + "digest": "dd836f06b41a10d6ed9bcbdae291d2886847ff66dc3ede2427382e469f60674c" + }, { "name": "ticket", "unicode": "1F3AB", @@ -7794,6 +10169,11 @@ "unicode": "1F39F", "digest": "ccafcc9583a84e847ff1eaa3d53187c5ab150a7d27c6a19363e59b9bc046b567" }, + { + "name": "admission_tickets", + "unicode": "1F39F", + "digest": "ccafcc9583a84e847ff1eaa3d53187c5ab150a7d27c6a19363e59b9bc046b567" + }, { "name": "tiger", "unicode": "1F42F", @@ -7809,6 +10189,11 @@ "unicode": "23F2", "digest": "c48199312ed42ff53a33bb2791db19e2e2521223cd49d8f758ea95b9b379c5ff" }, + { + "name": "timer_clock", + "unicode": "23F2", + "digest": "c48199312ed42ff53a33bb2791db19e2e2521223cd49d8f758ea95b9b379c5ff" + }, { "name": "tired_face", "unicode": "1F62B", @@ -7869,6 +10254,11 @@ "unicode": "1F6E0", "digest": "9b0a36dfdb475621d326359662b22cbdb80563c4f476aa5e7d7c00cdba605bd9" }, + { + "name": "hammer_and_wrench", + "unicode": "1F6E0", + "digest": "9b0a36dfdb475621d326359662b22cbdb80563c4f476aa5e7d7c00cdba605bd9" + }, { "name": "top", "unicode": "1F51D", @@ -7884,11 +10274,21 @@ "unicode": "23ED", "digest": "d5415ed140933f345fea8023a3d8fca30dcfcf7d19d9dc9771fa2cae9df62a3b" }, + { + "name": "next_track", + "unicode": "23ED", + "digest": "d5415ed140933f345fea8023a3d8fca30dcfcf7d19d9dc9771fa2cae9df62a3b" + }, { "name": "track_previous", "unicode": "23EE", "digest": "97ff4a59a236e5cf506fa3577b20715b3b0197e0f343a50615b36185d5b835f1" }, + { + "name": "previous_track", + "unicode": "23EE", + "digest": "97ff4a59a236e5cf506fa3577b20715b3b0197e0f343a50615b36185d5b835f1" + }, { "name": "trackball", "unicode": "1F5B2", @@ -7919,6 +10319,11 @@ "unicode": "1F6F2", "digest": "621bb967cd93fa9f8fd4b155965cc7572d3f91f88d94938ba10c8626718b623c" }, + { + "name": "diesel_locomotive", + "unicode": "1F6F2", + "digest": "621bb967cd93fa9f8fd4b155965cc7572d3f91f88d94938ba10c8626718b623c" + }, { "name": "tram", "unicode": "1F68A", @@ -7929,6 +10334,11 @@ "unicode": "1F6C6", "digest": "e24bb39ecfaaa746b03dc8418697d09ef327d5b077db39014f39d5fb87e23bd5" }, + { + "name": "triangle_with_rounded_corners", + "unicode": "1F6C6", + "digest": "e24bb39ecfaaa746b03dc8418697d09ef327d5b077db39014f39d5fb87e23bd5" + }, { "name": "triangular_flag_on_post", "unicode": "1F6A9", @@ -7994,6 +10404,11 @@ "unicode": "1F58F", "digest": "8a6c5b7d4c737866e7e32c6d9f7f447a48a0ac57a8909d43f87367d4a9b59246" }, + { + "name": "turned_ok_hand_sign", + "unicode": "1F58F", + "digest": "8a6c5b7d4c737866e7e32c6d9f7f447a48a0ac57a8909d43f87367d4a9b59246" + }, { "name": "turtle", "unicode": "1F422", @@ -8109,6 +10524,11 @@ "unicode": "1F984", "digest": "1b1e9c209dabe619db76fd346c3fb51b28ace0e4102697fe0973fe2d46aa9f08" }, + { + "name": "unicorn_face", + "unicode": "1F984", + "digest": "1b1e9c209dabe619db76fd346c3fb51b28ace0e4102697fe0973fe2d46aa9f08" + }, { "name": "unlock", "unicode": "1F513", @@ -8124,11 +10544,21 @@ "unicode": "1F643", "digest": "763fe2baf07a9b04f96958adf38a43c7dd2bc70d57398f49604307bd835cbb53" }, + { + "name": "upside_down_face", + "unicode": "1F643", + "digest": "763fe2baf07a9b04f96958adf38a43c7dd2bc70d57398f49604307bd835cbb53" + }, { "name": "urn", "unicode": "26B1", "digest": "dbfd5b90709d1b812d2fff71a5cfa10f84a4579866c2d7cd0e80759a22b2ba0e" }, + { + "name": "funeral_urn", + "unicode": "26B1", + "digest": "dbfd5b90709d1b812d2fff71a5cfa10f84a4579866c2d7cd0e80759a22b2ba0e" + }, { "name": "v", "unicode": "270C", @@ -8214,31 +10644,61 @@ "unicode": "1F596", "digest": "ca800fce797e652c5f47bf44992e8fbe19554688a36423fdf7c29ca6defae1e0" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers", + "unicode": "1F596", + "digest": "ca800fce797e652c5f47bf44992e8fbe19554688a36423fdf7c29ca6defae1e0" + }, { "name": "vulcan_tone1", "unicode": "1F596-1F3FB", "digest": "84bafdaca43426b053f5caa4e868ca109d99113a28ea9799db09d3c5d5f645c8" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone1", + "unicode": "1F596-1F3FB", + "digest": "84bafdaca43426b053f5caa4e868ca109d99113a28ea9799db09d3c5d5f645c8" + }, { "name": "vulcan_tone2", "unicode": "1F596-1F3FC", "digest": "e7cedf63ead957ee5c287e4cb0828ba70673e17b604f92b529875c32d094e7e3" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone2", + "unicode": "1F596-1F3FC", + "digest": "e7cedf63ead957ee5c287e4cb0828ba70673e17b604f92b529875c32d094e7e3" + }, { "name": "vulcan_tone3", "unicode": "1F596-1F3FD", "digest": "e124fef20f289921553274cf834f6dcc1a012889d30d9874dc5ad01afb8235b8" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone3", + "unicode": "1F596-1F3FD", + "digest": "e124fef20f289921553274cf834f6dcc1a012889d30d9874dc5ad01afb8235b8" + }, { "name": "vulcan_tone4", "unicode": "1F596-1F3FE", "digest": "ea2115f549e4680467521bbf362b229f4a8f0fdadbfaf231378d801f9b369f08" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone4", + "unicode": "1F596-1F3FE", + "digest": "ea2115f549e4680467521bbf362b229f4a8f0fdadbfaf231378d801f9b369f08" + }, { "name": "vulcan_tone5", "unicode": "1F596-1F3FF", "digest": "1b322e1252491f35ae02f0b279b6529dad867f2a6b3c2c3e77f981bed07e447d" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone5", + "unicode": "1F596-1F3FF", + "digest": "1b322e1252491f35ae02f0b279b6529dad867f2a6b3c2c3e77f981bed07e447d" + }, { "name": "walking", "unicode": "1F6B6", @@ -8429,16 +10889,31 @@ "unicode": "1F325", "digest": "d8ce416e6bdb0e59e06e2fceac3177dbe59fefc248fd8c6d76b80d1418141070" }, + { + "name": "white_sun_behind_cloud", + "unicode": "1F325", + "digest": "d8ce416e6bdb0e59e06e2fceac3177dbe59fefc248fd8c6d76b80d1418141070" + }, { "name": "white_sun_rain_cloud", "unicode": "1F326", "digest": "d2b132518261864ac4a95707eaeea335dd8351ed2b8ef4e2272ced456e309bf1" }, + { + "name": "white_sun_behind_cloud_with_rain", + "unicode": "1F326", + "digest": "d2b132518261864ac4a95707eaeea335dd8351ed2b8ef4e2272ced456e309bf1" + }, { "name": "white_sun_small_cloud", "unicode": "1F324", "digest": "b86a72f1cdb4d24fd3ab180aae9db012ca51fc01f3786aab596c2e330066b185" }, + { + "name": "white_sun_with_small_cloud", + "unicode": "1F324", + "digest": "b86a72f1cdb4d24fd3ab180aae9db012ca51fc01f3786aab596c2e330066b185" + }, { "name": "wind_blowing_face", "unicode": "1F32C", @@ -8524,6 +10999,11 @@ "unicode": "1F58E", "digest": "c4fc18ece6778339ebe14438aaf570e22385c3010c2d341824fa72ac6068cfeb" }, + { + "name": "left_writing_hand", + "unicode": "1F58E", + "digest": "c4fc18ece6778339ebe14438aaf570e22385c3010c2d341824fa72ac6068cfeb" + }, { "name": "writing_hand_tone1", "unicode": "270D-1F3FB", @@ -8589,6 +11069,11 @@ "unicode": "1F910", "digest": "8396249161b6d865861b56aabd17cae2c821b0d814f4249bf8cab0bb21fa8ee9" }, + { + "name": "zipper_mouth_face", + "unicode": "1F910", + "digest": "8396249161b6d865861b56aabd17cae2c821b0d814f4249bf8cab0bb21fa8ee9" + }, { "name": "zzz", "unicode": "1F4A4", diff --git a/fixtures/import_export/project.json b/fixtures/import_export/project.json new file mode 100644 index 0000000000000000000000000000000000000000..4a12c951dcbc4912238c01439cebaff1dd4d7b99 --- /dev/null +++ b/fixtures/import_export/project.json @@ -0,0 +1,434 @@ +{ + "name": "searchable_project", + "path": "gitlabhq", + "description": null, + "issues_enabled": true, + "wall_enabled": false, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": true, + "visibility_level": 20, + "archived": false, + "issues": [ + { + "id": 1, + "title": "Debitis vero omnis cum accusamus nihil rerum cupiditate.", + "assignee_id": 1, + "author_id": 2, + "project_id": 6, + "created_at": "2016-04-13T14:40:32.471Z", + "updated_at": "2016-04-13T14:40:38.956Z", + "position": 0, + "branch_name": null, + "description": null, + "milestone_id": null, + "state": "opened", + "iid": 1, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "notes": [ + { + "id": 1, + "note": ":+1: issue", + "noteable_type": "Issue", + "author_id": 21, + "created_at": "2016-04-13T14:40:38.944Z", + "updated_at": "2016-04-13T14:40:38.944Z", + "project_id": 8, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 1, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false + } + ] + } + ], + "labels": [ + { + "id": 1, + "title": "label1", + "color": "#990000", + "project_id": 6, + "created_at": "2016-04-13T14:40:34.704Z", + "updated_at": "2016-04-13T14:40:36.891Z", + "template": false, + "description": null + } + ], + "milestones": [ + { + "id": 1, + "title": "Milestone v1.2", + "project_id": 6, + "description": null, + "due_date": null, + "created_at": "2016-04-13T14:40:37.901Z", + "updated_at": "2016-04-13T14:40:37.901Z", + "state": "active", + "iid": 1 + } + ], + "snippets": [ + { + "id": 1, + "title": "Illo ipsa maxime magni aut.", + "content": "Excepturi delectus ut harum est molestiae dolor.", + "author_id": 10, + "project_id": 6, + "created_at": "2016-04-13T14:40:35.603Z", + "updated_at": "2016-04-13T14:40:36.903Z", + "file_name": "daphne.mraz", + "visibility_level": 0 + } + ], + "releases": [ + { + "id": 1, + "tag": "v1.1.0", + "description": "Awesome release", + "project_id": 6, + "created_at": "2016-04-13T14:40:36.223Z", + "updated_at": "2016-04-13T14:40:36.913Z" + } + ], + "events": [ + { + "id": 1, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 6, + "created_at": "2016-04-13T14:40:40.122Z", + "updated_at": "2016-04-13T14:40:40.122Z", + "action": 8, + "author_id": 1 + } + ], + "project_members": [ + { + "id": 1, + "user": { + "id": 1, + "email": "norval.gulgowski@schambergerboyle.co.uk", + "created_at": "2016-04-13T14:40:30.963Z", + "updated_at": "2016-04-13T14:40:30.963Z", + "name": "Jalon Cormier DVM", + "admin": false, + "projects_limit": 42, + "skype": "", + "linkedin": "", + "twitter": "", + "authentication_token": "tt-mPSZFvRBu8QzkW1Ss", + "theme_id": 2, + "bio": null, + "username": "vance.turner1", + "can_create_group": true, + "can_create_team": false, + "state": "active", + "color_scheme_id": 1, + "notification_level": 1, + "password_expires_at": null, + "created_by_id": null, + "last_credential_check_at": null, + "avatar": { + "url": null + }, + "hide_no_ssh_key": false, + "website_url": "", + "notification_email": "norval.gulgowski@schambergerboyle.co.uk", + "hide_no_password": false, + "password_automatically_set": false, + "location": null, + "encrypted_otp_secret": null, + "encrypted_otp_secret_iv": null, + "encrypted_otp_secret_salt": null, + "otp_required_for_login": false, + "otp_backup_codes": null, + "public_email": "", + "dashboard": "projects", + "project_view": "readme", + "consumed_timestep": null, + "layout": "fixed", + "hide_project_limit": false, + "otp_grace_period_started_at": null, + "ldap_email": false, + "external": false + } + } + ], + "merge_requests": [ + { + "id": 1, + "target_branch": "feature", + "source_branch": "master", + "source_project_id": 2, + "author_id": 5, + "assignee_id": null, + "title": "Dignissimos officia sit aut id dolor iure voluptatem expedita.", + "created_at": "2016-04-13T14:40:33.381Z", + "updated_at": "2016-04-13T14:40:39.850Z", + "milestone_id": null, + "state": "opened", + "merge_status": "can_be_merged", + "target_project_id": 6, + "iid": 1, + "description": null, + "position": 0, + "locked_at": null, + "updated_by_id": null, + "merge_error": null, + "merge_params": { + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "merge_request_diff": { + "id": 1, + "state": "collected", + "st_commits": [ + { + "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", + "parent_ids": [ + "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" + ], + "authored_date": "2014-02-27T10:01:38.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T10:01:38.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", + "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", + "parent_ids": [ + "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + ], + "authored_date": "2014-02-27T09:57:31.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:57:31.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", + "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", + "parent_ids": [ + "d14d6c0abdd253381df51a723d58691b2ee1ab08" + ], + "authored_date": "2014-02-27T09:54:21.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:54:21.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08", + "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", + "parent_ids": [ + "c1acaa58bbcbc3eafe538cb8274ba387047b69f8" + ], + "authored_date": "2014-02-27T09:49:50.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:49:50.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", + "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", + "parent_ids": [ + "ae73cb07c9eeaf35924a10f713b364d32b2dd34f" + ], + "authored_date": "2014-02-27T09:48:32.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:48:32.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + } + ], + "st_diffs": [ + { + "diff": "Binary files a/.DS_Store and /dev/null differ\n", + "new_path": ".DS_Store", + "old_path": ".DS_Store", + "a_mode": "100644", + "b_mode": "0", + "new_file": false, + "renamed_file": false, + "deleted_file": true, + "too_large": false + }, + { + "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n", + "new_path": ".gitignore", + "old_path": ".gitignore", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n", + "new_path": ".gitmodules", + "old_path": ".gitmodules", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "Binary files a/files/.DS_Store and /dev/null differ\n", + "new_path": "files/.DS_Store", + "old_path": "files/.DS_Store", + "a_mode": "100644", + "b_mode": "0", + "new_file": false, + "renamed_file": false, + "deleted_file": true, + "too_large": false + }, + { + "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", + "new_path": "files/ruby/popen.rb", + "old_path": "files/ruby/popen.rb", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n", + "new_path": "files/ruby/regex.rb", + "old_path": "files/ruby/regex.rb", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n", + "new_path": "gitlab-grack", + "old_path": "gitlab-grack", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n", + "new_path": "gitlab-shell", + "old_path": "gitlab-shell", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 1, + "created_at": "2016-04-13T14:40:33.474Z", + "updated_at": "2016-04-13T14:40:33.834Z", + "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", + "real_size": "8" + }, + "notes": [ + { + "id": 2, + "note": ":+1: merge_request", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-04-13T14:40:39.832Z", + "updated_at": "2016-04-13T14:40:39.832Z", + "project_id": 9, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 1, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false + } + ] + } + ], + "ci_commits": [ + { + "id": 2, + "project_id": 6, + "ref": "master", + "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "before_sha": null, + "push_data": null, + "created_at": "2016-04-13T14:40:37.759Z", + "updated_at": "2016-04-13T14:40:37.759Z", + "tag": false, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 6, + "statuses": [ + { + "id": 1, + "project_id": null, + "status": "success", + "finished_at": "2016-01-26T07:23:42.000Z", + "trace": null, + "created_at": "2016-04-13T14:40:37.717Z", + "updated_at": "2016-04-13T14:40:37.771Z", + "started_at": "2016-01-26T07:21:42.000Z", + "runner_id": null, + "coverage": null, + "commit_id": 2, + "commands": null, + "job_id": null, + "name": "default", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": null, + "trigger_request_id": null, + "stage_idx": null, + "tag": null, + "ref": null, + "user_id": null, + "target_url": null, + "description": "commit status", + "artifacts_file": null, + "gl_project_id": 7, + "artifacts_metadata": null, + "erased_by_id": null, + "erased_at": null + } + ] + } + ] +} \ No newline at end of file diff --git a/lib/api/api.rb b/lib/api/api.rb index 7d65145176b9059eb2ae12fcd76a9613bcd37dac..cc1004f80059e85ca310aa0ef55e73fe778f274b 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -57,5 +57,6 @@ module API mount Builds mount Variables mount Runners + mount Licenses end end diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 8e74e177ea05c5f66b10871508d765d915b34f1e..7388ed2f4eaf60fd0c834360dc885918e5e8b9a8 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -21,10 +21,9 @@ module API authorize!(:read_commit_status, user_project) not_found!('Commit') unless user_project.commit(params[:sha]) - ci_commit = user_project.ci_commit(params[:sha]) - return [] unless ci_commit - statuses = ci_commit.statuses + ci_commits = user_project.ci_commits.where(sha: params[:sha]) + statuses = ::CommitStatus.where(commit: ci_commits) statuses = statuses.latest unless parse_boolean(params[:all]) statuses = statuses.where(ref: params[:ref]) if params[:ref].present? statuses = statuses.where(stage: params[:stage]) if params[:stage].present? @@ -51,7 +50,21 @@ module API commit = @project.commit(params[:sha]) not_found! 'Commit' unless commit - ci_commit = @project.ensure_ci_commit(commit.sha) + # Since the CommitStatus is attached to Ci::Commit (in the future Pipeline) + # We need to always have the pipeline object + # To have a valid pipeline object that can be attached to specific MR + # Other CI service needs to send `ref` + # If we don't receive it, we will attach the CommitStatus to + # the first found branch on that commit + + ref = params[:ref] + unless ref + branches = @project.repository.branch_names_contains(commit.sha) + not_found! 'References for commit' if branches.none? + ref = branches.first + end + + ci_commit = @project.ensure_ci_commit(commit.sha, ref) name = params[:name] || params[:context] status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref]) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 939469b3886071523188804674c6a9bcccb1b992..716ca6f7ed9a0db7a7636b3651ca498591ef4ba4 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -263,14 +263,19 @@ module API expose :id, :path, :kind end - class ProjectAccess < Grape::Entity + class Member < Grape::Entity expose :access_level - expose :notification_level + expose :notification_level do |member, options| + if member.notification_setting + NotificationSetting.levels[member.notification_setting.level] + end + end end - class GroupAccess < Grape::Entity - expose :access_level - expose :notification_level + class ProjectAccess < Member + end + + class GroupAccess < Member end class ProjectService < Grape::Entity @@ -434,5 +439,17 @@ module API class Variable < Grape::Entity expose :key, :value end + + class RepoLicense < Grape::Entity + expose :key, :name, :nickname + expose :featured, as: :popular + expose :url, as: :html_url + expose(:source_url) { |license| license.meta['source'] } + expose(:description) { |license| license.meta['description'] } + expose(:conditions) { |license| license.meta['conditions'] } + expose(:permissions) { |license| license.meta['permissions'] } + expose(:limitations) { |license| license.meta['limitations'] } + expose :content + end end end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index c165de21a75728c5f42ee30206c9ea6a268f44b9..91e420832f388d943b6ea55fbf82949677a14d81 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -23,8 +23,10 @@ module API # Create group. Available only for users who can create groups. # # Parameters: - # name (required) - The name of the group - # path (required) - The path of the group + # name (required) - The name of the group + # path (required) - The path of the group + # description (optional) - The description of the group + # visibility_level (optional) - The visibility level of the group # Example Request: # POST /groups post do @@ -42,6 +44,28 @@ module API end end + # Update group. Available only for users who can administrate groups. + # + # Parameters: + # id (required) - The ID of a group + # path (optional) - The path of the group + # description (optional) - The description of the group + # visibility_level (optional) - The visibility level of the group + # Example Request: + # PUT /groups/:id + put ':id' do + group = find_group(params[:id]) + authorize! :admin_group, group + + attrs = attributes_for_keys [:name, :path, :description, :visibility_level] + + if ::Groups::UpdateService.new(group, current_user, attrs).execute + present group, with: Entities::GroupDetail + else + render_validation_error!(group) + end + end + # Get a single group, with containing projects # # Parameters: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 4921ae99e788436ab5e824d9bb1a6dd70b94f4ff..5bbf721321d71e1f6c67aafe859fd2a119086121 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -91,8 +91,7 @@ module API if can?(current_user, :read_group, group) group else - forbidden!("#{current_user.username} lacks sufficient "\ - "access to #{group.name}") + not_found!('Group') end end @@ -241,6 +240,10 @@ module API render_api_error!('413 Request Entity Too Large', 413) end + def not_modified! + render_api_error!('304 Not Modified', 304) + end + def render_validation_error!(model) if model.errors.any? render_api_error!(model.errors.messages || '400 Bad Request', 400) diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 2200208b946528a9bec8bfde4dfd276a3b437fc9..3ac7b50c4ce1f4dcb1cab3b4b89e6254d31f0818 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -23,9 +23,11 @@ module API end post "/allowed" do + Gitlab::Metrics.action = 'Grape#/internal/allowed' + status 200 - actor = + actor = if params[:key_id] Key.find_by(id: params[:key_id]) elsif params[:user_id] @@ -33,7 +35,7 @@ module API end project_path = params[:project] - + # Check for *.wiki repositories. # Strip out the .wiki from the pathname before finding the # project. This applies the correct project permissions to diff --git a/lib/api/issues.rb b/lib/api/issues.rb index c4ea05ee6cf3c0c31cba274f880213c0e7d3c502..8aa08fd5accf83c6026044b796a3aa719a293518 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -117,7 +117,7 @@ module API # assignee_id (optional) - The ID of a user to assign issue # milestone_id (optional) - The ID of a milestone to assign issue # labels (optional) - The labels of an issue - # created_at (optional) - The date + # created_at (optional) - Date time string, ISO 8601 formatted # Example Request: # POST /projects/:id/issues post ":id/issues" do @@ -166,12 +166,15 @@ module API # milestone_id (optional) - The ID of a milestone to assign issue # labels (optional) - The labels of an issue # state_event (optional) - The state event of an issue (close|reopen) + # updated_at (optional) - Date time string, ISO 8601 formatted # Example Request: # PUT /projects/:id/issues/:issue_id put ":id/issues/:issue_id" do issue = user_project.issues.find(params[:issue_id]) authorize! :update_issue, issue - attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :state_event] + keys = [:title, :description, :assignee_id, :milestone_id, :state_event] + keys << :updated_at if current_user.admin? || user_project.owner == current_user + attrs = attributes_for_keys(keys) # Validate label names in advance if (errors = validate_label_params(params)).any? @@ -195,6 +198,29 @@ module API end end + # Move an existing issue + # + # Parameters: + # id (required) - The ID of a project + # issue_id (required) - The ID of a project issue + # to_project_id (required) - The ID of the new project + # Example Request: + # POST /projects/:id/issues/:issue_id/move + post ':id/issues/:issue_id/move' do + required_attributes! [:to_project_id] + + issue = user_project.issues.find(params[:issue_id]) + new_project = Project.find(params[:to_project_id]) + + begin + issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project) + present issue, with: Entities::Issue, current_user: current_user + rescue ::Issues::MoveService::MoveError => error + render_api_error!(error.message, 400) + end + end + + # # Delete a project issue # # Parameters: @@ -208,6 +234,42 @@ module API authorize!(:destroy_issue, issue) issue.destroy end + + # Subscribes to a project issue + # + # Parameters: + # id (required) - The ID of a project + # issue_id (required) - The ID of a project issue + # Example Request: + # POST /projects/:id/issues/:issue_id/subscription + post ':id/issues/:issue_id/subscription' do + issue = user_project.issues.find(params[:issue_id]) + + if issue.subscribed?(current_user) + not_modified! + else + issue.toggle_subscription(current_user) + present issue, with: Entities::Issue, current_user: current_user + end + end + + # Unsubscribes from a project issue + # + # Parameters: + # id (required) - The ID of a project + # issue_id (required) - The ID of a project issue + # Example Request: + # DELETE /projects/:id/issues/:issue_id/subscription + delete ':id/issues/:issue_id/subscription' do + issue = user_project.issues.find(params[:issue_id]) + + if issue.subscribed?(current_user) + issue.unsubscribe(current_user) + present issue, with: Entities::Issue, current_user: current_user + else + not_modified! + end + end end end end diff --git a/lib/api/licenses.rb b/lib/api/licenses.rb new file mode 100644 index 0000000000000000000000000000000000000000..187d2c047036bb6c73eda93790d785fa6223dbf6 --- /dev/null +++ b/lib/api/licenses.rb @@ -0,0 +1,58 @@ +module API + # Licenses API + class Licenses < Grape::API + PROJECT_TEMPLATE_REGEX = + /[\<\{\[] + (project|description| + one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here + [\>\}\]]/xi.freeze + YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze + FULLNAME_TEMPLATE_REGEX = + /[\<\{\[] + (fullname|name\sof\s(author|copyright\sowner)) + [\>\}\]]/xi.freeze + + # Get the list of the available license templates + # + # Parameters: + # popular - Filter licenses to only the popular ones + # + # Example Request: + # GET /licenses + # GET /licenses?popular=1 + get 'licenses' do + options = { + featured: params[:popular].present? ? true : nil + } + present Licensee::License.all(options), with: Entities::RepoLicense + end + + # Get text for specific license + # + # Parameters: + # key (required) - The key of a license + # project - Copyrighted project name + # fullname - Full name of copyright holder + # + # Example Request: + # GET /licenses/mit + # + get 'licenses/:key', requirements: { key: /[\w\.-]+/ } do + required_attributes! [:key] + + not_found!('License') unless Licensee::License.find(params[:key]) + + # We create a fresh Licensee::License object since we'll modify its + # content in place below. + license = Licensee::License.new(params[:key]) + + license.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s) + license.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present? + + fullname = params[:fullname].presence || current_user.try(:name) + license.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname + + present license, with: Entities::RepoLicense + end + end +end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 4e7de8867b43aeedd564d088876d1c9cbb037b55..7e78609ecb9853e1566badece5528f49df8f9187 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -327,6 +327,42 @@ module API issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) present paginate(issues), with: Entities::Issue, current_user: current_user end + + # Subscribes to a merge request + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of a merge request + # Example Request: + # POST /projects/:id/issues/:merge_request_id/subscription + post "#{path}/subscription" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + if merge_request.subscribed?(current_user) + not_modified! + else + merge_request.toggle_subscription(current_user) + present merge_request, with: Entities::MergeRequest, current_user: current_user + end + end + + # Unsubscribes from a merge request + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of a merge request + # Example Request: + # DELETE /projects/:id/merge_requests/:merge_request_id/subscription + delete "#{path}/subscription" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + if merge_request.subscribed?(current_user) + merge_request.unsubscribe(current_user) + present merge_request, with: Entities::MergeRequest, current_user: current_user + else + not_modified! + end + end end end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index a1c98f5e8ff0376e85182c920b7471cfefbc541d..71a53e6f0d6f9d30e49def52fd16164cf24a3916 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -61,6 +61,7 @@ module API # id (required) - The ID of a project # noteable_id (required) - The ID of an issue or snippet # body (required) - The content of a note + # created_at (optional) - The date # Example Request: # POST /projects/:id/issues/:noteable_id/notes # POST /projects/:id/snippets/:noteable_id/notes @@ -73,6 +74,10 @@ module API noteable_id: params[noteable_id_str] } + if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user) + opts[:created_at] = params[:created_at] + end + @note = ::Notes::CreateService.new(user_project, current_user, opts).execute if @note.valid? diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 24b31005475fec499d06f56d69268ca8e414bf66..cc2c7a0c5032ece375a194ff363b73e8438bc6ef 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -272,6 +272,40 @@ module API present user_project, with: Entities::Project end + # Star project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # POST /projects/:id/star + post ':id/star' do + if current_user.starred?(user_project) + not_modified! + else + current_user.toggle_star(user_project) + user_project.reload + + present user_project, with: Entities::Project + end + end + + # Unstar project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # DELETE /projects/:id/star + delete ':id/star' do + if current_user.starred?(user_project) + current_user.toggle_star(user_project) + user_project.reload + + present user_project, with: Entities::Project + else + not_modified! + end + end + # Remove project # # Parameters: diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 0d0f0d4616d3d8f29d341cae1e10faf64cdfac2f..62161aadb9a09c714e95414978de82c05bd195eb 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -98,7 +98,6 @@ module API authorize! :download_code, user_project begin - RepositoryArchiveCacheWorker.perform_async header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format]) rescue not_found!('File') diff --git a/lib/api/tags.rb b/lib/api/tags.rb index d1a10479e448673e2b3effdeb89f54d94cd982e6..3e1ed3fe5c76b806fc6860cac1fd7caca2fd16c0 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -12,7 +12,7 @@ module API # Example Request: # GET /projects/:id/repository/tags get ":id/repository/tags" do - present user_project.repo.tags.sort_by(&:name).reverse, + present user_project.repository.tags.sort_by(&:name).reverse, with: Entities::RepoTag, project: user_project end diff --git a/lib/api/users.rb b/lib/api/users.rb index 0a14bac07c0ba2ec9c8b3a052a9095463daa8262..ea6fa2dc8a84cc82774f6deca1727719d63c1b2b 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -11,6 +11,10 @@ module API # GET /users?search=Admin # GET /users?username=root get do + unless can?(current_user, :read_users_list, nil) + render_api_error!("Not authorized.", 403) + end + if params[:username].present? @users = User.where(username: params[:username]) else @@ -36,10 +40,12 @@ module API get ":id" do @user = User.find(params[:id]) - if current_user.is_admin? + if current_user && current_user.is_admin? present @user, with: Entities::UserFull - else + elsif can?(current_user, :read_user, @user) present @user, with: Entities::User + else + render_api_error!("User not found.", 404) end end diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb index 4fc3443ac681381049d887b79d8df426239d079f..5f8ff01b0a93833b9ab4de6ad822f907e9cf11cc 100644 --- a/lib/award_emoji.rb +++ b/lib/award_emoji.rb @@ -14,19 +14,29 @@ class AwardEmoji food_drink: "Food" }.with_indifferent_access + CATEGORY_ALIASES = { + symbols: "objects_symbols", + foods: "food_drink", + travel: "travel_places" + }.with_indifferent_access + def self.normilize_emoji_name(name) aliases[name] || name end def self.emoji_by_category unless @emoji_by_category - @emoji_by_category = {} + @emoji_by_category = Hash.new { |h, key| h[key] = [] } emojis.each do |emoji_name, data| data["name"] = emoji_name - @emoji_by_category[data["category"]] ||= [] - @emoji_by_category[data["category"]] << data + # Skip Fitzpatrick(tone) modifiers + next if data["category"] == "modifier" + + category = CATEGORY_ALIASES[data["category"]] || data["category"] + + @emoji_by_category[category] << data end @emoji_by_category = @emoji_by_category.sort.to_h diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index a2987850d03210b1f4d14b980b48d53cc8a1f8c8..8488a493b55cfd8df648b3d76b32a5cbc36e8b9a 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -18,9 +18,7 @@ module Banzai def references_in(text, pattern = Label.reference_pattern) text.gsub(pattern) do |match| - project = project_from_ref($~[:project]) - params = label_params($~[:label_id].to_i, $~[:label_name]) - label = project.labels.find_by(params) + label = find_label($~[:project], $~[:label_id], $~[:label_name]) if label yield match, label.id, $~[:project], $~ @@ -30,18 +28,12 @@ module Banzai end end - def url_for_object(label, project) - h = Gitlab::Routing.url_helpers - h.namespace_project_issues_url(project.namespace, project, label_name: label.name, - only_path: context[:only_path]) - end + def find_label(project_ref, label_id, label_name) + project = project_from_ref(project_ref) + return unless project - def object_link_text(object, matches) - if context[:project] == object.project - LabelsHelper.render_colored_label(object) - else - LabelsHelper.render_colored_cross_project_label(object) - end + label_params = label_params(label_id, label_name) + project.labels.find_by(label_params) end # Parameters to pass to `Label.find_by` based on the given arguments @@ -55,7 +47,21 @@ module Banzai if name { name: name.tr('"', '') } else - { id: id } + { id: id.to_i } + end + end + + def url_for_object(label, project) + h = Gitlab::Routing.url_helpers + h.namespace_project_issues_url(project.namespace, project, label_name: label.name, + only_path: context[:only_path]) + end + + def object_link_text(object, matches) + if context[:project] == object.project + LabelsHelper.render_colored_label(object) + else + LabelsHelper.render_colored_cross_project_label(object) end end end diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 4e85d2c3c744003562cf27b9bc073f2992b552c9..353c4ddebf8edc3221ea5f0a484843a966d540f0 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -23,6 +23,8 @@ module Ci rack_response({ 'message' => '500 Internal Server Error' }, 500) end + content_type :txt, 'text/plain' + content_type :json, 'application/json' format :json helpers ::Ci::API::Helpers diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 2e9a5d311f9ba57a5437d6c0fdf3ea0fb21487f9..607359769d1537ff506cbf491de92ead1890a13c 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -50,6 +50,39 @@ module Ci end end + # Send incremental log update - Runners only + # + # Parameters: + # id (required) - The ID of a build + # Body: + # content of logs to append + # Headers: + # Content-Range (required) - range of content that was sent + # BUILD-TOKEN (required) - The build authorization token + # Example Request: + # PATCH /builds/:id/trace.txt + patch ":id/trace.txt" do + build = Ci::Build.find_by_id(params[:id]) + not_found! unless build + authenticate_build_token!(build) + forbidden!('Build has been erased!') if build.erased? + + error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range') + content_range = request.headers['Content-Range'] + content_range = content_range.split('-') + + current_length = build.trace_length + unless current_length == content_range[0].to_i + return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{current_length}" }) + end + + build.append_trace(request.body.read, content_range[0].to_i) + + status 202 + header 'Build-Status', build.status + header 'Range', "0-#{build.trace_length}" + end + # Authorize artifacts uploading for build - Runners only # # Parameters: diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index b7209c14148e5dddb4446e1bd17eef231950ff7b..ff9887cba1eeb9c0d66d5dc1db250800859d5f21 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -4,12 +4,12 @@ module Ci DEFAULT_STAGES = %w(build test deploy) DEFAULT_STAGE = 'test' - ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache] + ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache] ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when, :artifacts, :cache, - :dependencies] + :dependencies, :before_script, :after_script, :variables] - attr_reader :before_script, :image, :services, :variables, :path, :cache + attr_reader :before_script, :after_script, :image, :services, :path, :cache def initialize(config, path = nil) @config = YAML.safe_load(config, [Symbol], [], true) @@ -40,10 +40,22 @@ module Ci @stages || DEFAULT_STAGES end + def global_variables + @variables + end + + def job_variables(name) + job = @jobs[name.to_sym] + return [] unless job + + job.fetch(:variables, []) + end + private def initial_parsing @before_script = @config[:before_script] || [] + @after_script = @config[:after_script] @image = @config[:image] @services = @config[:services] @stages = @config[:stages] || @config[:types] @@ -72,7 +84,7 @@ module Ci { stage_idx: stages.index(job[:stage]), stage: job[:stage], - commands: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}", + commands: [job[:before_script] || @before_script, job[:script]].flatten.join("\n"), tag_list: job[:tags] || [], name: name, only: job[:only], @@ -85,23 +97,32 @@ module Ci artifacts: job[:artifacts], cache: job[:cache] || @cache, dependencies: job[:dependencies], + after_script: job[:after_script] || @after_script, }.compact } end - def normalize_script(script) - if script.is_a? Array - script.join("\n") - else - script + def validate! + validate_global! + + @jobs.each do |name, job| + validate_job!(name, job) end + + true end - def validate! + private + + def validate_global! unless validate_array_of_strings(@before_script) raise ValidationError, "before_script should be an array of strings" end + unless @after_script.nil? || validate_array_of_strings(@after_script) + raise ValidationError, "after_script should be an array of strings" + end + unless @image.nil? || @image.is_a?(String) raise ValidationError, "image should be a string" end @@ -115,43 +136,39 @@ module Ci end unless @variables.nil? || validate_variables(@variables) - raise ValidationError, "variables should be a map of key-valued strings" + raise ValidationError, "variables should be a map of key-value strings" end - if @cache - if @cache[:key] && !validate_string(@cache[:key]) - raise ValidationError, "cache:key parameter should be a string" - end - - if @cache[:untracked] && !validate_boolean(@cache[:untracked]) - raise ValidationError, "cache:untracked parameter should be an boolean" - end + validate_global_cache! if @cache + end - if @cache[:paths] && !validate_array_of_strings(@cache[:paths]) - raise ValidationError, "cache:paths parameter should be an array of strings" - end + def validate_global_cache! + if @cache[:key] && !validate_string(@cache[:key]) + raise ValidationError, "cache:key parameter should be a string" end - @jobs.each do |name, job| - validate_job!(name, job) + if @cache[:untracked] && !validate_boolean(@cache[:untracked]) + raise ValidationError, "cache:untracked parameter should be an boolean" end - true + if @cache[:paths] && !validate_array_of_strings(@cache[:paths]) + raise ValidationError, "cache:paths parameter should be an array of strings" + end end def validate_job!(name, job) validate_job_name!(name) validate_job_keys!(name, job) validate_job_types!(name, job) + validate_job_script!(name, job) validate_job_stage!(name, job) if job[:stage] + validate_job_variables!(name, job) if job[:variables] validate_job_cache!(name, job) if job[:cache] validate_job_artifacts!(name, job) if job[:artifacts] validate_job_dependencies!(name, job) if job[:dependencies] end - private - def validate_job_name!(name) if name.blank? || !validate_string(name) raise ValidationError, "job name should be non-empty string" @@ -167,10 +184,6 @@ module Ci end def validate_job_types!(name, job) - if !validate_string(job[:script]) && !validate_array_of_strings(job[:script]) - raise ValidationError, "#{name} job: script should be a string or an array of a strings" - end - if job[:image] && !validate_string(job[:image]) raise ValidationError, "#{name} job: image should be a string" end @@ -200,12 +213,33 @@ module Ci end end + def validate_job_script!(name, job) + if !validate_string(job[:script]) && !validate_array_of_strings(job[:script]) + raise ValidationError, "#{name} job: script should be a string or an array of a strings" + end + + if job[:before_script] && !validate_array_of_strings(job[:before_script]) + raise ValidationError, "#{name} job: before_script should be an array of strings" + end + + if job[:after_script] && !validate_array_of_strings(job[:after_script]) + raise ValidationError, "#{name} job: after_script should be an array of strings" + end + end + def validate_job_stage!(name, job) unless job[:stage].is_a?(String) && job[:stage].in?(stages) raise ValidationError, "#{name} job: stage parameter should be #{stages.join(", ")}" end end + def validate_job_variables!(name, job) + unless validate_variables(job[:variables]) + raise ValidationError, + "#{name} job: variables should be a map of key-value strings" + end + end + def validate_job_cache!(name, job) if job[:cache][:key] && !validate_string(job[:cache][:key]) raise ValidationError, "#{name} job: cache:key parameter should be a string" diff --git a/lib/ci/status.rb b/lib/ci/status.rb deleted file mode 100644 index 3fb1fe29494adc79d6780630ba82ae552b04dce5..0000000000000000000000000000000000000000 --- a/lib/ci/status.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Ci - class Status - def self.get_status(statuses) - if statuses.none? - 'skipped' - elsif statuses.all? { |status| status.success? || status.ignored? } - 'success' - elsif statuses.all?(&:pending?) - 'pending' - elsif statuses.any?(&:running?) || statuses.any?(&:pending?) - 'running' - elsif statuses.all?(&:canceled?) - 'canceled' - else - 'failed' - end - end - end -end diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb index 2eae55e534b4e6d9ed0a5b1dcb44d13bb959772d..440dd44ece72bd628f52ff14bd20bda3344edf4a 100644 --- a/lib/file_size_validator.rb +++ b/lib/file_size_validator.rb @@ -1,9 +1,9 @@ class FileSizeValidator < ActiveModel::EachValidator - MESSAGES = { is: :wrong_size, minimum: :size_too_small, maximum: :size_too_big }.freeze - CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze + MESSAGES = { is: :wrong_size, minimum: :size_too_small, maximum: :size_too_big }.freeze + CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze - DEFAULT_TOKENIZER = lambda { |value| value.split(//) } - RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long] + DEFAULT_TOKENIZER = -> (value) { value.split(//) }.freeze + RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long].freeze def initialize(options) if range = (options.delete(:in) || options.delete(:within)) diff --git a/lib/gitlab.rb b/lib/gitlab.rb index 6108697bc2012e22d96896194df7c7cadbc5b125..7479e729db14421948590b64c3cffbfec2c1297f 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -1,4 +1,7 @@ require 'gitlab/git' module Gitlab + def self.com? + Gitlab.config.gitlab.url == 'https://gitlab.com' + end end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index b9bb6e76081bcf0f6b1679592324abe5797384f2..5e2fb863a8fe5d45ce09fa35f8042e6ca73ca3f6 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -54,19 +54,6 @@ module Gitlab "#{path}.git", "#{new_path}.git"]) end - # Update HEAD for repository - # - # path - project path with namespace - # branch - repository branch name - # - # Ex. - # update_repository_head("gitlab/gitlab-ci", "3-1-stable") - # - def update_repository_head(path, branch) - Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'update-head', - "#{path}.git", branch]) - end - # Fork repository to new namespace # # path - project path with namespace @@ -92,33 +79,6 @@ module Gitlab 'rm-project', "#{name}.git"]) end - # Add repository branch from passed ref - # - # path - project path with namespace - # branch_name - new branch name - # ref - HEAD for new branch - # - # Ex. - # add_branch("gitlab/gitlab-ci", "4-0-stable", "master") - # - def add_branch(path, branch_name, ref) - Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'create-branch', - "#{path}.git", branch_name, ref]) - end - - # Remove repository branch - # - # path - project path with namespace - # branch_name - branch name to remove - # - # Ex. - # rm_branch("gitlab/gitlab-ci", "4-0-stable") - # - def rm_branch(path, branch_name) - Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'rm-branch', - "#{path}.git", branch_name]) - end - # Add repository tag from passed ref # # path - project path with namespace @@ -137,19 +97,6 @@ module Gitlab Gitlab::Utils.system_silent(cmd) end - # Remove repository tag - # - # path - project path with namespace - # tag_name - tag name to remove - # - # Ex. - # rm_tag("gitlab/gitlab-ci", "v4.0") - # - def rm_tag(path, tag_name) - Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'rm-tag', - "#{path}.git", tag_name]) - end - # Gc repository # # path - project path with namespace diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb index d88a6eaac6b65b59a6d74f693ee1975086063037..9bb507b5edda65e528d0ad5398289b47e6581457 100644 --- a/lib/gitlab/bitbucket_import/client.rb +++ b/lib/gitlab/bitbucket_import/client.rb @@ -5,6 +5,17 @@ module Gitlab attr_reader :consumer, :api + def self.from_project(project) + import_data_credentials = project.import_data.credentials if project.import_data + if import_data_credentials && import_data_credentials[:bb_session] + token = import_data_credentials[:bb_session][:bitbucket_access_token] + token_secret = import_data_credentials[:bb_session][:bitbucket_access_token_secret] + new(token, token_secret) + else + raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" + end + end + def initialize(access_token = nil, access_token_secret = nil) @consumer = ::OAuth::Consumer.new( config.app_id, @@ -54,7 +65,7 @@ module Gitlab def issues(project_identifier) all_issues = [] offset = 0 - per_page = 50 # Maximum number allowed by Bitbucket + per_page = 50 # Maximum number allowed by Bitbucket index = 0 begin @@ -120,7 +131,7 @@ module Gitlab end def config - Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket"} + Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket" } end def bitbucket_options diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 46e51a4bf6d9868bc16dc2c439da3cf981c7b799..7beaecd1cf065637cf09a672e8ac843bce4bfbae 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -5,10 +5,7 @@ module Gitlab def initialize(project) @project = project - import_data = project.import_data.try(:data) - bb_session = import_data["bb_session"] if import_data - @client = Client.new(bb_session["bitbucket_access_token"], - bb_session["bitbucket_access_token_secret"]) + @client = Client.from_project(@project) @formatter = Gitlab::ImportFormatter.new end diff --git a/lib/gitlab/bitbucket_import/key_deleter.rb b/lib/gitlab/bitbucket_import/key_deleter.rb index f4dd393ad29f04c9ee43b6fa7319b4f59a2adf82..e03c3155b3ebeebf18e9e80fe62d135596fc2e88 100644 --- a/lib/gitlab/bitbucket_import/key_deleter.rb +++ b/lib/gitlab/bitbucket_import/key_deleter.rb @@ -6,10 +6,7 @@ module Gitlab def initialize(project) @project = project @current_user = project.creator - import_data = project.import_data.try(:data) - bb_session = import_data["bb_session"] if import_data - @client = Client.new(bb_session["bitbucket_access_token"], - bb_session["bitbucket_access_token_secret"]) + @client = Client.from_project(@project) end def execute diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index 03aac1a025a8d14b244a62b15cec763e56d16fb6..941f818b8478f6f76d2ef422a51304b3178b6273 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -23,7 +23,8 @@ module Gitlab import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git", ).execute - project.create_import_data(data: { "bb_session" => session_data } ) + project.create_or_update_import_data(credentials: { bb_session: session_data }) + project end end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 1acc22fe5bfe1c409d6ecdf1c55dc33db4973db5..f44d1b3a44ec91dc1c3c351ec28a17ff63997bb4 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -34,7 +34,8 @@ module Gitlab max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, two_factor_grace_period: 48, - akismet_enabled: false + akismet_enabled: false, + repository_checks_enabled: true, ) end diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index c2260a5f7acc806d29d9e6061237ad429fb5474a..ffe49364379b253df0c45d9bb061d1765401bdc0 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -52,11 +52,6 @@ module Gitlab private - def redis - # Maybe someday we want to use a connection pool... - @redis ||= Redis.new(url: Gitlab::RedisConfig.url) - end - def redis_key "gitlab:exclusive_lease:#{@key}" end diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index db580b5e578725513e9f73b2ed97ae8a519632b0..501d5a955478abb6aa4cc112e1446bfe1d313d30 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -8,17 +8,17 @@ module Gitlab import_data = project.import_data.try(:data) repo_data = import_data['repo'] if import_data - @repo = FogbugzImport::Repository.new(repo_data) - - @known_labels = Set.new + if repo_data + @repo = FogbugzImport::Repository.new(repo_data) + @known_labels = Set.new + else + raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" + end end def execute return true unless repo.valid? - - data = project.import_data.try(:data) - - client = Gitlab::FogbugzImport::Client.new(token: data['fb_session']['token'], uri: data['fb_session']['uri']) + client = Gitlab::FogbugzImport::Client.new(token: fb_session[:token], uri: fb_session[:uri]) @cases = client.cases(@repo.id.to_i) @categories = client.categories @@ -30,6 +30,10 @@ module Gitlab private + def fb_session + @import_data_credentials ||= project.import_data.credentials[:fb_session] if project.import_data && project.import_data.credentials + end + def user_map @user_map ||= begin user_map = Hash.new @@ -236,9 +240,8 @@ module Gitlab end def build_attachment_url(rel_url) - data = project.import_data.try(:data) - uri = data['fb_session']['uri'] - token = data['fb_session']['token'] + uri = fb_session[:uri] + token = fb_session[:token] "#{uri}/#{rel_url}&token=#{token}" end diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb index e0163499e3094066b386167d9527eaa60f6158fa..3840765db878139a444bf2786a03fc49b8e86817 100644 --- a/lib/gitlab/fogbugz_import/project_creator.rb +++ b/lib/gitlab/fogbugz_import/project_creator.rb @@ -24,13 +24,7 @@ module Gitlab import_url: Project::UNKNOWN_IMPORT_URL ).execute - project.create_import_data( - data: { - 'repo' => repo.raw_data, - 'user_map' => user_map, - 'fb_session' => fb_session - } - ) + project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session }) project end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 172c5441e36fade9cbc61b78cb59437538d9fe10..0f9e3ee14ee742bbc31c419c56334b7d5abcff73 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -7,18 +7,45 @@ module Gitlab def initialize(project) @project = project - import_data = project.import_data.try(:data) - github_session = import_data["github_session"] if import_data - @client = Client.new(github_session["github_access_token"]) - @formatter = Gitlab::ImportFormatter.new + if import_data_credentials + @client = Client.new(import_data_credentials[:user]) + @formatter = Gitlab::ImportFormatter.new + else + raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" + end end def execute - import_issues && import_pull_requests && import_wiki + import_labels && import_milestones && import_issues && + import_pull_requests && import_wiki end private + def import_data_credentials + @import_data_credentials ||= project.import_data.credentials if project.import_data + end + + def import_labels + client.labels(project.import_source).each do |raw_data| + Label.create!(LabelFormatter.new(project, raw_data).attributes) + end + + true + rescue ActiveRecord::RecordInvalid => e + raise Projects::ImportService::Error, e.message + end + + def import_milestones + client.list_milestones(project.import_source, state: :all).each do |raw_data| + Milestone.create!(MilestoneFormatter.new(project, raw_data).attributes) + end + + true + rescue ActiveRecord::RecordInvalid => e + raise Projects::ImportService::Error, e.message + end + def import_issues client.list_issues(project.import_source, state: :all, sort: :created, @@ -27,6 +54,7 @@ module Gitlab if gh_issue.valid? issue = Issue.create!(gh_issue.attributes) + apply_labels(gh_issue.number, issue) if gh_issue.has_comments? import_comments(gh_issue.number, issue) @@ -49,6 +77,7 @@ module Gitlab merge_request = MergeRequest.new(pull_request.attributes) if merge_request.save + apply_labels(pull_request.number, merge_request) import_comments(pull_request.number, merge_request) import_comments_on_diff(pull_request.number, merge_request) end @@ -60,6 +89,18 @@ module Gitlab raise Projects::ImportService::Error, e.message end + def apply_labels(number, issuable) + issue = client.issue(project.import_source, number) + + if issue.labels.count > 0 + label_ids = issue.labels.map do |raw| + Label.find_by(LabelFormatter.new(project, raw).attributes).try(:id) + end + + issuable.update_attribute(:label_ids, label_ids) + end + end + def import_comments(issue_number, noteable) comments = client.issue_comments(project.import_source, issue_number) create_comments(comments, noteable) diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb index 1e3ba44f27c273f24a2bb05f45c1917d6459a0aa..c8173913b4e3975044aa86ef0db97daf9250fb0e 100644 --- a/lib/gitlab/github_import/issue_formatter.rb +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -3,7 +3,9 @@ module Gitlab class IssueFormatter < BaseFormatter def attributes { + iid: number, project: project, + milestone: milestone, title: raw_data.title, description: description, state: state, @@ -54,6 +56,12 @@ module Gitlab @formatter.author_line(author) + body end + def milestone + if raw_data.milestone.present? + project.milestones.find_by(iid: raw_data.milestone.number) + end + end + def state raw_data.state == 'closed' ? 'closed' : 'opened' end diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb new file mode 100644 index 0000000000000000000000000000000000000000..c2b9d40b511ab15e939644476300562101e394b4 --- /dev/null +++ b/lib/gitlab/github_import/label_formatter.rb @@ -0,0 +1,23 @@ +module Gitlab + module GithubImport + class LabelFormatter < BaseFormatter + def attributes + { + project: project, + title: title, + color: color + } + end + + private + + def color + "##{raw_data.color}" + end + + def title + raw_data.name + end + end + end +end diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb new file mode 100644 index 0000000000000000000000000000000000000000..e91a7e328cfb0d6c92821c3911a56a61a12dedf5 --- /dev/null +++ b/lib/gitlab/github_import/milestone_formatter.rb @@ -0,0 +1,48 @@ +module Gitlab + module GithubImport + class MilestoneFormatter < BaseFormatter + def attributes + { + iid: number, + project: project, + title: title, + description: description, + due_date: due_date, + state: state, + created_at: created_at, + updated_at: updated_at + } + end + + private + + def number + raw_data.number + end + + def title + raw_data.title + end + + def description + raw_data.description + end + + def due_date + raw_data.due_on + end + + def state + raw_data.state == 'closed' ? 'closed' : 'active' + end + + def created_at + raw_data.created_at + end + + def updated_at + state == 'closed' ? raw_data.closed_at : raw_data.updated_at + end + end + end +end diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb index 474927069a50878ae21ddc7519bc0f4a4438bca9..f4221003db5119a171c1a69921cbb4f9a4f15615 100644 --- a/lib/gitlab/github_import/project_creator.rb +++ b/lib/gitlab/github_import/project_creator.rb @@ -11,7 +11,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo.name, path: repo.name, @@ -23,9 +23,6 @@ module Gitlab import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"), wiki_enabled: !repo.has_wiki? # If repo has wiki we'll import it later ).execute - - project.create_import_data(data: { "github_session" => session_data } ) - project end end end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 4e507b090e8d3b94ed1237098e862c089a6f019e..d21b942ad4bc54064b46b1a365c67ac79a134c65 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -3,6 +3,7 @@ module Gitlab class PullRequestFormatter < BaseFormatter def attributes { + iid: number, title: raw_data.title, description: description, source_project: source_project, @@ -10,6 +11,7 @@ module Gitlab target_project: target_project, target_branch: target_branch.name, state: state, + milestone: milestone, author_id: author_id, assignee_id: assignee_id, created_at: raw_data.created_at, @@ -57,6 +59,12 @@ module Gitlab formatter.author_line(author) + body end + def milestone + if raw_data.milestone.present? + project.milestones.find_by(iid: raw_data.milestone.number) + end + end + def source_project project end diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb index 850b73244c628fe5d60718a27fefdca38704eb7c..96717b42bae29ad67504348f704424c515766333 100644 --- a/lib/gitlab/gitlab_import/importer.rb +++ b/lib/gitlab/gitlab_import/importer.rb @@ -5,10 +5,13 @@ module Gitlab def initialize(project) @project = project - import_data = project.import_data.try(:data) - gitlab_session = import_data["gitlab_session"] if import_data - @client = Client.new(gitlab_session["gitlab_access_token"]) - @formatter = Gitlab::ImportFormatter.new + credentials = import_data + if credentials && credentials[:password] + @client = Client.new(credentials[:password]) + @formatter = Gitlab::ImportFormatter.new + else + raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" + end end def execute diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb index 7baaadb813c60ab555f2f4c993bc6c5b5f24c2dc..77c33db4b593ccda5b78b56e52184bb23266220e 100644 --- a/lib/gitlab/gitlab_import/project_creator.rb +++ b/lib/gitlab/gitlab_import/project_creator.rb @@ -23,7 +23,6 @@ module Gitlab import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{@session_data[:gitlab_access_token]}@") ).execute - project.create_import_data(data: { "gitlab_session" => session_data } ) project end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..5ebaad6ca6e7d9cd6ad75da6a03845721e601a5e --- /dev/null +++ b/lib/gitlab/gon_helper.rb @@ -0,0 +1,17 @@ +module Gitlab + module GonHelper + def add_gon_variables + gon.api_version = API::API.version + gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s + gon.default_issues_tracker = Project.new.default_issue_tracker.to_param + gon.max_file_size = current_application_settings.max_attachment_size + gon.relative_url_root = Gitlab.config.gitlab.relative_url_root + gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class + + if current_user + gon.current_user_id = current_user.id + gon.api_token = current_user.private_token + end + end + end +end diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb index 87821c2346094f94869b8144a2a6412c739618af..0abb7a64c17e39d34ed0ddb820a717740d69b324 100644 --- a/lib/gitlab/google_code_import/project_creator.rb +++ b/lib/gitlab/google_code_import/project_creator.rb @@ -24,12 +24,7 @@ module Gitlab import_url: repo.import_url ).execute - project.create_import_data( - data: { - "repo" => repo.raw_data, - "user_map" => user_map - } - ) + project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }) project end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index fe88850c33dca4ed81fb69dbfae72feda67ecb00..99fa70a6ec1266785535c454fbe5bfdcd8219c1b 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -3,19 +3,21 @@ module Gitlab extend self def export_path(relative_path:) - File.join(storage_path, relative_path, "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_gitlab_export") + File.join(storage_path, relative_path) end def project_atts %i(name path description issues_enabled wall_enabled merge_requests_enabled wiki_enabled snippets_enabled visibility_level archived) end + def project_tree_list + project_tree.map {|r| r.is_a?(Hash) ? r.keys.first : r } + end + def project_tree Gitlab::ImportExport::ImportExportReader.project_tree end - private - def storage_path File.join(Settings.shared['path'], 'tmp/project_exports') end diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 7bf4b476b6c8d6695688cae82f3af994675c9a0c..3665e2edb7d6f565a66dacc57be56085d495c883 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -5,6 +5,14 @@ module Gitlab tar_with_options(archive: archive, dir: dir, options: 'cf') end + def untar_czf(archive:, dir:) + untar_with_options(archive: archive, dir: dir, options: 'czf') + end + + def untar_cf(archive:, dir:) + untar_with_options(archive: archive, dir: dir, options: 'cf') + end + def tar_czf(archive:, dir:) tar_with_options(archive: archive, dir: dir, options: 'czf') end @@ -16,7 +24,13 @@ module Gitlab end def tar_with_options(archive:, dir:, options:) - cmd = %W(tar -#{options} #{archive} -C #{dir} .) + cmd = %W(tar -#{options} #{archive} -C #{dir}) + _output, status = Gitlab::Popen.popen(cmd) + status.zero? + end + + def untar_with_options(archive:, dir:, options:) + cmd = %W(tar -#{options} #{archive} -C #{dir}) _output, status = Gitlab::Popen.popen(cmd) status.zero? end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 92f492e9013af99404e2d123936f845642027fbf..f7a0d8fe6cec93cc572e656e6b316d548996f17a 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -1,4 +1,4 @@ -# Class relationships to be included in the project import/export +# Model relationships to be included in the project import/export :project_tree: - :issues: - :notes @@ -15,6 +15,7 @@ - :ci_commits: - :statuses +# Only include the following attributes for the models specified. :attributes_only: :project: - :name @@ -32,6 +33,7 @@ - :email - :username +# Do not include the following attributes for the models specified. :attributes_except: :snippets: - :expired_at \ No newline at end of file diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb new file mode 100644 index 0000000000000000000000000000000000000000..9f399845437169f0c0097a00320dc6e1d66fbe79 --- /dev/null +++ b/lib/gitlab/import_export/importer.rb @@ -0,0 +1,26 @@ +module Gitlab + module ImportExport + class Importer + include Gitlab::ImportExport::CommandLineUtil + + def self.import(*args) + new(*args).import + end + + def initialize(archive_file: , storage_path:) + @archive_file = archive_file + @storage_path = storage_path + end + + def import + decompress_archive + end + + private + + def decompress_archive + untar_czf(archive: @archive_file, dir: @storage_path) + end + end + end +end diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb new file mode 100644 index 0000000000000000000000000000000000000000..d6124106f575fc0b7f86e2e3a2bb4da54380c5e8 --- /dev/null +++ b/lib/gitlab/import_export/members_mapper.rb @@ -0,0 +1,59 @@ +module Gitlab + module ImportExport + class MembersMapper + + def self.map(*args) + new(*args).map + end + + def initialize(exported_members:, user:, project_id:) + @exported_members = exported_members + @user = user + @project_id = project_id + end + + def map + @project_member_map = Hash.new(default_project_member) + @exported_members.each do |member| + existing_user = User.where(find_project_user_query(member)).first + assign_member(existing_user, member) if existing_user + end + @project_member_map + end + + private + + def assign_member(existing_user, member) + old_user_id = member['user']['id'] + member['user'] = existing_user + project_member = ProjectMember.new(member_hash(member)) + @project_member_map[old_user_id] = project_member.user.id if project_member.save + end + + def member_hash(member) + member.except('id').merge(source_id: @project_id) + end + + #TODO: If default, then we need to leave a comment 'Comment by <original username>' on comments + def default_project_member + @default_project_member ||= + begin + default_member = ProjectMember.new(default_project_member_hash) + default_member.user.id if default_member.save + end + end + + def default_project_member_hash + { user: @user, access_level: ProjectMember::MASTER, source_id: @project_id } + end + + def find_project_user_query(member) + user_arel[:username].eq(member['user']['username']).or(user_arel[:email].eq(member['user']['email'])) + end + + def user_arel + @user_arel ||= User.arel_table + end + end + end +end diff --git a/lib/gitlab/import_export/project_factory.rb b/lib/gitlab/import_export/project_factory.rb new file mode 100644 index 0000000000000000000000000000000000000000..c7137844a0a286a78a969795f260b0d2905d6354 --- /dev/null +++ b/lib/gitlab/import_export/project_factory.rb @@ -0,0 +1,40 @@ +module Gitlab + module ImportExport + module ProjectFactory + extend self + + def create(project_params:, user:) + project = Project.new(project_params.except('id')) + project.creator = user + check_namespace(project_params['namespace_id'], project, user) + end + + def check_namespace(namespace_id, project, user) + if namespace_id + # Find matching namespace and check if it allowed + # for current user if namespace_id passed. + unless allowed_namespace?(user, namespace_id) + project.namespace_id = nil + deny_namespace(project) + end + else + # Set current user namespace if namespace_id is nil + project.namespace_id = user.namespace_id + end + project + end + + private + + def allowed_namespace?(user, namespace_id) + namespace = Namespace.find_by(id: namespace_id) + user.can?(:create_projects, namespace) + end + + def deny_namespace(project) + project.errors.add(:namespace, "is not valid") + end + + end + end +end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb new file mode 100644 index 0000000000000000000000000000000000000000..4e0f555afe93fc8a564ae3f0ef736bfa77101c70 --- /dev/null +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -0,0 +1,87 @@ +module Gitlab + module ImportExport + class ProjectTreeRestorer + attr_reader :project + + def initialize(path:, user:) + @path = File.join(path, 'project.json') + @user = user + end + + def restore + json = IO.read(@path) + @tree_hash = ActiveSupport::JSON.decode(json) + @project_members = @tree_hash.delete('project_members') + create_relations + end + + private + + def members_map + @members ||= Gitlab::ImportExport::MembersMapper.map( + exported_members: @project_members, user: @user, project_id: project.id) + end + + def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) + saved = [] + relation_list.each do |relation| + next if !relation.is_a?(Hash) && tree_hash[relation.to_s].blank? + if relation.is_a?(Hash) + create_sub_relations(relation, tree_hash) + end + relation_key = relation.is_a?(Hash) ? relation.keys.first : relation + relation_hash = create_relation(relation_key, tree_hash[relation_key.to_s]) + saved << project.update_attribute(relation_key, relation_hash) + end + saved.all? + end + + def default_relation_list + Gitlab::ImportExport::ImportExportReader.tree.reject { |model| model.is_a?(Hash) && model[:project_members] } + end + + def project + @project ||= create_project + end + + def create_project + project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } + project = Gitlab::ImportExport::ProjectFactory.create( + project_params: project_params, user: @user) + project.save + project + end + + def create_sub_relations(relation, tree_hash) + tree_hash[relation.keys.first.to_s].each do |relation_item| + relation.values.flatten.each do |sub_relation| + relation_hash = relation_item[sub_relation.to_s] + next if relation_hash.blank? + process_sub_relation(relation_hash, relation_item, sub_relation) + end + end + end + + def process_sub_relation(relation_hash, relation_item, sub_relation) + sub_relation_object = nil + if relation_hash.is_a?(Array) + sub_relation_object = create_relation(sub_relation, relation_hash) + else + sub_relation_object = relation_from_factory(sub_relation, relation_hash) + end + relation_item[sub_relation.to_s] = sub_relation_object + end + + def create_relation(relation, relation_hash_list) + [relation_hash_list].flatten.map do |relation_hash| + relation_from_factory(relation, relation_hash) + end + end + + def relation_from_factory(relation, relation_hash) + Gitlab::ImportExport::RelationFactory.create( + relation_sym: relation, relation_hash: relation_hash.merge('project_id' => project.id), members_map: members_map) + end + end + end +end diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index b2615f8273b382bc50f194be5a1f69c1782b3e16..f6fab0fe22d7cf1fd4efefa216cd96de44ade4d2 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -25,7 +25,7 @@ module Gitlab end def project_filename - "#{@project.name}.json" + "project.json" end def project_json_tree diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb new file mode 100644 index 0000000000000000000000000000000000000000..dd992ef443eed3d53bbc26fa257b1c75136335fc --- /dev/null +++ b/lib/gitlab/import_export/relation_factory.rb @@ -0,0 +1,60 @@ +module Gitlab + module ImportExport + module RelationFactory + extend self + + OVERRIDES = { snippets: :project_snippets, ci_commits: 'Ci::Commit', statuses: 'commit_status' }.freeze + USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze + + def create(relation_sym:, relation_hash:, members_map:) + relation_sym = parse_relation_sym(relation_sym) + klass = parse_relation(relation_hash, relation_sym) + + update_user_references(relation_hash, members_map) + update_project_references(relation_hash, klass) + + imported_object(klass, relation_hash) + end + + private + + def update_user_references(relation_hash, members_map) + USER_REFERENCES.each do |reference| + if relation_hash[reference] + relation_hash[reference] = members_map[relation_hash[reference]] + end + end + end + + def update_project_references(relation_hash, klass) + project_id = relation_hash.delete('project_id') + + # project_id may not be part of the export, but we always need to populate it if required. + relation_hash['project_id'] = project_id if klass.column_names.include?('project_id') + relation_hash['gl_project_id'] = project_id if relation_hash ['gl_project_id'] + relation_hash['target_project_id'] = project_id if relation_hash['target_project_id'] + relation_hash['source_project_id'] = -1 if relation_hash['source_project_id'] + end + + def relation_class(relation_sym) + relation_sym.to_s.classify.constantize + end + + def parse_relation_sym(relation_sym) + OVERRIDES[relation_sym] || relation_sym + end + + def imported_object(klass, relation_hash) + imported_object = klass.new(relation_hash) + imported_object.importing = true if imported_object.respond_to?(:importing) + imported_object + end + + def parse_relation(relation_hash, relation_sym) + klass = relation_class(relation_sym) + relation_hash.delete('id') + klass + end + end + end +end diff --git a/lib/gitlab/import_export/repo_bundler.rb b/lib/gitlab/import_export/repo_bundler.rb index 7a1c2a12a536136329cf1080088e11ac137d2061..86c9501b708697c052d7f67d6b73ad2322adfef2 100644 --- a/lib/gitlab/import_export/repo_bundler.rb +++ b/lib/gitlab/import_export/repo_bundler.rb @@ -27,7 +27,7 @@ module Gitlab end def project_filename - "#{@project.name}.bundle" + "project.bundle" end def path_to_repo diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb new file mode 100644 index 0000000000000000000000000000000000000000..47be303e22ae83326e4a2d437c510df3155cf6ce --- /dev/null +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -0,0 +1,31 @@ +module Gitlab + module ImportExport + class RepoRestorer + include Gitlab::ImportExport::CommandLineUtil + + def initialize(project: , path:, bundler_file: ) + @project = project + @path = File.join(path, bundler_file) + end + + def restore + return false unless File.exists?(@path) + # Move repos dir to 'repositories.old' dir + + FileUtils.mkdir_p(repos_path) + FileUtils.mkdir_p(path_to_repo) + untar_cf(archive: @path, dir: path_to_repo) + end + + private + + def repos_path + Gitlab.config.gitlab_shell.repos_path + end + + def path_to_repo + @project.repository.path_to_repo + end + end + end +end diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index f26804d240289e974e79142b6d91dcfa013d7c85..f87e0fdc7ea20a628220d7fcf5b62a42a5ead753 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -31,7 +31,7 @@ module Gitlab end def archive_file - @archive_file ||= File.join(@storage_path, '..', 'project.tar.gz') + @archive_file ||= File.join(@storage_path, '..', "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_project_export.tar.gz") end end end diff --git a/lib/gitlab/import_export/wiki_repo_bundler.rb b/lib/gitlab/import_export/wiki_repo_bundler.rb index 9ef0febee54f1cd89bba488fa4ffc3dbdfb12542..7821c6716280947b491b371317464000dc9b5799 100644 --- a/lib/gitlab/import_export/wiki_repo_bundler.rb +++ b/lib/gitlab/import_export/wiki_repo_bundler.rb @@ -19,7 +19,7 @@ module Gitlab private def project_filename - "#{@project.name}.wiki.bundle" + "project.wiki.bundle" end def path_to_repo diff --git a/lib/gitlab/import_url.rb b/lib/gitlab/import_url.rb new file mode 100644 index 0000000000000000000000000000000000000000..d23b013c1f56e9673771e28fca645d12a0e31624 --- /dev/null +++ b/lib/gitlab/import_url.rb @@ -0,0 +1,41 @@ +module Gitlab + class ImportUrl + def initialize(url, credentials: nil) + @url = URI.parse(URI.encode(url)) + @credentials = credentials + end + + def sanitized_url + @sanitized_url ||= safe_url.to_s + end + + def credentials + @credentials ||= { user: @url.user, password: @url.password } + end + + def full_url + @full_url ||= generate_full_url.to_s + end + + private + + def generate_full_url + return @url unless valid_credentials? + @full_url = @url.dup + @full_url.user = credentials[:user] + @full_url.password = credentials[:password] + @full_url + end + + def safe_url + safe_url = @url.dup + safe_url.password = nil + safe_url.user = nil + safe_url + end + + def valid_credentials? + credentials && credentials.is_a?(Hash) && credentials.any? + end + end +end diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 2a0a5629be55093c7ed13756e6115e7e072a18c7..49f702f91f68e850da83cff3ac8f765b3214bef4 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -14,7 +14,8 @@ module Gitlab method_call_threshold: current_application_settings[:metrics_method_call_threshold], host: current_application_settings[:metrics_host], port: current_application_settings[:metrics_port], - sample_interval: current_application_settings[:metrics_sample_interval] || 15 + sample_interval: current_application_settings[:metrics_sample_interval] || 15, + packet_size: current_application_settings[:metrics_packet_size] || 1 } end @@ -41,9 +42,9 @@ module Gitlab prepared = prepare_metrics(metrics) pool.with do |connection| - prepared.each do |metric| + prepared.each_slice(settings[:packet_size]) do |slice| begin - connection.write_points([metric]) + connection.write_points(slice) rescue StandardError end end @@ -104,6 +105,25 @@ module Gitlab retval end + # Adds a tag to the current transaction (if any) + # + # name - The name of the tag to add. + # value - The value of the tag. + def self.tag_transaction(name, value) + trans = current_transaction + + trans.add_tag(name, value) if trans + end + + # Sets the action of the current transaction (if any) + # + # action - The name of the action. + def self.action=(action) + trans = current_transaction + + trans.action = action if trans + end + # When enabled this should be set before being used as the usual pattern # "@foo ||= bar" is _not_ thread-safe. if enabled? diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index face1921d2ea6c0a1394ed2b933979d5c78d1157..708ef79f3040c4051b05bcd591da37274074c457 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -11,6 +11,8 @@ module Gitlab module Instrumentation SERIES = 'method_calls' + PROXY_IVAR = :@__gitlab_instrumentation_proxy + def self.configure yield self end @@ -91,6 +93,18 @@ module Gitlab end end + # Returns true if a module is instrumented. + # + # mod - The module to check + def self.instrumented?(mod) + mod.instance_variable_defined?(PROXY_IVAR) + end + + # Returns the proxy module (if any) of `mod`. + def self.proxy_module(mod) + mod.instance_variable_get(PROXY_IVAR) + end + # Instruments a method. # # type - The type (:class or :instance) of method to instrument. @@ -99,9 +113,8 @@ module Gitlab def self.instrument(type, mod, name) return unless Metrics.enabled? - name = name.to_sym - alias_name = :"_original_#{name}" - target = type == :instance ? mod : mod.singleton_class + name = name.to_sym + target = type == :instance ? mod : mod.singleton_class if type == :instance target = mod @@ -113,6 +126,12 @@ module Gitlab method = mod.method(name) end + unless instrumented?(target) + target.instance_variable_set(PROXY_IVAR, Module.new) + end + + proxy_module = self.proxy_module(target) + # Some code out there (e.g. the "state_machine" Gem) checks the arity of # a method to make sure it only passes arguments when the method expects # any. If we were to always overwrite a method to take an `*args` @@ -125,17 +144,13 @@ module Gitlab args_signature = '*args, &block' end - send_signature = "__send__(#{alias_name.inspect}, #{args_signature})" - - target.class_eval <<-EOF, __FILE__, __LINE__ + 1 - alias_method #{alias_name.inspect}, #{name.inspect} - + proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1 def #{name}(#{args_signature}) trans = Gitlab::Metrics::Instrumentation.transaction if trans start = Time.now - retval = #{send_signature} + retval = super duration = (Time.now - start) * 1000.0 if duration >= Gitlab::Metrics.method_call_threshold @@ -148,10 +163,12 @@ module Gitlab retval else - #{send_signature} + super end end EOF + + target.prepend(proxy_module) end # Small layer of indirection to make it easier to stub out the current diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb index 8008b3bc8953631851f193f946fa6f1d62b39133..96cad941d5c208420313fbbceb8709b31afeee4e 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -9,6 +9,7 @@ module Gitlab return unless current_transaction current_transaction.increment(:sql_duration, event.duration) + current_transaction.increment(:sql_count, 1) end private diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb index 18523e0aefe460a25dd5818b77ccafe58daf651a..8bdc89a7751f3f77a8a001b51b57dcf2641e9dd1 100644 --- a/lib/gitlab/note_data_builder.rb +++ b/lib/gitlab/note_data_builder.rb @@ -59,8 +59,7 @@ module Gitlab repository: project.hook_attrs.slice(:name, :url, :description, :homepage) } - base_data[:object_attributes][:url] = - Gitlab::UrlBuilder.new(:note).build(note.id) + base_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(note) base_data end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 832fb08a5261b274600135f8b60038a66b153594..356e96fcbab1aa3ad5ade8d8093c19ed4f0c44e5 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -54,6 +54,12 @@ module Gitlab @user ||= build_new_user end + if external_provider? && @user + @user.external = true + elsif @user + @user.external = false + end + @user end @@ -113,6 +119,10 @@ module Gitlab end end + def external_provider? + Gitlab.config.omniauth.external_providers.include?(auth_hash.provider) + end + def block_after_signup? if creating_linked_ldap_user? ldap_config.block_auto_created_users diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 97d1edab9c13510ad7641196245e0a6d64c64098..67622f321a69e014f53924d6ea3b044ff952e012 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -36,11 +36,12 @@ module Gitlab commit.hook_attrs(with_changed_files: true) end - type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push" + type = Gitlab::Git.tag_ref?(ref) ? 'tag_push' : 'push' # Hash to be passed as post_receive_data data = { object_kind: type, + event_name: type, before: oldrev, after: newrev, ref: ref, diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 319447669dc3e2244fab38e164be808ffab6cd70..5c352c96de522a57f7fe819cd67eadab89f08d60 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -1,6 +1,8 @@ module Gitlab class Redis CACHE_NAMESPACE = 'cache:gitlab' + SESSION_NAMESPACE = 'session:gitlab' + SIDEKIQ_NAMESPACE = 'resque:gitlab' attr_reader :url diff --git a/lib/gitlab/repository_check_logger.rb b/lib/gitlab/repository_check_logger.rb new file mode 100644 index 0000000000000000000000000000000000000000..485b596ca572520c9799d6cd7fa3d16d3ee5ab68 --- /dev/null +++ b/lib/gitlab/repository_check_logger.rb @@ -0,0 +1,7 @@ +module Gitlab + class RepositoryCheckLogger < Gitlab::Logger + def self.file_name_noext + 'repocheck' + end + end +end diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb index 372327433252885eeaf3beeb542aaa6d4580fd60..ae85b294d31526c1304259980ae583755d275655 100644 --- a/lib/gitlab/sidekiq_middleware/memory_killer.rb +++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb @@ -29,8 +29,8 @@ module Gitlab "in #{GRACE_TIME} seconds" sleep(GRACE_TIME) - Sidekiq.logger.warn "sending SIGUSR1 to PID #{Process.pid}" - Process.kill('SIGUSR1', Process.pid) + Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid}" + Process.kill('SIGTERM', Process.pid) Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\ "#{SHUTDOWN_SIGNAL} to PID #{Process.pid}" diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index f301d42939d17570ca570f0a50aee6a808f8168f..2bbbd3074e86f7fbe3bc9160ea175468391c9f89 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -4,51 +4,65 @@ module Gitlab include GitlabRoutingHelper include ActionView::RecordIdentifier - def initialize(type) - @type = type - end + attr_reader :object - def build(id) - case @type - when :issue - build_issue_url(id) - when :merge_request - build_merge_request_url(id) - when :note - build_note_url(id) + def self.build(object) + new(object).url + end + def url + case object + when Commit + commit_url + when Issue + issue_url(object) + when MergeRequest + merge_request_url(object) + when Note + note_url + when WikiPage + wiki_page_url + else + raise NotImplementedError.new("No URL builder defined for #{object.class}") end end private - def build_issue_url(id) - issue = Issue.find(id) - issue_url(issue) + def initialize(object) + @object = object end - def build_merge_request_url(id) - merge_request = MergeRequest.find(id) - merge_request_url(merge_request) + def commit_url(opts = {}) + return '' if object.project.nil? + + namespace_project_commit_url({ + namespace_id: object.project.namespace, + project_id: object.project, + id: object.id + }.merge!(opts)) end - def build_note_url(id) - note = Note.find(id) - if note.for_commit? - namespace_project_commit_url(namespace_id: note.project.namespace, - id: note.commit_id, - project_id: note.project, - anchor: dom_id(note)) - elsif note.for_issue? - issue = Issue.find(note.noteable_id) - issue_url(issue, anchor: dom_id(note)) - elsif note.for_merge_request? - merge_request = MergeRequest.find(note.noteable_id) - merge_request_url(merge_request, anchor: dom_id(note)) - elsif note.for_snippet? - snippet = Snippet.find(note.noteable_id) - project_snippet_url(snippet, anchor: dom_id(note)) + def note_url + if object.for_commit? + commit_url(id: object.commit_id, anchor: dom_id(object)) + + elsif object.for_issue? + issue = Issue.find(object.noteable_id) + issue_url(issue, anchor: dom_id(object)) + + elsif object.for_merge_request? + merge_request = MergeRequest.find(object.noteable_id) + merge_request_url(merge_request, anchor: dom_id(object)) + + elsif object.for_snippet? + snippet = Snippet.find(object.noteable_id) + project_snippet_url(snippet, anchor: dom_id(object)) end end + + def wiki_page_url + "#{Gitlab.config.gitlab.url}#{object.wiki.wiki_base_path}/#{object.slug}" + end end end diff --git a/lib/support/nginx/gitlab_ci b/lib/support/nginx/gitlab_ci deleted file mode 100644 index bf05edfd78038115cbddf0de1462db5dbcad8e57..0000000000000000000000000000000000000000 --- a/lib/support/nginx/gitlab_ci +++ /dev/null @@ -1,29 +0,0 @@ -# GITLAB CI -server { - listen 80 default_server; # e.g., listen 192.168.1.1:80; - server_name YOUR_CI_SERVER_FQDN; # e.g., server_name source.example.com; - - access_log /var/log/nginx/gitlab_ci_access.log; - error_log /var/log/nginx/gitlab_ci_error.log; - - # expose API to fix runners - location /api { - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - proxy_set_header X-Real-IP $remote_addr; - - # You need to specify your DNS servers that are able to resolve YOUR_GITLAB_SERVER_FQDN - resolver 8.8.8.8 8.8.4.4; - proxy_pass $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri; - } - - # redirect all other CI requests - location / { - return 301 $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri; - } - - # adjust this to match the largest build log your runners might submit, - # set to 0 to disable limit - client_max_body_size 10m; -} \ No newline at end of file diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake index 7ec00a898fd3bff50aa54727a431a82ea4f29707..030ee8bafcb04da4fa39f7f4417b4afa50d42b29 100644 --- a/lib/tasks/gemojione.rake +++ b/lib/tasks/gemojione.rake @@ -5,12 +5,23 @@ namespace :gemojione do require 'json' dir = Gemojione.index.images_path + digests = [] + aliases = Hash.new { |hash, key| hash[key] = [] } + aliases_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json') - digests = AwardEmoji.emojis.map do |name, emoji_hash| + JSON.parse(File.read(aliases_path)).each do |alias_name, real_name| + aliases[real_name] << alias_name + end + + AwardEmoji.emojis.map do |name, emoji_hash| fpath = File.join(dir, "#{emoji_hash['unicode']}.png") digest = Digest::SHA256.file(fpath).hexdigest - { name: name, unicode: emoji_hash['unicode'], digest: digest } + digests << { name: name, unicode: emoji_hash['unicode'], digest: digest } + + aliases[name].each do |alias_name| + digests << { name: alias_name, unicode: emoji_hash['unicode'], digest: digest } + end end out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index 4cbccf2ca897c469aafe0c1f5989f6452922c9a8..48baecfd2a23614aeb192ab833088ce6a27f9fcb 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -14,7 +14,7 @@ namespace :gitlab do puts "" end - Rake::Task["db:setup"].invoke + Rake::Task["db:reset"].invoke Rake::Task["add_limits_mysql"].invoke Rake::Task["setup_postgresql"].invoke Rake::Task["db:seed_fu"].invoke diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 4a7ee7dbb64ddc642b3a71578f3b83bc093ac4f3..247383aa46c6423486d9f864b6908a24129b27e1 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -11,7 +11,7 @@ retry() { return 1 } -if [ -f /.dockerinit ]; then +if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then mkdir -p vendor # Install phantomjs package diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 410b993fdfbd2414e47359ddf34a5ebfe04dfdee..28cf804c1b2dfc11acd057eeb8561729e996720c 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -12,13 +12,13 @@ describe AutocompleteController do project.team << [user, :master] end - let(:body) { JSON.parse(response.body) } - describe 'GET #users with project ID' do before do get(:users, project_id: project.id) end + let(:body) { JSON.parse(response.body) } + it { expect(body).to be_kind_of(Array) } it { expect(body.size).to eq 1 } it { expect(body.map { |u| u["username"] }).to include(user.username) } @@ -143,4 +143,24 @@ describe AutocompleteController do it { expect(body.size).to eq 0 } end end + + context 'author of issuable included' do + before do + sign_in(user) + end + + let(:body) { JSON.parse(response.body) } + + it 'includes the author' do + get(:users, author_id: non_member.id) + + expect(body.first["username"]).to eq non_member.username + end + + it 'rejects non existent user ids' do + get(:users, author_id: 99999) + + expect(body.collect { |u| u['id'] }).not_to include(99999) + end + end end diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb index f09e4fcb15443728dd4037cc9496138d424f9c65..cf5c606c7236865a8e19aa5a52cd4ce84b1e7269 100644 --- a/spec/controllers/commit_controller_spec.rb +++ b/spec/controllers/commit_controller_spec.rb @@ -4,6 +4,8 @@ describe Projects::CommitController do let(:project) { create(:project) } let(:user) { create(:user) } let(:commit) { project.commit("master") } + let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } + let(:master_pickable_commit) { project.commit(master_pickable_sha) } before do sign_in(user) @@ -192,4 +194,53 @@ describe Projects::CommitController do end end end + + describe '#cherry_pick' do + context 'when target branch is not provided' do + it 'should render the 404 page' do + post(:cherry_pick, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: master_pickable_commit.id) + + expect(response).not_to be_success + expect(response.status).to eq(404) + end + end + + context 'when the cherry-pick was successful' do + it 'should redirect to the commits page' do + post(:cherry_pick, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: master_pickable_commit.id) + + expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') + expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.') + end + end + + context 'when the cherry_pick failed' do + before do + post(:cherry_pick, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: master_pickable_commit.id) + end + + it 'should redirect to the commit page' do + # Cherry-picking a commit that has been already cherry-picked. + post(:cherry_pick, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: master_pickable_commit.id) + + expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) + expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.') + end + end + end end diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a59865987151f57d85e0f12194a65c79ced1ab86 --- /dev/null +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Groups::GroupMembersController do + let(:user) { create(:user) } + let(:group) { create(:group) } + + context "index" do + before do + group.add_owner(user) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + it 'renders index with group members' do + get :index, group_id: group.path + + expect(response.status).to eq(200) + expect(response).to render_template(:index) + end + end +end diff --git a/spec/controllers/groups/notification_settings_controller_spec.rb b/spec/controllers/groups/notification_settings_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0786e45515a51c637970e54ab0c86233130ec215 --- /dev/null +++ b/spec/controllers/groups/notification_settings_controller_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Groups::NotificationSettingsController do + let(:group) { create(:group) } + let(:user) { create(:user) } + + describe '#update' do + context 'when not authorized' do + it 'redirects to sign in page' do + put :update, + group_id: group.to_param, + notification_setting: { level: :participating } + + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when authorized' do + before do + sign_in(user) + end + + it 'returns success' do + put :update, + group_id: group.to_param, + notification_setting: { level: :participating } + + expect(response.status).to eq 200 + end + end + end +end diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb index b6573f105dcffb1a4f1f04416d12b74bf46fed66..3a82083717ff07833ba8d954bde6cd24f1bcd76f 100644 --- a/spec/controllers/profiles/keys_controller_spec.rb +++ b/spec/controllers/profiles/keys_controller_spec.rb @@ -1,7 +1,17 @@ require 'spec_helper' describe Profiles::KeysController do - let(:user) { create(:user) } + let(:user) { create(:user) } + + describe '#new' do + before { sign_in(user) } + + it 'redirect to #index' do + get :new + + expect(response).to redirect_to(profile_keys_path) + end + end describe "#get_keys" do describe "non existant user" do diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..40bd83af8613f06ba6a006c5d49bd81be3e20944 --- /dev/null +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Projects::GroupLinksController do + let(:project) { create(:project, :private) } + let(:group) { create(:group, :private) } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + sign_in(user) + end + + describe '#create' do + shared_context 'link project to group' do + before do + post(:create, namespace_id: project.namespace.to_param, + project_id: project.to_param, + link_group_id: group.id, + link_group_access: ProjectGroupLink.default_access) + end + end + + context 'when user has access to group he want to link project to' do + before { group.add_developer(user) } + include_context 'link project to group' + + it 'links project with selected group' do + expect(group.shared_projects).to include project + end + + it 'redirects to project group links page'do + expect(response).to redirect_to( + namespace_project_group_links_path(project.namespace, project) + ) + end + end + + context 'when user doers not have access to group he want to link to' do + include_context 'link project to group' + + it 'renders 404' do + expect(response.status).to eq 404 + end + + it 'does not share project with that group' do + expect(group.shared_projects).to_not include project + end + end + end +end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 75e6b6f45a7fd10565b9e79bf9284ec822e39ab9..c54e83339a120540d87175171ff31166798c5504 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -157,6 +157,34 @@ describe Projects::MergeRequestsController do end end + describe 'PUT #update' do + context 'there is no source project' do + let(:project) { create(:project) } + let(:fork_project) { create(:forked_project_with_submodules) } + let(:merge_request) { create(:merge_request, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + + before do + fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + fork_project.save + merge_request.reload + fork_project.destroy + end + + it 'closes MR without errors' do + post :update, + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + merge_request: { + state_event: 'close' + } + + expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) + expect(merge_request.reload.closed?).to be_truthy + end + end + end + describe "DELETE #destroy" do it "denies access to users unless they're admin or project owner" do delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid diff --git a/spec/controllers/projects/notification_settings_controller_spec.rb b/spec/controllers/projects/notification_settings_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4908b5456487c445625a323df77953b46b7c51b8 --- /dev/null +++ b/spec/controllers/projects/notification_settings_controller_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Projects::NotificationSettingsController do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + project.team << [user, :developer] + end + + describe '#update' do + context 'when not authorized' do + it 'redirects to sign in page' do + put :update, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + notification_setting: { level: :participating } + + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when authorized' do + before do + sign_in(user) + end + + it 'returns success' do + put :update, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + notification_setting: { level: :participating } + + expect(response.status).to eq 200 + end + end + end +end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index d47e4ab9a4f18cca96549219ad34cd096ee6c418..ed64e7cf9af67b8c6388907b529e6ae38103a4a9 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -46,4 +46,20 @@ describe Projects::ProjectMembersController do end end end + + describe '#index' do + let(:project) { create(:project, :private) } + + context 'when user is member' do + let(:member) { create(:user) } + + before do + project.team << [member, :guest] + sign_in(member) + get :index, namespace_id: project.namespace.to_param, project_id: project.to_param + end + + it { expect(response.status).to eq(200) } + end + end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 7337ff58be121153a38d51f1cbc967742b46aa5a..8045c8b940d722d33d08fd91e9a986533cb8f1b5 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -33,7 +33,30 @@ describe UsersController do it 'renders the show template' do get :show, username: user.username - expect(response).to be_success + expect(response.status).to eq(200) + expect(response).to render_template('show') + end + end + end + + context 'when public visibility level is restricted' do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + context 'when logged out' do + it 'renders 404' do + get :show, username: user.username + expect(response.status).to eq(404) + end + end + + context 'when logged in' do + before { sign_in(user) } + + it 'renders show' do + get :show, username: user.username + expect(response.status).to eq(200) expect(response).to render_template('show') end end diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb new file mode 100644 index 0000000000000000000000000000000000000000..ac6eb0a7897440e504d5ca5367a60817b89c904b --- /dev/null +++ b/spec/factories/commits.rb @@ -0,0 +1,12 @@ +require_relative '../support/repo_helpers' + +FactoryGirl.define do + factory :commit do + git_commit RepoHelpers.sample_commit + project factory: :empty_project + + initialize_with do + new(git_commit, project) + end + end +end diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb new file mode 100644 index 0000000000000000000000000000000000000000..7700b15d538199fb70ba6b06fda67b40d22aff27 --- /dev/null +++ b/spec/factories/oauth_access_tokens.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: oauth_access_tokens +# +# id :integer not null, primary key +# resource_owner_id :integer +# application_id :integer +# token :string not null +# refresh_token :string +# expires_in :integer +# revoked_at :datetime +# created_at :datetime not null +# scopes :string +# + +FactoryGirl.define do + factory :oauth_access_token do + resource_owner + application + token '123456' + end +end diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb new file mode 100644 index 0000000000000000000000000000000000000000..d116a57383077095ae3af27dc3fd96c52fa30c1e --- /dev/null +++ b/spec/factories/oauth_applications.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do + name { FFaker::Name.name } + uid { FFaker::Name.name } + redirect_uri { FFaker::Internet.uri('http') } + owner + owner_type 'User' + end +end diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb new file mode 100644 index 0000000000000000000000000000000000000000..a3403fd76ae2556706998f4b3cd9f95943494c1d --- /dev/null +++ b/spec/factories/project_wikis.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :project_wiki do + project factory: :empty_project + user factory: :user + initialize_with { new(project, user) } + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index a5c60c51c5b12657c1de737b4cb52a25094748f2..a9b2148bd2ad477aa7dece6e41ca86ba417545fe 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,7 +1,7 @@ FactoryGirl.define do sequence(:name) { FFaker::Name.name } - factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator] do + factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator, :resource_owner] do email { FFaker::Internet.email } name sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" } diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb new file mode 100644 index 0000000000000000000000000000000000000000..938ccf2306b975331311d1292f5796d0bc693efd --- /dev/null +++ b/spec/factories/wiki_pages.rb @@ -0,0 +1,9 @@ +require 'ostruct' + +FactoryGirl.define do + factory :wiki_page do + page = OpenStruct.new(url_path: 'some-name') + association :wiki, factory: :project_wiki, strategy: :build + initialize_with { new(wiki, page, true) } + end +end diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..661fb7618091a73cf89bdd9d6e497e75f27cdeb1 --- /dev/null +++ b/spec/features/admin/admin_uses_repository_checks_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +feature 'Admin uses repository checks', feature: true do + before { login_as :admin } + + scenario 'to trigger a single check' do + project = create(:empty_project) + visit_admin_project_page(project) + + page.within('.repository-check') do + click_button 'Trigger repository check' + end + + expect(page).to have_content('Repository check was triggered') + end + + scenario 'to see a single failed repository check' do + project = create(:empty_project) + project.update_columns( + last_repository_check_failed: true, + last_repository_check_at: Time.now, + ) + visit_admin_project_page(project) + + page.within('.alert') do + expect(page.text).to match(/Last repository check \(.* ago\) failed/) + end + end + + scenario 'to clear all repository checks', js: true do + visit admin_application_settings_path + + expect(RepositoryCheck::ClearWorker).to receive(:perform_async) + + click_link 'Clear all repository checks' + + expect(page).to have_content('Started asynchronous removal of all repository check states.') + end + + def visit_admin_project_page(project) + visit admin_namespace_project_path(project.namespace, project) + end +end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 6da3a857b3f670091762fc340fb3d07654380108..090a941958f901c37c0a20590f5acae65e5027d3 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -86,6 +86,20 @@ describe "Builds" do end end end + + context 'Build raw trace' do + before do + @build.run! + @build.trace = 'BUILD TRACE' + visit namespace_project_build_path(@project.namespace, @project, @build) + end + + it do + page.within('.build-controls') do + expect(page).to have_link 'Raw' + end + end + end end describe "POST /:project/builds/:id/cancel" do @@ -120,4 +134,20 @@ describe "Builds" do it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } end + + describe "GET /:project/builds/:id/raw" do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build.run! + @build.trace = 'BUILD TRACE' + visit namespace_project_build_path(@project.namespace, @project, @build) + end + + it 'sends the right headers' do + page.within('.build-controls') { click_link 'Raw' } + + expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) + end + end end diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7f654684143934dffa6851e0af98ddfce2004315 --- /dev/null +++ b/spec/features/issues/filter_by_labels_spec.rb @@ -0,0 +1,167 @@ +require 'rails_helper' + +feature 'Issue filtering by Labels', feature: true do + include WaitForAjax + + let(:project) { create(:project, :public) } + let!(:user) { create(:user)} + let!(:label) { create(:label, project: project) } + + before do + bug = create(:label, project: project, title: 'bug') + feature = create(:label, project: project, title: 'feature') + enhancement = create(:label, project: project, title: 'enhancement') + + issue1 = create(:issue, title: "Bugfix1", project: project) + issue1.labels << bug + + issue2 = create(:issue, title: "Bugfix2", project: project) + issue2.labels << bug + issue2.labels << enhancement + + issue3 = create(:issue, title: "Feature1", project: project) + issue3.labels << feature + + project.team << [user, :master] + login_as(user) + + visit namespace_project_issues_path(project.namespace, project) + end + + context 'filter by label bug', js: true do + before do + page.find('.js-label-select').click + wait_for_ajax + execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + end + + it 'should show issue "Bugfix1" and "Bugfix2" in issues list' do + expect(page).to have_content "Bugfix1" + expect(page).to have_content "Bugfix2" + end + + it 'should not show "Feature1" in issues list' do + expect(page).not_to have_content "Feature1" + end + + it 'should show label "bug" in filtered-labels' do + expect(find('.filtered-labels')).to have_content "bug" + end + + it 'should not show label "feature" and "enhancement" in filtered-labels' do + expect(find('.filtered-labels')).not_to have_content "feature" + expect(find('.filtered-labels')).not_to have_content "enhancement" + end + end + + context 'filter by label feature', js: true do + before do + page.find('.js-label-select').click + wait_for_ajax + execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()") + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + end + + it 'should show issue "Feature1" in issues list' do + expect(page).to have_content "Feature1" + end + + it 'should not show "Bugfix1" and "Bugfix2" in issues list' do + expect(page).not_to have_content "Bugfix2" + expect(page).not_to have_content "Bugfix1" + end + + it 'should show label "feature" in filtered-labels' do + expect(find('.filtered-labels')).to have_content "feature" + end + + it 'should not show label "bug" and "enhancement" in filtered-labels' do + expect(find('.filtered-labels')).not_to have_content "bug" + expect(find('.filtered-labels')).not_to have_content "enhancement" + end + end + + context 'filter by label enhancement', js: true do + before do + page.find('.js-label-select').click + wait_for_ajax + execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()") + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + end + + it 'should show issue "Bugfix2" in issues list' do + expect(page).to have_content "Bugfix2" + end + + it 'should not show "Feature1" and "Bugfix1" in issues list' do + expect(page).not_to have_content "Feature1" + expect(page).not_to have_content "Bugfix1" + end + + it 'should show label "enhancement" in filtered-labels' do + expect(find('.filtered-labels')).to have_content "enhancement" + end + + it 'should not show label "feature" and "bug" in filtered-labels' do + expect(find('.filtered-labels')).not_to have_content "bug" + expect(find('.filtered-labels')).not_to have_content "feature" + end + end + + context 'filter by label enhancement or feature', js: true do + before do + page.find('.js-label-select').click + wait_for_ajax + execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()") + execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()") + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + end + + it 'should not show "Bugfix1" or "Feature1" in issues list' do + expect(page).not_to have_content "Bugfix1" + expect(page).not_to have_content "Feature1" + end + + it 'should show label "enhancement" and "feature" in filtered-labels' do + expect(find('.filtered-labels')).to have_content "enhancement" + expect(find('.filtered-labels')).to have_content "feature" + end + + it 'should not show label "bug" in filtered-labels' do + expect(find('.filtered-labels')).not_to have_content "bug" + end + end + + context 'filter by label enhancement and bug in issues list', js: true do + before do + page.find('.js-label-select').click + wait_for_ajax + execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()") + execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + end + + it 'should show issue "Bugfix2" in issues list' do + expect(page).to have_content "Bugfix2" + end + + it 'should not show "Feature1"' do + expect(page).not_to have_content "Feature1" + end + + it 'should show label "bug" and "enhancement" in filtered-labels' do + expect(find('.filtered-labels')).to have_content "bug" + expect(find('.filtered-labels')).to have_content "enhancement" + end + + it 'should not show label "feature" in filtered-labels' do + expect(find('.filtered-labels')).not_to have_content "feature" + end + end +end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 90822a8c123821e5e6e7a61d92c8ca531838a68e..192e3619375c8eb801f0b63e5d4d6e28f766b23b 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -76,6 +76,43 @@ describe 'Filter issues', feature: true do end end + describe 'Filter issues for label from issues#index', js: true do + before do + visit namespace_project_issues_path(project.namespace, project) + find('.js-label-select').click + end + + it 'should filter by any label' do + find('.dropdown-menu-labels a', text: 'Any Label').click + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + sleep 2 + + page.within '.labels-filter' do + expect(page).to have_content 'Any Label' + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Any Label') + end + + it 'should filter by no label' do + find('.dropdown-menu-labels a', text: 'No Label').click + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + sleep 2 + + page.within '.labels-filter' do + expect(page).to have_content 'No Label' + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label') + end + + it 'should filter by no label' do + find('.dropdown-menu-labels a', text: label.title).click + page.within '.labels-filter' do + expect(page).to have_content label.title + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + end + describe 'Filter issues for assignee and label from issues#index' do before do @@ -90,6 +127,7 @@ describe 'Filter issues', feature: true do find('.js-label-select').click find('.dropdown-menu-labels .dropdown-content a', text: label.title).click + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click sleep 2 end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 6fda0c3186674c06b1d660e3e5d0421e18a35e57..84c8e20ebaa6d03f870ea657d0ac88f4181a1bde 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -42,11 +42,9 @@ feature 'issue move to another project' do expect(current_url).to include project_path(new_project) - page.within('.issue') do - expect(page).to have_content("Text with #{cross_reference}!1") - expect(page).to have_content("Moved from #{cross_reference}#1") - expect(page).to have_content(issue.title) - end + expect(page).to have_content("Text with #{cross_reference}!1") + expect(page).to have_content("Moved from #{cross_reference}#1") + expect(page).to have_content(issue.title) end context 'projects user does not have permission to move issue to exist' do @@ -74,7 +72,7 @@ feature 'issue move to another project' do def edit_issue(issue) visit issue_path(issue) - page.within('.issuable-header') { click_link 'Edit' } + page.within('.issuable-actions') { first(:link, 'Edit').click } end def issue_path(issue) diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index 3eb903a93feb565982b30474ff5d450433cadcf0..b03dd0f666df56392a78b3492a602ab193bc8b30 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -48,7 +48,7 @@ feature 'Multiple issue updating from issues#index', feature: true do click_update_issues_button page.within('.issue .controls') do - expect(find('.author_link')["data-original-title"]).to have_content(user.name) + expect(find('.author_link')["title"]).to have_content(user.name) end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 79000666ccc2d92276f723b2edd32e72f8fd38ca..90476ab369be4dfbd4beca16dbb8f5ea5308987e 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -45,7 +45,7 @@ describe 'Issues', feature: true do project: project) end - it 'allows user to select unasigned', js: true do + it 'allows user to select unassigned', js: true do visit edit_namespace_project_issue_path(project.namespace, project, issue) expect(page).to have_content "Assignee #{@user.name}" @@ -64,6 +64,18 @@ describe 'Issues', feature: true do end end + describe 'Issue info' do + it 'excludes award_emoji from comment count' do + issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar') + create(:upvote_note, noteable: issue) + + visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) + + expect(page).to have_content 'foobar' + expect(page.all('.issue-no-comments').first.text).to eq "0" + end + end + describe 'Filter issue' do before do ['foobar', 'barbaz', 'gitlab'].each do |title| @@ -100,7 +112,7 @@ describe 'Issues', feature: true do end describe 'filter issue' do - titles = ['foo','bar','baz'] + titles = %w[foo bar baz] titles.each_with_index do |title, index| let!(title.to_sym) do create(:issue, title: title, @@ -141,8 +153,94 @@ describe 'Issues', feature: true do expect(first_issue).to include('baz') end + describe 'sorting by due date' do + before do + foo.update(due_date: 1.day.from_now) + bar.update(due_date: 6.days.from_now) + end + + it 'sorts by recently due date' do + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_soon) + + expect(first_issue).to include('foo') + end + + it 'sorts by least recently due date' do + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_later) + + expect(first_issue).to include('bar') + end + + it 'sorts by least recently due date by excluding nil due dates' do + bar.update(due_date: nil) + + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_later) + + expect(first_issue).to include('foo') + end + end + + describe 'filtering by due date' do + before do + foo.update(due_date: 1.day.from_now) + bar.update(due_date: 6.days.from_now) + end + + it 'filters by none' do + visit namespace_project_issues_path(project.namespace, project, due_date: Issue::NoDueDate.name) + + expect(page).not_to have_content('foo') + expect(page).not_to have_content('bar') + expect(page).to have_content('baz') + end + + it 'filters by any' do + visit namespace_project_issues_path(project.namespace, project, due_date: Issue::AnyDueDate.name) + + expect(page).to have_content('foo') + expect(page).to have_content('bar') + expect(page).to have_content('baz') + end + + it 'filters by due this week' do + foo.update(due_date: Date.today.beginning_of_week + 2.days) + bar.update(due_date: Date.today.end_of_week) + baz.update(due_date: Date.today - 8.days) + + visit namespace_project_issues_path(project.namespace, project, due_date: Issue::DueThisWeek.name) + + expect(page).to have_content('foo') + expect(page).to have_content('bar') + expect(page).not_to have_content('baz') + end + + it 'filters by due this month' do + foo.update(due_date: Date.today.beginning_of_month + 2.days) + bar.update(due_date: Date.today.end_of_month) + baz.update(due_date: Date.today - 50.days) + + visit namespace_project_issues_path(project.namespace, project, due_date: Issue::DueThisMonth.name) + + expect(page).to have_content('foo') + expect(page).to have_content('bar') + expect(page).not_to have_content('baz') + end + + it 'filters by overdue' do + foo.update(due_date: Date.today + 2.days) + bar.update(due_date: Date.today + 20.days) + baz.update(due_date: Date.yesterday) + + visit namespace_project_issues_path(project.namespace, project, due_date: Issue::Overdue.name) + + expect(page).not_to have_content('foo') + expect(page).not_to have_content('bar') + expect(page).to have_content('baz') + end + end + describe 'sorting by milestone' do - before :each do + before do foo.milestone = newer_due_milestone foo.save bar.milestone = later_due_milestone @@ -165,7 +263,7 @@ describe 'Issues', feature: true do describe 'combine filter and sort' do let(:user2) { create(:user) } - before :each do + before do foo.assignee = user2 foo.save bar.assignee = user2 @@ -187,7 +285,7 @@ describe 'Issues', feature: true do describe 'update assignee from issue#show' do let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } - context 'by autorized user' do + context 'by authorized user' do it 'allows user to select unassigned', js: true do visit namespace_project_issue_path(project.namespace, project, issue) @@ -212,7 +310,7 @@ describe 'Issues', feature: true do let(:guest) { create(:user) } - before :each do + before do project.team << [[guest], :guest] end @@ -255,7 +353,7 @@ describe 'Issues', feature: true do context 'by unauthorized user' do let(:guest) { create(:user) } - before :each do + before do project.team << [guest, :guest] issue.milestone = milestone issue.save @@ -273,13 +371,30 @@ describe 'Issues', feature: true do describe 'removing assignee' do let(:user2) { create(:user) } - before :each do + before do issue.assignee = user2 issue.save end end end + describe 'new issue' do + context 'dropzone upload file', js: true do + before do + visit new_namespace_project_issue_path(project.namespace, project) + end + + it 'should upload file when dragging into textarea' do + drop_in_dropzone test_image_file + + # Wait for the file to upload + sleep 1 + + expect(page.find_field("issue_description").value).to have_content 'banana_sample' + end + end + end + def first_issue page.all('ul.issues-list > li').first.text end @@ -287,4 +402,25 @@ describe 'Issues', feature: true do def last_issue page.all('ul.issues-list > li').last.text end + + def drop_in_dropzone(file_path) + # Generate a fake input selector + page.execute_script <<-JS + var fakeFileInput = window.$('<input/>').attr( + {id: 'fakeFileInput', type: 'file'} + ).appendTo('body'); + JS + # Attach the file to the fake input selector with Capybara + attach_file("fakeFileInput", file_path) + # Add the file to a fileList array and trigger the fake drop event + page.execute_script <<-JS + var fileList = [$('#fakeFileInput')[0].files[0]]; + var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); + $('.div-dropzone')[0].dropzone.listeners[0].events.drop(e); + JS + end + + def test_image_file + File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') + end end diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_requests/cherry_pick_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..82bc5226d07fc7f4130af93965945949e9138e61 --- /dev/null +++ b/spec/features/merge_requests/cherry_pick_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'Cherry-pick Merge Requests' do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user) } + + before do + login_as user + project.team << [user, :master] + end + + context "Viewing a merged merge request" do + before do + service = MergeRequests::MergeService.new(project, user) + + perform_enqueued_jobs do + service.execute(merge_request) + end + end + + # Fast-forward merge, or merged before GitLab 8.5. + context "Without a merge commit" do + before do + merge_request.merge_commit_sha = nil + merge_request.save + end + + it "doesn't show a Cherry-pick button" do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + + expect(page).not_to have_link "Cherry-pick" + end + end + + context "With a merge commit" do + it "shows a Cherry-pick button" do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + + expect(page).to have_link "Cherry-pick" + end + end + end +end diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9e007ab7635f90ede8dd52b60e531c60359ce0dd --- /dev/null +++ b/spec/features/merge_requests/edit_mr_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'Edit Merge Request', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } + + before do + project.team << [user, :master] + + login_as user + + visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + context 'editing a MR' do + it 'form should have class js-quick-submit' do + expect(page).to have_selector('.js-quick-submit') + end + end +end diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index c57ab5f3b0348ec1c4813e1e992ba61fdf88a6f3..e3ecd60a5f3e12ebedd772fc315d6d00f49d5ef0 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -2,8 +2,14 @@ require 'rails_helper' feature 'Merge Request filtering by Milestone', feature: true do let(:project) { create(:project, :public) } + let!(:user) { create(:user)} let(:milestone) { create(:milestone, project: project) } + before do + project.team << [user, :master] + login_as(user) + end + scenario 'filters by no Milestone', js: true do create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :simple, source_project: project, milestone: milestone) diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 5f855ccc701b532c29f4e25b7fa9da2b4a3f640e..389812ff7e1c06045ecef26d9e43ce3cdc22aabe 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -4,6 +4,20 @@ describe 'Comments', feature: true do include RepoHelpers include WaitForAjax + describe 'On merge requests page', feature: true do + it 'excludes award_emoji from comment count' do + merge_request = create(:merge_request) + project = merge_request.source_project + create(:upvote_note, noteable: merge_request, project: project) + + login_as :admin + visit namespace_project_merge_requests_path(project.namespace, project) + + expect(merge_request.mr_and_commit_notes.count).to eq 1 + expect(page.all('.merge-request-no-comments').first.text).to eq "0" + end + end + describe 'On a merge request', js: true, feature: true do let!(:merge_request) { create(:merge_request) } let!(:project) { merge_request.source_project } @@ -129,6 +143,17 @@ describe 'Comments', feature: true do end end end + + describe 'comment info' do + it 'excludes award_emoji from comment count' do + create(:upvote_note, noteable: merge_request, project: project) + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + + expect(merge_request.mr_and_commit_notes.count).to eq 2 + expect(find('.notes-tab span.badge').text).to eq "1" + end + end end describe 'On a merge request diff', js: true, feature: true do diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1adab7e9c6c3ef66f70eed0ecf83d0f081b8359b --- /dev/null +++ b/spec/features/participants_autocomplete_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +feature 'Member autocomplete', feature: true do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + let(:participant) { create(:user) } + let(:author) { create(:user) } + + before do + allow_any_instance_of(Commit).to receive(:author).and_return(author) + login_as user + end + + shared_examples "open suggestions" do + it 'suggestions are displayed' do + expect(page).to have_selector('.atwho-view', visible: true) + end + + it 'author is suggested' do + page.within('.atwho-view', visible: true) do + expect(page).to have_content(author.username) + end + end + + it 'participant is suggested' do + page.within('.atwho-view', visible: true) do + expect(page).to have_content(participant.username) + end + end + end + + context 'adding a new note on a Issue', js: true do + before do + issue = create(:issue, author: author, project: project) + create(:note, note: 'Ultralight Beam', noteable: issue, author: participant) + visit_issue(project, issue) + end + + context 'when typing @' do + include_examples "open suggestions" + before do + open_member_suggestions + end + end + end + + context 'adding a new note on a Merge Request ', js: true do + before do + merge = create(:merge_request, source_project: project, target_project: project, author: author) + create(:note, note: 'Ultralight Beam', noteable: merge, author: participant) + visit_merge_request(project, merge) + end + + context 'when typing @' do + include_examples "open suggestions" + before do + open_member_suggestions + end + end + end + + context 'adding a new note on a Commit ', js: true do + let(:commit) { project.commit } + + before do + allow(commit).to receive(:author).and_return(author) + create(:note_on_commit, author: participant, project: project, commit_id: project.repository.commit.id, note: 'No More Parties in LA') + visit_commit(project, commit) + end + + context 'when typing @' do + include_examples "open suggestions" + before do + open_member_suggestions + end + end + end + + def open_member_suggestions + sleep 1 + page.within('.new-note') do + sleep 1 + find('#note_note').native.send_keys('@') + end + end + + def visit_issue(project, issue) + visit namespace_project_issue_path(project.namespace, project, issue) + end + + def visit_merge_request(project, merge) + visit namespace_project_merge_request_path(project.namespace, project, merge) + end + + def visit_commit(project, commit) + visit namespace_project_commit_path(project.namespace, project, commit) + end +end diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1a5a9059dbd6067ec4d09e5d3be81241013c8aa4 --- /dev/null +++ b/spec/features/profiles/oauth_applications_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'Profile > Applications', feature: true do + let(:user) { create(:user) } + + before do + login_as(user) + end + + describe 'User manages applications', js: true do + it 'deletes an application' do + create(:oauth_application, owner: user) + visit oauth_applications_path + + page.within('.oauth-applications') do + expect(page).to have_content('Your applications (1)') + click_button 'Destroy' + end + + expect(page).to have_content('The application was deleted successfully') + expect(page).to have_content('Your applications (0)') + expect(page).to have_content('Authorized applications (0)') + end + + it 'deletes an authorized application' do + create(:oauth_access_token, resource_owner: user) + visit oauth_applications_path + + page.within('.oauth-authorized-applications') do + expect(page).to have_content('Authorized applications (1)') + click_button 'Revoke' + end + + expect(page).to have_content('The application was revoked access.') + expect(page).to have_content('Your applications (0)') + expect(page).to have_content('Authorized applications (0)') + end + end +end diff --git a/spec/features/project/commits/cherry_pick_spec.rb b/spec/features/project/commits/cherry_pick_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0559b02f3218f6130c061fb4b756b29636a0d844 --- /dev/null +++ b/spec/features/project/commits/cherry_pick_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe 'Cherry-pick Commits' do + let(:project) { create(:project) } + let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } + let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') } + + + before do + login_as :user + project.team << [@user, :master] + visit namespace_project_commits_path(project.namespace, project, project.repository.root_ref, { limit: 5 }) + end + + context "I cherry-pick a commit" do + it do + visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) + find("a[href='#modal-cherry-pick-commit']").click + page.within('#modal-cherry-pick-commit') do + uncheck 'create_merge_request' + click_button 'Cherry-pick' + end + expect(page).to have_content('The commit has been successfully cherry-picked.') + end + end + + context "I cherry-pick a merge commit" do + it do + visit namespace_project_commit_path(project.namespace, project, master_pickable_merge.id) + find("a[href='#modal-cherry-pick-commit']").click + page.within('#modal-cherry-pick-commit') do + uncheck 'create_merge_request' + click_button 'Cherry-pick' + end + expect(page).to have_content('The commit has been successfully cherry-picked.') + end + end + + context "I cherry-pick a commit that was previously cherry-picked" do + it do + visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) + find("a[href='#modal-cherry-pick-commit']").click + page.within('#modal-cherry-pick-commit') do + uncheck 'create_merge_request' + click_button 'Cherry-pick' + end + visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) + find("a[href='#modal-cherry-pick-commit']").click + page.within('#modal-cherry-pick-commit') do + uncheck 'create_merge_request' + click_button 'Cherry-pick' + end + expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.') + end + end + + context "I cherry-pick a commit in a new merge request" do + it do + visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) + find("a[href='#modal-cherry-pick-commit']").click + page.within('#modal-cherry-pick-commit') do + click_button 'Cherry-pick' + end + expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.') + end + end +end diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3d6ffbc4c6bc97be8eeea498b22933349284d9ab --- /dev/null +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +feature 'project owner creates a license file', feature: true, js: true do + include Select2Helper + + let(:project_master) { create(:user) } + let(:project) { create(:project) } + background do + project.repository.remove_file(project_master, 'LICENSE', 'Remove LICENSE', 'master') + project.team << [project_master, :master] + login_as(project_master) + visit namespace_project_path(project.namespace, project) + end + + scenario 'project master creates a license file manually from a template' do + visit namespace_project_tree_path(project.namespace, project, project.repository.root_ref) + find('.add-to-tree').click + click_link 'New file' + + fill_in :file_name, with: 'LICENSE' + + expect(page).to have_selector('.license-selector') + + select2('mit', from: '#license_type') + + file_content = find('.file-content') + expect(file_content).to have_content('The MIT License (MIT)') + expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + + fill_in :commit_message, with: 'Add a LICENSE file', visible: true + click_button 'Commit Changes' + + expect(current_path).to eq( + namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) + expect(page).to have_content('The MIT License (MIT)') + expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + end + + scenario 'project master creates a license file from the "Add license" link' do + click_link 'Add License' + + expect(current_path).to eq( + namespace_project_new_blob_path(project.namespace, project, 'master')) + expect(find('#file_name').value).to eq('LICENSE') + expect(page).to have_selector('.license-selector') + + select2('mit', from: '#license_type') + + file_content = find('.file-content') + expect(file_content).to have_content('The MIT License (MIT)') + expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + + fill_in :commit_message, with: 'Add a LICENSE file', visible: true + click_button 'Commit Changes' + + expect(current_path).to eq( + namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) + expect(page).to have_content('The MIT License (MIT)') + expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + end +end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3268e240200e6a983b3c1c22fc47144388760ffa --- /dev/null +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +feature 'project owner sees a link to create a license file in empty project', feature: true, js: true do + include Select2Helper + + let(:project_master) { create(:user) } + let(:project) { create(:empty_project) } + background do + project.team << [project_master, :master] + login_as(project_master) + end + + scenario 'project master creates a license file from a template' do + visit namespace_project_path(project.namespace, project) + click_link 'Create empty bare repository' + click_on 'LICENSE' + + expect(current_path).to eq( + namespace_project_new_blob_path(project.namespace, project, 'master')) + expect(find('#file_name').value).to eq('LICENSE') + expect(page).to have_selector('.license-selector') + + select2('mit', from: '#license_type') + + file_content = find('.file-content') + expect(file_content).to have_content('The MIT License (MIT)') + expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + + fill_in :commit_message, with: 'Add a LICENSE file', visible: true + # Remove pre-receive hook so we can push without auth + FileUtils.rm_f(File.join(project.repository.path, 'hooks', 'pre-receive')) + click_button 'Commit Changes' + + expect(current_path).to eq( + namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) + expect(page).to have_content('The MIT License (MIT)') + expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + end +end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index ed97b6cb577e95c33ac3f25c434507dc8180bf27..782c0bfe6661222701dba8e0583820c1e756db87 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -100,8 +100,7 @@ feature 'Project', feature: true do it 'click toggle and show dropdown', js: true do find('.js-projects-dropdown-toggle').click - wait_for_ajax - expect(page).to have_css('.select2-results li', count: 1) + expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 1) end end diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index e8886e7edf922dbcd51da3ced6dabefee5481e27..8edeb8d18af91e7975c457cbb2a0a10dde290249 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -80,6 +80,22 @@ describe "Runners" do end end + describe "shared runners description" do + let(:shared_runners_text) { 'custom **shared** runners description' } + let(:shared_runners_html) { 'custom shared runners description' } + + before do + stub_application_setting(shared_runners_text: shared_runners_text) + project = FactoryGirl.create :empty_project, shared_runners_enabled: false + project.team << [user, :master] + visit runners_path(project) + end + + it "sees shared runners description" do + expect(page.find(".shared-runners-description")).to have_content(shared_runners_html) + end + end + describe "show page" do before do @project = FactoryGirl.create :empty_project diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 3e6289a46b1248d1ed83b1a15c583dea58920704..029a11ea43ced6cdea8df610c2ef3d388f8c0424 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -10,6 +10,10 @@ describe "Search", feature: true do visit search_path end + it 'top right search form is not present' do + expect(page).not_to have_selector('.search') + end + describe 'searching for Projects' do it 'finds a project' do page.within '.search-holder' do diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 79d5bf4cf06cc892927234ae43c395978daae4be..8625ea6bc10a79ba7ff83499de242b6e253076df 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -101,12 +101,12 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for owner } it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_denied_for :external } end describe "GET /:project_path/blob" do diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index 0a89193eb67d554c3f3beaa7bbaf47211c5a785d..544270b40376ed3a0f13eb02472a0bfd8bd85ae0 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -101,9 +101,9 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for owner } it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } it { is_expected.to be_denied_for :user } it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 40daac89d40f22be676864edd3d5e9079d7b6438..4def4f99bc0e8bca01c024d1cc75fcce3a463433 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -101,12 +101,12 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for owner } it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for :external } end describe "GET /:project_path/builds" do diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..01472743b2aaa3593bba79b75d50e595d827dd9c --- /dev/null +++ b/spec/features/signup_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +feature 'Signup', feature: true do + describe 'signup with no errors' do + it 'creates the user account and sends a confirmation email' do + user = build(:user) + + visit root_path + + fill_in 'user_name', with: user.name + fill_in 'user_username', with: user.username + fill_in 'user_email', with: user.email + fill_in 'user_password_sign_up', with: user.password + click_button "Sign up" + + expect(current_path).to eq user_session_path + expect(page).to have_content("A message with a confirmation link has been sent to your email address.") + end + end + + describe 'signup with errors' do + it "displays the errors" do + existing_user = create(:user) + user = build(:user) + + visit root_path + + fill_in 'user_name', with: user.name + fill_in 'user_username', with: user.username + fill_in 'user_email', with: existing_user.email + fill_in 'user_password_sign_up', with: user.password + click_button "Sign up" + + expect(current_path).to eq user_registration_path + expect(page).to have_content("error prohibited this user from being saved") + expect(page).to have_content("Email has already been taken") + end + + it 'does not redisplay the password' do + existing_user = create(:user) + user = build(:user) + + visit root_path + + fill_in 'user_name', with: user.name + fill_in 'user_username', with: user.username + fill_in 'user_email', with: existing_user.email + fill_in 'user_password_sign_up', with: user.password + click_button "Sign up" + + expect(current_path).to eq user_registration_path + expect(page.body).not_to match(/#{user.password}/) + end + end +end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..113d4c40cfca493d28d5c97bd7a2e76fc3ec26e9 --- /dev/null +++ b/spec/features/todos/todos_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe 'Dashboard Todos', feature: true do + let(:user){ create(:user) } + let(:author){ create(:user) } + let(:project){ create(:project) } + let(:issue){ create(:issue) } + let(:todos_per_page){ Todo.default_per_page } + let(:todos_total){ todos_per_page + 1 } + + describe 'GET /dashboard/todos' do + context 'User does not have todos' do + before do + login_as(user) + visit dashboard_todos_path + end + it 'shows "All done" message' do + expect(page).to have_content "You're all done!" + end + end + + context 'User has a todo', js: true do + before do + create(:todo, :mentioned, user: user, project: project, target: issue, author: author) + login_as(user) + visit dashboard_todos_path + end + + it 'todo is present' do + expect(page).to have_selector('.todos-list .todo', count: 1) + end + + describe 'deleting the todo' do + before do + first('.done-todo').click + end + + it 'is removed from the list' do + expect(page).not_to have_selector('.todos-list .todo') + end + + it 'shows "All done" message' do + expect(page).to have_content("You're all done!") + end + end + end + + context 'User has multiple pages of Todos' do + let(:todo_total_pages){ (todos_total.to_f/todos_per_page).ceil } + + before do + todos_total.times do + create(:todo, :mentioned, user: user, project: project, target: issue, author: author) + end + + login_as(user) + visit dashboard_todos_path + end + + it 'is paginated' do + expect(page).to have_selector('.gl-pagination') + end + + it 'is has the right number of pages' do + expect(page).to have_selector('.gl-pagination .page', count: todo_total_pages) + end + + describe 'deleting last todo from last page', js: true do + it 'redirects to the previous page' do + page.within('.gl-pagination') do + click_link todo_total_pages.to_s + end + first('.done-todo').click + expect(page).to have_content(Todo.last.body) + end + end + end + end +end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index b16480554622bf064284ce14688d2083a72332c3..bc607a29751d85639ab9c25f981ea8ccd672b465 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -62,6 +62,22 @@ describe IssuesFinder do expect(issues).to eq([issue2]) end + it 'returns unique issues when filtering by multiple labels' do + label2 = create(:label, project: project2) + + create(:label_link, label: label2, target: issue2) + + params = { + scope: 'all', + label_name: [label.title, label2.title].join(','), + state: 'opened' + } + + issues = IssuesFinder.new(user, params).execute + + expect(issues).to eq([issue2]) + end + it 'should filter by no label name' do params = { scope: "all", label_name: Label::None.title, state: 'opened' } issues = IssuesFinder.new(user, params).execute diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb index 4f8d9c672620ce6c64b1c092b7944ca896c1e177..f942695b6f0f5b24104de6baa68928448563a8f9 100644 --- a/spec/helpers/ci_status_helper_spec.rb +++ b/spec/helpers/ci_status_helper_spec.rb @@ -6,8 +6,8 @@ describe CiStatusHelper do let(:success_commit) { double("Ci::Commit", status: 'success') } let(:failed_commit) { double("Ci::Commit", status: 'failed') } - describe 'ci_status_icon' do - it { expect(helper.ci_status_icon(success_commit)).to include('fa-check') } - it { expect(helper.ci_status_icon(failed_commit)).to include('fa-close') } + describe 'ci_icon_for_status' do + it { expect(helper.ci_icon_for_status(success_commit.status)).to include('fa-check') } + it { expect(helper.ci_icon_for_status(failed_commit.status)).to include('fa-close') } end end diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..727c25ff529da6723b50e8c0a0d920612997a0e9 --- /dev/null +++ b/spec/helpers/commits_helper_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +describe CommitsHelper do + describe 'commit_author_link' do + it 'escapes the author email' do + commit = double( + author: nil, + author_name: 'Persistent XSS', + author_email: 'my@email.com" onmouseover="alert(1)' + ) + + expect(helper.commit_author_link(commit)). + not_to include('onmouseover="alert(1)"') + end + end + + describe 'commit_committer_link' do + it 'escapes the committer email' do + commit = double( + committer: nil, + committer_name: 'Persistent XSS', + committer_email: 'my@email.com" onmouseover="alert(1)' + ) + + expect(helper.commit_committer_link(commit)). + not_to include('onmouseover="alert(1)"') + end + end +end diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3391234e9f5c5f0c362d2a9fa0fcdedad0fb7954 --- /dev/null +++ b/spec/helpers/import_helper_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +describe ImportHelper do + describe '#github_project_link' do + context 'when provider does not specify a custom URL' do + it 'uses default GitHub URL' do + allow(Gitlab.config.omniauth).to receive(:providers). + and_return([Settingslogic.new('name' => 'github')]) + + expect(helper.github_project_link('octocat/Hello-World')). + to include('href="https://github.com/octocat/Hello-World"') + end + end + + context 'when provider specify a custom URL' do + it 'uses custom URL' do + allow(Gitlab.config.omniauth).to receive(:providers). + and_return([Settingslogic.new('name' => 'github', 'url' => 'https://github.company.com')]) + + expect(helper.github_project_link('octocat/Hello-World')). + to include('href="https://github.company.com/octocat/Hello-World"') + end + end + end +end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index f1aba4cfdf3aa3cb7183aaf636f1ac063feb0458..9d5f009ebe1a802b1386282b677af83ccee7fc75 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -2,34 +2,15 @@ require 'spec_helper' describe NotificationsHelper do describe 'notification_icon' do - let(:notification) { double(disabled?: false, participating?: false, watch?: false) } - - context "disabled notification" do - before { allow(notification).to receive(:disabled?).and_return(true) } - - it "has a red icon" do - expect(notification_icon(notification)).to match('class="fa fa-volume-off ns-mute"') - end - end - - context "participating notification" do - before { allow(notification).to receive(:participating?).and_return(true) } - - it "has a blue icon" do - expect(notification_icon(notification)).to match('class="fa fa-volume-down ns-part"') - end - end - - context "watched notification" do - before { allow(notification).to receive(:watch?).and_return(true) } - - it "has a green icon" do - expect(notification_icon(notification)).to match('class="fa fa-volume-up ns-watch"') - end - end + it { expect(notification_icon(:disabled)).to match('class="fa fa-microphone-slash fa-fw"') } + it { expect(notification_icon(:participating)).to match('class="fa fa-volume-up fa-fw"') } + it { expect(notification_icon(:mention)).to match('class="fa fa-at fa-fw"') } + it { expect(notification_icon(:global)).to match('class="fa fa-globe fa-fw"') } + it { expect(notification_icon(:watch)).to match('class="fa fa-eye fa-fw"') } + end - it "has a blue icon" do - expect(notification_icon(notification)).to match('class="fa fa-circle-o ns-default"') - end + describe 'notification_title' do + it { expect(notification_title(:watch)).to match('Watch') } + it { expect(notification_title(:mention)).to match('On mention') } end end diff --git a/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml index e5850b62659acfb7126d1b519ee90118648f1cb7..4547feeb212ffe3aa9c7ff8b8ad0c6d64f03cc53 100644 --- a/spec/javascripts/fixtures/project_title.html.haml +++ b/spec/javascripts/fixtures/project_title.html.haml @@ -1,7 +1,20 @@ -%h1.title - %a - GitLab Org - %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} - GitLab Test - %input#project_path.project-item-select.js-projects-dropdown.ajax-project-select{type: "hidden", name: "project_path", "data-include-groups" => "false"} - %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle +.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" } + .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/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee index 050b6e362c65d19efec6d698570d9b267e8e1586..dd160e821b3d6e54610ac184e87f7d701e6350ec 100644 --- a/spec/javascripts/notes_spec.js.coffee +++ b/spec/javascripts/notes_spec.js.coffee @@ -1,4 +1,5 @@ #= require notes +#= require gl_form window.gon = {} window.disableButtonIfEmptyField = -> null diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee index 47c7b7febe35bf55398160881613539b36d8ce57..3d8de2ff989bde00db9d7964192a35f0188289be 100644 --- a/spec/javascripts/project_title_spec.js.coffee +++ b/spec/javascripts/project_title_spec.js.coffee @@ -1,4 +1,6 @@ +#= require bootstrap #= require select2 +#= require gl_dropdown #= require api #= require project_select #= require project @@ -14,9 +16,6 @@ describe 'Project Title', -> fixture.load('project_title.html') @project = new Project() - spyOn(@project, 'changeProject').and.callFake (url) -> - window.current_project_url = url - describe 'project list', -> beforeEach => @projects_data = fixture.load('projects.json')[0] @@ -29,18 +28,9 @@ describe 'Project Title', -> it 'to show on toggle click', => $('.js-projects-dropdown-toggle').click() - - expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(true) - expect($('.ajax-project-dropdown li').length).toBe(@projects_data.length) + expect($('.header-content').hasClass('open')).toBe(true) it 'hide dropdown', -> - $("#select2-drop-mask").click() - - expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(false) - - it 'change project when clicking item', -> - $('.js-projects-dropdown-toggle').click() - $('.ajax-project-dropdown li:nth-child(2)').trigger('mouseup') + $(".dropdown-menu-close-icon").click() - expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(false) - expect(window.current_project_url).toBe('http://localhost:3000/h5bp/html5-boilerplate') + expect($('.header-content').hasClass('open')).toBe(false) diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/award_emoji_spec.rb index 330678f7f164c5a589b46d7c114c6d138ae77afe..88c22912950610965ab286c8720f7db286b8c009 100644 --- a/spec/lib/award_emoji_spec.rb +++ b/spec/lib/award_emoji_spec.rb @@ -16,4 +16,11 @@ describe AwardEmoji do end end end + + describe '.emoji_by_category' do + it "only contains known categories" do + undefined_categories = AwardEmoji.emoji_by_category.keys - AwardEmoji::CATEGORIES.keys + expect(undefined_categories).to be_empty + end + end end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 94468abcbb3c197265bff809a4d52732b0d468d4..b0a38e7c2510b8a01808c9040485525934adacd7 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -178,27 +178,37 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end describe 'cross project label references' do - let(:another_project) { create(:empty_project, :public) } - let(:project_name) { another_project.name_with_namespace } - let(:label) { create(:label, project: another_project, color: '#00ff00') } - let(:reference) { label.to_reference(project) } + context 'valid project referenced' do + let(:another_project) { create(:empty_project, :public) } + let(:project_name) { another_project.name_with_namespace } + let(:label) { create(:label, project: another_project, color: '#00ff00') } + let(:reference) { label.to_reference(project) } - let!(:result) { reference_filter("See #{reference}") } + let!(:result) { reference_filter("See #{reference}") } - it 'points to referenced project issues page' do - expect(result.css('a').first.attr('href')) - .to eq urls.namespace_project_issues_url(another_project.namespace, - another_project, - label_name: label.name) - end + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(another_project.namespace, + another_project, + label_name: label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')) + .to match /background-color: #00ff00/ + end - it 'has valid color' do - expect(result.css('a span').first.attr('style')) - .to match /background-color: #00ff00/ + it 'contains cross project content' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}" + end end - it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}" + context 'project that does not exist referenced' do + let(:result) { reference_filter('aaa/bbb~ccc') } + + it 'does not link reference' do + expect(result.to_html).to eq 'aaa/bbb~ccc' + end end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index dcb8a3451bd10d068e805de09192ac185c3e4c42..643acf0303c560504b43e5ca3c46821aaec3193c 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -286,6 +286,81 @@ module Ci end end + + describe "Scripts handling" do + let(:config_data) { YAML.dump(config) } + let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) } + + subject { config_processor.builds_for_stage_and_ref("test", "master").first } + + describe "before_script" do + context "in global context" do + let(:config) do + { + before_script: ["global script"], + test: { script: ["script"] } + } + end + + it "return commands with scripts concencaced" do + expect(subject[:commands]).to eq("global script\nscript") + end + end + + context "overwritten in local context" do + let(:config) do + { + before_script: ["global script"], + test: { before_script: ["local script"], script: ["script"] } + } + end + + it "return commands with scripts concencaced" do + expect(subject[:commands]).to eq("local script\nscript") + end + end + end + + describe "script" do + let(:config) do + { + test: { script: ["script"] } + } + end + + it "return commands with scripts concencaced" do + expect(subject[:commands]).to eq("script") + end + end + + describe "after_script" do + context "in global context" do + let(:config) do + { + after_script: ["after_script"], + test: { script: ["script"] } + } + end + + it "return after_script in options" do + expect(subject[:options][:after_script]).to eq(["after_script"]) + end + end + + context "overwritten in local context" do + let(:config) do + { + after_script: ["local after_script"], + test: { after_script: ["local after_script"], script: ["script"] } + } + end + + it "return after_script in options" do + expect(subject[:options][:after_script]).to eq(["local after_script"]) + end + end + end + end describe "Image and service handling" do it "returns image and service when defined" do @@ -345,20 +420,76 @@ module Ci end end - describe "Variables" do - it "returns variables when defined" do - variables = { - var1: "value1", - var2: "value2", - } - config = YAML.dump({ - variables: variables, - before_script: ["pwd"], - rspec: { script: "rspec" } - }) + describe 'Variables' do + context 'when global variables are defined' do + it 'returns global variables' do + variables = { + VAR1: 'value1', + VAR2: 'value2', + } - config_processor = GitlabCiYamlProcessor.new(config, path) - expect(config_processor.variables).to eq(variables) + config = YAML.dump({ + variables: variables, + before_script: ['pwd'], + rspec: { script: 'rspec' } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.global_variables).to eq(variables) + end + end + + context 'when job variables are defined' do + context 'when syntax is correct' do + it 'returns job variables' do + variables = { + KEY1: 'value1', + SOME_KEY_2: 'value2' + } + + config = YAML.dump( + { before_script: ['pwd'], + rspec: { + variables: variables, + script: 'rspec' } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.job_variables(:rspec)).to eq variables + end + end + + context 'when syntax is incorrect' do + it 'raises error' do + variables = [:KEY1, 'value1', :KEY2, 'value2'] + + config = YAML.dump( + { before_script: ['pwd'], + rspec: { + variables: variables, + script: 'rspec' } + }) + + expect { GitlabCiYamlProcessor.new(config, path) } + .to raise_error(GitlabCiYamlProcessor::ValidationError, + /job: variables should be a map/) + end + end + end + + context 'when job variables are not defined' do + it 'returns empty array' do + config = YAML.dump({ + before_script: ['pwd'], + rspec: { script: 'rspec' } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.job_variables(:rspec)).to eq [] + end end end @@ -536,7 +667,7 @@ module Ci stage_idx: 1, name: :normal_job, only: nil, - commands: "\ntest", + commands: "test", tag_list: [], options: {}, when: "on_success", @@ -563,7 +694,7 @@ EOT stage_idx: 1, name: :job1, only: nil, - commands: "\nexecute-script-for-job", + commands: "execute-script-for-job", tag_list: [], options: {}, when: "on_success", @@ -575,7 +706,7 @@ EOT stage_idx: 1, name: :job2, only: nil, - commands: "\nexecute-script-for-job", + commands: "execute-script-for-job", tag_list: [], options: {}, when: "on_success", @@ -607,6 +738,27 @@ EOT end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script should be an array of strings") end + it "returns errors if job before_script parameter is not an array of strings" do + config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) + expect do + GitlabCiYamlProcessor.new(config, path) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: before_script should be an array of strings") + end + + it "returns errors if after_script parameter is invalid" do + config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config, path) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script should be an array of strings") + end + + it "returns errors if job after_script parameter is not an array of strings" do + config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) + expect do + GitlabCiYamlProcessor.new(config, path) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: after_script should be an array of strings") + end + it "returns errors if image parameter is invalid" do config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) expect do @@ -730,14 +882,14 @@ EOT config = YAML.dump({ variables: "test", rspec: { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-value strings") end - it "returns errors if variables is not a map of key-valued strings" do + it "returns errors if variables is not a map of key-value strings" do config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-value strings") end it "returns errors if job when is not on_success, on_failure or always" do diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index 329792bb68542657809239fc031d53f7812172c7..b6f7a2e7ec4b44c0711e3c898d520a06a69e32af 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -42,7 +42,7 @@ describe Gitlab::Badge::Build do end context 'build exists' do - let(:ci_commit) { create(:ci_commit, project: project, sha: sha) } + let(:ci_commit) { create(:ci_commit, project: project, sha: sha, ref: branch) } let!(:build) { create(:ci_build, commit: ci_commit) } @@ -57,7 +57,7 @@ describe Gitlab::Badge::Build do describe '#data' do let(:data) { badge.data } - it 'contains infromation about success' do + it 'contains information about success' do expect(status_node(data, 'success')).to be_truthy end end @@ -74,7 +74,7 @@ describe Gitlab::Badge::Build do describe '#data' do let(:data) { badge.data } - it 'contains infromation about failure' do + it 'contains information about failure' do expect(status_node(data, 'failed')).to be_truthy end end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index c413132abe56a3dc4246b2cf728d043ec75c0f58..1a833f255a56a7ee20b29d7fbf20544c878000ac 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -34,9 +34,9 @@ describe Gitlab::BitbucketImport::Importer, lib: true do let(:project_identifier) { 'namespace/repo' } let(:data) do { - bb_session: { - bitbucket_access_token: "123456", - bitbucket_access_token_secret: "secret" + 'bb_session' => { + 'bitbucket_access_token' => "123456", + 'bitbucket_access_token_secret' => "secret" } } end @@ -44,7 +44,7 @@ describe Gitlab::BitbucketImport::Importer, lib: true do create( :project, import_source: project_identifier, - import_data: ProjectImportData.new(data: data) + import_data: ProjectImportData.new(credentials: data) ) end let(:importer) { Gitlab::BitbucketImport::Importer.new(project) } diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb index fd05428b32223856dbd74f88c031ccfba81e5f87..0e7ffbe9b8eb3dfcd578729e3df2f7a612197c32 100644 --- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -2,13 +2,14 @@ require 'spec_helper' describe Gitlab::GithubImport::IssueFormatter, lib: true do let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) } - let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } + let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:base_data) do { number: 1347, + milestone: nil, state: 'open', title: 'Found a bug', body: "I'm having a problem with this.", @@ -26,11 +27,13 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do describe '#attributes' do context 'when issue is open' do - let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) } + let(:raw_data) { double(base_data.merge(state: 'open')) } it 'returns formatted attributes' do expected = { + iid: 1347, project: project, + milestone: nil, title: 'Found a bug', description: "*Created by: octocat*\n\nI'm having a problem with this.", state: 'opened', @@ -46,11 +49,13 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do context 'when issue is closed' do let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) } + let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) } it 'returns formatted attributes' do expected = { + iid: 1347, project: project, + milestone: nil, title: 'Found a bug', description: "*Created by: octocat*\n\nI'm having a problem with this.", state: 'closed', @@ -65,7 +70,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end context 'when it is assigned to someone' do - let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) } + let(:raw_data) { double(base_data.merge(assignee: octocat)) } it 'returns nil as assignee_id when is not a GitLab user' do expect(issue.attributes.fetch(:assignee_id)).to be_nil @@ -78,8 +83,23 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end end + context 'when it has a milestone' do + let(:milestone) { double(number: 45) } + let(:raw_data) { double(base_data.merge(milestone: milestone)) } + + it 'returns nil when milestone does not exist' do + expect(issue.attributes.fetch(:milestone)).to be_nil + end + + it 'returns milestone when it exists' do + milestone = create(:milestone, project: project, iid: 45) + + expect(issue.attributes.fetch(:milestone)).to eq milestone + end + end + context 'when author is a GitLab user' do - let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) } + let(:raw_data) { double(base_data.merge(user: octocat)) } 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 @@ -95,7 +115,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do describe '#has_comments?' do context 'when number of comments is greater than zero' do - let(:raw_data) { OpenStruct.new(base_data.merge(comments: 1)) } + let(:raw_data) { double(base_data.merge(comments: 1)) } it 'returns true' do expect(issue.has_comments?).to eq true @@ -103,7 +123,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end context 'when number of comments is equal to zero' do - let(:raw_data) { OpenStruct.new(base_data.merge(comments: 0)) } + let(:raw_data) { double(base_data.merge(comments: 0)) } it 'returns false' do expect(issue.has_comments?).to eq false @@ -112,7 +132,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end describe '#number' do - let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) } + let(:raw_data) { double(base_data.merge(number: 1347)) } it 'returns pull request number' do expect(issue.number).to eq 1347 @@ -121,7 +141,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do describe '#valid?' do context 'when mention a pull request' do - let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: OpenStruct.new)) } + let(:raw_data) { double(base_data.merge(pull_request: double)) } it 'returns false' do expect(issue.valid?).to eq false @@ -129,7 +149,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end context 'when does not mention a pull request' do - let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: nil)) } + let(:raw_data) { double(base_data.merge(pull_request: nil)) } it 'returns true' do expect(issue.valid?).to eq true diff --git a/spec/lib/gitlab/github_import/label_formatter_spec.rb b/spec/lib/gitlab/github_import/label_formatter_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e94440a7fb05b2251accb3823f79a5ef5b0b4071 --- /dev/null +++ b/spec/lib/gitlab/github_import/label_formatter_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::LabelFormatter, lib: true do + + describe '#attributes' do + it 'returns formatted attributes' do + project = create(:project) + raw = double(name: 'improvements', color: 'e6e6e6') + + formatter = described_class.new(project, raw) + + expect(formatter.attributes).to eq({ + project: project, + title: 'improvements', + color: '#e6e6e6' + }) + end + end +end diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5a421e505811167bec0f3d2cb1d79c1aad429666 --- /dev/null +++ b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::MilestoneFormatter, lib: true do + let(:project) { create(:empty_project) } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + let(:base_data) do + { + number: 1347, + state: 'open', + title: '1.0', + description: 'Version 1.0', + due_on: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil + } + end + + subject(:formatter) { described_class.new(project, raw_data)} + + describe '#attributes' do + context 'when milestone is open' do + let(:raw_data) { double(base_data.merge(state: 'open')) } + + it 'returns formatted attributes' do + expected = { + iid: 1347, + project: project, + title: '1.0', + description: 'Version 1.0', + state: 'active', + due_date: nil, + created_at: created_at, + updated_at: updated_at + } + + expect(formatter.attributes).to eq(expected) + end + end + + context 'when milestone is closed' do + let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } + let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) } + + it 'returns formatted attributes' do + expected = { + iid: 1347, + project: project, + title: '1.0', + description: 'Version 1.0', + state: 'closed', + due_date: nil, + created_at: created_at, + updated_at: closed_at + } + + expect(formatter.attributes).to eq(expected) + end + end + + context 'when milestone has a due date' do + let(:due_date) { DateTime.strptime('2011-01-28T19:01:12Z') } + let(:raw_data) { double(base_data.merge(due_on: due_date)) } + + it 'returns formatted attributes' do + expected = { + iid: 1347, + project: project, + title: '1.0', + description: 'Version 1.0', + state: 'active', + due_date: due_date, + created_at: created_at, + updated_at: updated_at + } + + expect(formatter.attributes).to eq(expected) + end + end + end +end diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb index c93a3ebdaeccef652ade6ce67732b54893c38e5d..0f363b8b0aaa6db109ce6d01ebfe39de5a1689bb 100644 --- a/spec/lib/gitlab/github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/github_import/project_creator_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do owner: OpenStruct.new(login: "john") ) end - let(:namespace){ create(:group, owner: user) } + let(:namespace) { create(:group, owner: user) } let(:token) { "asdffg" } let(:access_params) { { github_access_token: token } } @@ -27,6 +27,8 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do project = project_creator.execute expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git") + expect(project.safe_import_url).to eq("https://*****@gitlab.com/asd/vim.git") + expect(project.import_data.credentials).to eq(user: "asdffg", password: nil) expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) end end 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 e49dcb42342f54089a48d66e91cc4a8f7566afff..e59c0ca110e66b35cc16c59601892cf78e041025 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -2,17 +2,18 @@ require 'spec_helper' describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:project) { create(:project) } - let(:repository) { OpenStruct.new(id: 1, fork: false) } + let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } - let(:source_branch) { OpenStruct.new(ref: 'feature', repo: source_repo) } + let(:source_branch) { double(ref: 'feature', repo: source_repo) } let(:target_repo) { repository } - let(:target_branch) { OpenStruct.new(ref: 'master', repo: target_repo) } - let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } + let(:target_branch) { double(ref: 'master', repo: target_repo) } + let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:base_data) do { number: 1347, + milestone: nil, state: 'open', title: 'New feature', body: 'Please pull these awesome changes', @@ -31,10 +32,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do describe '#attributes' do context 'when pull request is open' do - let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) } + let(:raw_data) { double(base_data.merge(state: 'open')) } it 'returns formatted attributes' do expected = { + iid: 1347, title: 'New feature', description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, @@ -42,6 +44,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do target_project: project, target_branch: 'master', state: 'opened', + milestone: nil, author_id: project.creator_id, assignee_id: nil, created_at: created_at, @@ -54,10 +57,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do context 'when pull request is closed' do let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) } + let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) } it 'returns formatted attributes' do expected = { + iid: 1347, title: 'New feature', description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, @@ -65,6 +69,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do target_project: project, target_branch: 'master', state: 'closed', + milestone: nil, author_id: project.creator_id, assignee_id: nil, created_at: created_at, @@ -77,10 +82,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do context 'when pull request is merged' do let(:merged_at) { DateTime.strptime('2011-01-28T13:01:12Z') } - let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', merged_at: merged_at)) } + let(:raw_data) { double(base_data.merge(state: 'closed', merged_at: merged_at)) } it 'returns formatted attributes' do expected = { + iid: 1347, title: 'New feature', description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, @@ -88,6 +94,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do target_project: project, target_branch: 'master', state: 'merged', + milestone: nil, author_id: project.creator_id, assignee_id: nil, created_at: created_at, @@ -99,7 +106,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when it is assigned to someone' do - let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) } + let(:raw_data) { double(base_data.merge(assignee: octocat)) } it 'returns nil as assignee_id when is not a GitLab user' do expect(pull_request.attributes.fetch(:assignee_id)).to be_nil @@ -113,7 +120,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when author is a GitLab user' do - let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) } + let(:raw_data) { double(base_data.merge(user: octocat)) } 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 @@ -125,10 +132,25 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id end end + + context 'when it has a milestone' do + let(:milestone) { double(number: 45) } + let(:raw_data) { double(base_data.merge(milestone: milestone)) } + + it 'returns nil when milestone does not exists' do + expect(pull_request.attributes.fetch(:milestone)).to be_nil + end + + it 'returns milestone when is exists' do + milestone = create(:milestone, project: project, iid: 45) + + expect(pull_request.attributes.fetch(:milestone)).to eq milestone + end + end end describe '#number' do - let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) } + let(:raw_data) { double(base_data.merge(number: 1347)) } it 'returns pull request number' do expect(pull_request.number).to eq 1347 @@ -136,11 +158,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end describe '#valid?' do - let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') } + let(:invalid_branch) { double(ref: 'invalid-branch').as_null_object } context 'when source, and target repositories are the same' do context 'and source and target branches exists' do - let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) } + let(:raw_data) { double(base_data.merge(head: source_branch, base: target_branch)) } it 'returns true' do expect(pull_request.valid?).to eq true @@ -148,7 +170,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'and source branch doesn not exists' do - let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) } + let(:raw_data) { double(base_data.merge(head: invalid_branch, base: target_branch)) } it 'returns false' do expect(pull_request.valid?).to eq false @@ -156,7 +178,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'and target branch doesn not exists' do - let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) } + let(:raw_data) { double(base_data.merge(head: source_branch, base: invalid_branch)) } it 'returns false' do expect(pull_request.valid?).to eq false @@ -165,8 +187,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when source repo is a fork' do - let(:source_repo) { OpenStruct.new(id: 2, fork: true) } - let(:raw_data) { OpenStruct.new(base_data) } + let(:source_repo) { double(id: 2, fork: true) } + let(:raw_data) { double(base_data) } it 'returns false' do expect(pull_request.valid?).to eq false @@ -174,8 +196,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when target repo is a fork' do - let(:target_repo) { OpenStruct.new(id: 2, fork: true) } - let(:raw_data) { OpenStruct.new(base_data) } + let(:target_repo) { double(id: 2, fork: true) } + let(:raw_data) { double(base_data) } it 'returns false' do expect(pull_request.valid?).to eq false diff --git a/spec/lib/gitlab/github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb index aed2aa39e3a33ef3d3062fc1fe3a7a4df9becf23..1bd29b8a563f3c71b4f335c64f8b7d06cfcf0800 100644 --- a/spec/lib/gitlab/github_import/wiki_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' describe Gitlab::GithubImport::WikiFormatter, lib: true do let(:project) do - create(:project, namespace: create(:namespace, path: 'gitlabhq'), - import_url: 'https://xxx@github.com/gitlabhq/sample.gitlabhq.git') + create(:project, + namespace: create(:namespace, path: 'gitlabhq'), + import_url: 'https://xxx@github.com/gitlabhq/sample.gitlabhq.git') end - subject(:wiki) { described_class.new(project)} + subject(:wiki) { described_class.new(project) } describe '#path_with_namespace' do it 'appends .wiki to project path' do diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9175356c641a1ae0fc9892ae3a1935f39128cf40 --- /dev/null +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::MembersMapper, services: true do + describe :map do + + let(:user) { create(:user) } + let(:project) { create(:project, :public, name: 'searchable_project') } + let(:user2) { create(:user) } + let(:exported_user_id) { 99 } + let(:exported_members) do + [{ + "id" => 2, + "access_level" => 40, + "source_id" => 14, + "source_type" => "Project", + "user_id" => 19, + "notification_level" => 3, + "created_at" => "2016-03-11T10:21:44.822Z", + "updated_at" => "2016-03-11T10:21:44.822Z", + "created_by_id" => nil, + "invite_email" => nil, + "invite_token" => nil, + "invite_accepted_at" => nil, + "user" => + { + "id" => exported_user_id, + "email" => user2.email, + "username" => user2.username + } + }] + end + + let(:members_mapper) do + Gitlab::ImportExport::MembersMapper.new( + exported_members: exported_members, user: user, project_id: project.id) + end + + it 'maps a project member' do + expect(members_mapper.map[exported_user_id]).to eq(user2.id) + end + + it 'defaults to importer project member if it does not exist' do + expect(members_mapper.map[-1]).to eq(user.id) + end + end + + def project_member_user_id(id) + members_mapper.map[id] + end +end diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f3d3a57ddd7b7ef3bdcf244a7c61214a0e5c0614 --- /dev/null +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do + describe :restore do + + let(:user) { create(:user) } + let(:project_tree_restorer) { Gitlab::ImportExport::ProjectTreeRestorer.new(path: "fixtures/import_export/project.json", user: user) } + + context 'JSON' do + let(:restored_project_json) do + project_tree_restorer.restore + end + + it 'restores models based on JSON' do + expect(restored_project_json).to be true + end + end + end +end diff --git a/spec/lib/gitlab/import_url_spec.rb b/spec/lib/gitlab/import_url_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f758cb8693c45f1e9e01a4ab18f6f0ed63288ccb --- /dev/null +++ b/spec/lib/gitlab/import_url_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::ImportUrl do + + let(:credentials) { { user: 'blah', password: 'password' } } + let(:import_url) do + Gitlab::ImportUrl.new("https://github.com/me/project.git", credentials: credentials) + end + + describe :full_url do + it { expect(import_url.full_url).to eq("https://blah:password@github.com/me/project.git") } + end + + describe :sanitized_url do + it { expect(import_url.sanitized_url).to eq("https://github.com/me/project.git") } + end + + describe :credentials do + it { expect(import_url.credentials).to eq(credentials) } + end +end diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index ad4290c43bbc725aaffd40e756f144f156f6bb32..5c885a7a982a360812b5dd7f0e3cc9f26ca779f7 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -33,8 +33,16 @@ describe Gitlab::Metrics::Instrumentation do described_class.instrument_method(@dummy, :foo) end - it 'renames the original method' do - expect(@dummy).to respond_to(:_original_foo) + it 'instruments the Class' do + target = @dummy.singleton_class + + expect(described_class.instrumented?(target)).to eq(true) + end + + it 'defines a proxy method' do + mod = described_class.proxy_module(@dummy.singleton_class) + + expect(mod.method_defined?(:foo)).to eq(true) end it 'calls the instrumented method with the correct arguments' do @@ -76,6 +84,14 @@ describe Gitlab::Metrics::Instrumentation do expect(dummy.method(:test).arity).to eq(0) end + + describe 'when a module is instrumented multiple times' do + it 'calls the instrumented method with the correct arguments' do + described_class.instrument_method(@dummy, :foo) + + expect(@dummy.foo).to eq('foo') + end + end end describe 'with metrics disabled' do @@ -86,7 +102,9 @@ describe Gitlab::Metrics::Instrumentation do it 'does not instrument the method' do described_class.instrument_method(@dummy, :foo) - expect(@dummy).to_not respond_to(:_original_foo) + target = @dummy.singleton_class + + expect(described_class.instrumented?(target)).to eq(false) end end end @@ -100,8 +118,14 @@ describe Gitlab::Metrics::Instrumentation do instrument_instance_method(@dummy, :bar) end - it 'renames the original method' do - expect(@dummy.method_defined?(:_original_bar)).to eq(true) + it 'instruments instances of the Class' do + expect(described_class.instrumented?(@dummy)).to eq(true) + end + + it 'defines a proxy method' do + mod = described_class.proxy_module(@dummy) + + expect(mod.method_defined?(:bar)).to eq(true) end it 'calls the instrumented method with the correct arguments' do @@ -144,7 +168,7 @@ describe Gitlab::Metrics::Instrumentation do described_class. instrument_instance_method(@dummy, :bar) - expect(@dummy.method_defined?(:_original_bar)).to eq(false) + expect(described_class.instrumented?(@dummy)).to eq(false) end end end @@ -167,18 +191,17 @@ describe Gitlab::Metrics::Instrumentation do it 'recursively instruments a class hierarchy' do described_class.instrument_class_hierarchy(@dummy) - expect(@child1).to respond_to(:_original_child1_foo) - expect(@child2).to respond_to(:_original_child2_foo) + expect(described_class.instrumented?(@child1.singleton_class)).to eq(true) + expect(described_class.instrumented?(@child2.singleton_class)).to eq(true) - expect(@child1.method_defined?(:_original_child1_bar)).to eq(true) - expect(@child2.method_defined?(:_original_child2_bar)).to eq(true) + expect(described_class.instrumented?(@child1)).to eq(true) + expect(described_class.instrumented?(@child2)).to eq(true) end it 'does not instrument the root module' do described_class.instrument_class_hierarchy(@dummy) - expect(@dummy).to_not respond_to(:_original_foo) - expect(@dummy.method_defined?(:_original_bar)).to eq(false) + expect(described_class.instrumented?(@dummy)).to eq(false) end end @@ -190,7 +213,7 @@ describe Gitlab::Metrics::Instrumentation do it 'instruments all public class methods' do described_class.instrument_methods(@dummy) - expect(@dummy).to respond_to(:_original_foo) + expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true) end it 'only instruments methods directly defined in the module' do @@ -223,7 +246,7 @@ describe Gitlab::Metrics::Instrumentation do it 'instruments all public instance methods' do described_class.instrument_instance_methods(@dummy) - expect(@dummy.method_defined?(:_original_bar)).to eq(true) + expect(described_class.instrumented?(@dummy)).to eq(true) end it 'only instruments methods directly defined in the module' do diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 7bc070a4d0967fa33345f8650028127a24accbc9..e3293a012078ade4ad825bbbaebeddf0666d8b21 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -28,6 +28,9 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do expect(transaction).to receive(:increment). with(:sql_duration, 0.2) + expect(transaction).to receive(:increment). + with(:sql_count, 1) + subscriber.sql(event) end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 3dee13e27f442d2b7c5cba896f175c0f36b3062a..96f7eabbca650d1e1824bab65fb867d8a971d95b 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -98,4 +98,53 @@ describe Gitlab::Metrics do end end end + + describe '.tag_transaction' do + context 'without a transaction' do + it 'does nothing' do + expect_any_instance_of(Gitlab::Metrics::Transaction). + not_to receive(:add_tag) + + Gitlab::Metrics.tag_transaction(:foo, 'bar') + end + end + + context 'with a transaction' do + let(:transaction) { Gitlab::Metrics::Transaction.new } + + it 'adds the tag to the transaction' do + expect(Gitlab::Metrics).to receive(:current_transaction). + and_return(transaction) + + expect(transaction).to receive(:add_tag). + with(:foo, 'bar') + + Gitlab::Metrics.tag_transaction(:foo, 'bar') + end + end + end + + describe '.action=' do + context 'without a transaction' do + it 'does nothing' do + expect_any_instance_of(Gitlab::Metrics::Transaction). + not_to receive(:action=) + + Gitlab::Metrics.action = 'foo' + end + end + + context 'with a transaction' do + it 'sets the action of a transaction' do + trans = Gitlab::Metrics::Transaction.new + + expect(Gitlab::Metrics).to receive(:current_transaction). + and_return(trans) + + expect(trans).to receive(:action=).with('foo') + + Gitlab::Metrics.action = 'foo' + end + end + end end diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb index da6526774437aa24bc4551ffce44db080520ab13..f093d0a0d8b3a7dd5b481841cf7e92d68abdc783 100644 --- a/spec/lib/gitlab/note_data_builder_spec.rb +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -4,13 +4,12 @@ describe 'Gitlab::NoteDataBuilder', lib: true do let(:project) { create(:project) } let(:user) { create(:user) } let(:data) { Gitlab::NoteDataBuilder.build(note, user) } - let(:note_url) { Gitlab::UrlBuilder.new(:note).build(note.id) } let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors before(:each) do expect(data).to have_key(:object_attributes) expect(data[:object_attributes]).to have_key(:url) - expect(data[:object_attributes][:url]).to eq(note_url) + expect(data[:object_attributes][:url]).to eq(Gitlab::UrlBuilder.build(note)) expect(data[:object_kind]).to eq('note') expect(data[:user]).to eq(user.hook_attrs) end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 3a769acfdc0a28f679fda4120eea652aea9b7b1e..6727a83e58a70c41975229884ceb21ac517e460d 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -15,20 +15,20 @@ describe Gitlab::OAuth::User, lib: true do end let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } - describe :persisted? do + describe '#persisted?' do let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } it "finds an existing user based on uid and provider (facebook)" do expect( oauth_user.persisted? ).to be_truthy end - it "returns false if use is not found in database" do + it 'returns false if user is not found in database' do allow(auth_hash).to receive(:uid).and_return('non-existing') expect( oauth_user.persisted? ).to be_falsey end end - describe :save do + describe '#save' do def stub_omniauth_config(messages) allow(Gitlab.config.omniauth).to receive_messages(messages) end @@ -40,8 +40,27 @@ describe Gitlab::OAuth::User, lib: true do let(:provider) { 'twitter' } describe 'signup' do - shared_examples "to verify compliance with allow_single_sign_on" do - context "with new allow_single_sign_on enabled syntax" do + shared_examples 'to verify compliance with allow_single_sign_on' do + context 'provider is marked as external' do + it 'should mark user as external' do + stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter']) + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_truthy + end + end + + context 'provider was external, now has been removed' do + it 'should mark existing user internal' do + create(:omniauth_user, extern_uid: 'my-uid', provider: 'twitter', external: true) + stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['facebook']) + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_falsey + end + end + + context 'with new allow_single_sign_on enabled syntax' do before { stub_omniauth_config(allow_single_sign_on: ['twitter']) } it "creates a user from Omniauth" do @@ -67,16 +86,16 @@ describe Gitlab::OAuth::User, lib: true do end end - context "with new allow_single_sign_on disabled syntax" do + context 'with new allow_single_sign_on disabled syntax' do before { stub_omniauth_config(allow_single_sign_on: []) } - it "throws an error" do + it 'throws an error' do expect{ oauth_user.save }.to raise_error StandardError end end - context "with old allow_single_sign_on disabled (Default)" do + context 'with old allow_single_sign_on disabled (Default)' do before { stub_omniauth_config(allow_single_sign_on: false) } - it "throws an error" do + it 'throws an error' do expect{ oauth_user.save }.to raise_error StandardError end end diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb index 961022b9d12ebc85b65cc82d66776470bd19f1ff..7fc34139eff2ee8e54138001ded97b2858cf473b 100644 --- a/spec/lib/gitlab/push_data_builder_spec.rb +++ b/spec/lib/gitlab/push_data_builder_spec.rb @@ -14,11 +14,11 @@ describe Gitlab::PushDataBuilder, lib: true do it { expect(data[:ref]).to eq('refs/heads/master') } it { expect(data[:commits].size).to eq(3) } it { expect(data[:total_commits_count]).to eq(3) } - it { expect(data[:commits].first[:added]).to eq(["gitlab-grack"]) } - it { expect(data[:commits].first[:modified]).to eq([".gitmodules"]) } + it { expect(data[:commits].first[:added]).to eq(['gitlab-grack']) } + it { expect(data[:commits].first[:modified]).to eq(['.gitmodules']) } it { expect(data[:commits].first[:removed]).to eq([]) } - include_examples 'project hook data' + include_examples 'project hook data with deprecateds' include_examples 'deprecated repository hook data' end @@ -34,9 +34,18 @@ describe Gitlab::PushDataBuilder, lib: true do it { expect(data[:checkout_sha]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } it { expect(data[:after]).to eq('8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b') } it { expect(data[:ref]).to eq('refs/tags/v1.1.0') } + it { expect(data[:user_id]).to eq(user.id) } + it { expect(data[:user_name]).to eq(user.name) } + it { expect(data[:user_email]).to eq(user.email) } + it { expect(data[:user_avatar]).to eq(user.avatar_url) } + it { expect(data[:project_id]).to eq(project.id) } + it { expect(data[:project]).to be_a(Hash) } it { expect(data[:commits]).to be_empty } it { expect(data[:total_commits_count]).to be_zero } + include_examples 'project hook data with deprecateds' + include_examples 'deprecated repository hook data' + it 'does not raise an error when given nil commits' do expect { described_class.build(spy, spy, spy, spy, spy, nil) }. not_to raise_error diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index f023be6ae45dd3e7374a5a70d6e4703eafb17f1c..bf11472407a89d9d41b891a2586c8ea1e65281ae 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -1,77 +1,119 @@ require 'spec_helper' describe Gitlab::UrlBuilder, lib: true do - describe 'When asking for an issue' do - it 'returns the issue url' do - issue = create(:issue) - url = Gitlab::UrlBuilder.new(:issue).build(issue.id) - expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}" - end - end + describe '.build' do + context 'when passing a Commit' do + it 'returns a proper URL' do + commit = build_stubbed(:commit) - describe 'When asking for an merge request' do - it 'returns the merge request url' do - merge_request = create(:merge_request) - url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" + url = described_class.build(commit) + + expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}" + end end - end - describe 'When asking for a note on commit' do - let(:note) { create(:note_on_commit) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + context 'when passing an Issue' do + it 'returns a proper URL' do + issue = build_stubbed(:issue, iid: 42) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + url = described_class.build(issue) + + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}" + end end - end - describe 'When asking for a note on commit diff' do - let(:note) { create(:note_on_commit_diff) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + context 'when passing a MergeRequest' do + it 'returns a proper URL' do + merge_request = build_stubbed(:merge_request, iid: 42) + + url = described_class.build(merge_request) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" + end end - end - describe 'When asking for a note on issue' do - let(:issue) { create(:issue) } - let(:note) { create(:note_on_issue, noteable_id: issue.id) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + context 'when passing a Note' do + context 'on a Commit' do + it 'returns a proper URL' do + note = build_stubbed(:note_on_commit) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}" - end - end + url = described_class.build(note) - describe 'When asking for a note on merge request' do - let(:merge_request) { create(:merge_request) } - let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" - end - end + context 'on a CommitDiff' do + it 'returns a proper URL' do + note = build_stubbed(:note_on_commit_diff) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end - describe 'When asking for a note on merge request diff' do - let(:merge_request) { create(:merge_request) } - let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + context 'on an Issue' do + it 'returns a proper URL' do + issue = create(:issue, iid: 42) + note = build_stubbed(:note_on_issue, noteable: issue) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}" + end + end + + context 'on a MergeRequest' do + it 'returns a proper URL' do + merge_request = create(:merge_request, iid: 42) + note = build_stubbed(:note_on_merge_request, noteable: merge_request) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + context 'on a MergeRequestDiff' do + it 'returns a proper URL' do + merge_request = create(:merge_request, iid: 42) + note = build_stubbed(:note_on_merge_request_diff, noteable: merge_request) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + context 'on a ProjectSnippet' do + it 'returns a proper URL' do + project_snippet = create(:project_snippet) + note = build_stubbed(:note_on_project_snippet, noteable: project_snippet) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" + end + end + + context 'on another object' do + it 'returns a proper URL' do + project = build_stubbed(:project) + + expect { described_class.build(project) }. + to raise_error(NotImplementedError, 'No URL builder defined for Project') + end + end end - end - describe 'When asking for a note on project snippet' do - let(:snippet) { create(:project_snippet) } - let(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + context 'when passing a WikiPage' do + it 'returns a proper URL' do + wiki_page = build(:wiki_page) + url = described_class.build(wiki_page) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" + expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_base_path}/#{wiki_page.slug}" + end end end end diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c59dfea5c55f1dce507915547fcff4ee8f2541a6 --- /dev/null +++ b/spec/lib/gitlab_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe Gitlab, lib: true do + describe '.com?' do + it 'is true when on GitLab.com' do + stub_config_setting(url: 'https://gitlab.com') + + expect(described_class.com?).to eq true + end + + it 'is false when not on GitLab.com' do + stub_config_setting(url: 'http://example.com') + + expect(described_class.com?).to eq false + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 631b5094f4206a4c39a761bc7863aee9fe87efb2..495c5cbac00836419a01585f526b186241bf474c 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -213,7 +213,7 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ end it 'contains a link to the new merge request' do @@ -268,7 +268,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ end it 'contains the name of the previous assignee' do @@ -302,7 +302,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ end it 'contains the names of the added labels' do @@ -331,7 +331,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/i + is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/i end it 'contains the new status' do @@ -364,7 +364,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ end it 'contains the new status' do @@ -502,7 +502,7 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ end it 'contains a link to the merge request note' do diff --git a/spec/mailers/repository_check_mailer_spec.rb b/spec/mailers/repository_check_mailer_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..00613c7b6710cc33ca7de3aa266526fa9f0a4741 --- /dev/null +++ b/spec/mailers/repository_check_mailer_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe RepositoryCheckMailer do + include EmailSpec::Matchers + + describe '.notify' do + it 'emails all admins' do + admins = create_list(:admin, 3) + + mail = described_class.notify(1) + + expect(mail).to deliver_to admins.map(&:email) + end + + it 'mentions the number of failed checks' do + mail = described_class.notify(3) + + expect(mail).to have_subject 'GitLab Admin | 3 projects failed their last repository check' + end + end +end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index b7457808040e725dace82a174ed9527c0ee0c42a..b5d356aa06697eda31754b55a9162e991dabfe36 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -238,6 +238,22 @@ describe Ci::Build, models: true do it { is_expected.to eq(predefined_variables + predefined_trigger_variable + yaml_variables + secure_variables + trigger_variables) } end + + context 'when job variables are defined' do + ## + # Job-level variables are defined in gitlab_ci.yml fixture + # + context 'when job variables are unique' do + let(:build) { create(:ci_build, name: 'staging') } + + it 'includes job variables' do + expect(subject).to include( + { key: :KEY1, value: 'value1', public: true }, + { key: :KEY2, value: 'value2', public: true } + ) + end + end + end end end end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 412842337bae8ff47bc6f83a610547f8393f4326..82c18aaa01aa2db6846e54563f71318275beeb06 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -27,6 +27,8 @@ describe Ci::Commit, models: true do it { is_expected.to have_many(:trigger_requests) } it { is_expected.to have_many(:builds) } it { is_expected.to validate_presence_of :sha } + it { is_expected.to validate_presence_of :status } + it { is_expected.to delegate_method(:stages).to(:statuses) } it { is_expected.to respond_to :git_author_name } it { is_expected.to respond_to :git_author_email } @@ -52,57 +54,9 @@ describe Ci::Commit, models: true do it { expect(commit.sha).to start_with(subject) } end - describe :stage do - subject { commit.stage } - - before do - @second = FactoryGirl.create :commit_status, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: 'pending' - @first = FactoryGirl.create :commit_status, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: 'pending' - end - - it 'returns first running stage' do - is_expected.to eq('test') - end - - context 'first build succeeded' do - before do - @first.success - end - - it 'returns last running stage' do - is_expected.to eq('deploy') - end - end - - context 'all builds succeeded' do - before do - @first.success - @second.success - end - - it 'returns nil' do - is_expected.to be_nil - end - end - end - describe :create_next_builds do end - describe :refs do - subject { commit.refs } - - before do - FactoryGirl.create :commit_status, commit: commit, name: 'deploy' - FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'develop' - FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'master' - end - - it 'returns all refs' do - is_expected.to contain_exactly('master', 'develop', nil) - end - end - describe :retried do subject { commit.retried } @@ -117,10 +71,10 @@ describe Ci::Commit, models: true do end describe :create_builds do - let!(:commit) { FactoryGirl.create :ci_commit, project: project } + let!(:commit) { FactoryGirl.create :ci_commit, project: project, ref: 'master', tag: false } def create_builds(trigger_request = nil) - commit.create_builds('master', false, nil, trigger_request) + commit.create_builds(nil, trigger_request) end def create_next_builds @@ -143,67 +97,6 @@ describe Ci::Commit, models: true do expect(create_next_builds).to be_falsey end - context 'for different ref' do - def create_develop_builds - commit.create_builds('develop', false, nil, nil) - end - - it 'creates builds' do - expect(create_builds).to be_truthy - commit.builds.update_all(status: "success") - expect(commit.builds.count(:all)).to eq(2) - - expect(create_develop_builds).to be_truthy - commit.builds.update_all(status: "success") - expect(commit.builds.count(:all)).to eq(4) - expect(commit.refs.size).to eq(2) - expect(commit.builds.pluck(:name).uniq.size).to eq(2) - end - end - - context 'for build triggers' do - let(:trigger) { FactoryGirl.create :ci_trigger, project: project } - let(:trigger_request) { FactoryGirl.create :ci_trigger_request, commit: commit, trigger: trigger } - - it 'creates builds' do - expect(create_builds(trigger_request)).to be_truthy - expect(commit.builds.count(:all)).to eq(2) - end - - it 'rebuilds commit' do - expect(create_builds).to be_truthy - expect(commit.builds.count(:all)).to eq(2) - - expect(create_builds(trigger_request)).to be_truthy - expect(commit.builds.count(:all)).to eq(4) - end - - it 'creates next builds' do - expect(create_builds(trigger_request)).to be_truthy - expect(commit.builds.count(:all)).to eq(2) - commit.builds.update_all(status: "success") - - expect(create_next_builds).to be_truthy - expect(commit.builds.count(:all)).to eq(4) - end - - context 'for [ci skip]' do - before do - allow(commit).to receive(:git_commit_message) { 'message [ci skip]' } - end - - it 'rebuilds commit' do - expect(commit.status).to eq('skipped') - expect(create_builds).to be_truthy - - # since everything in Ci::Commit is cached we need to fetch a new object - new_commit = Ci::Commit.find_by_id(commit.id) - expect(new_commit.status).to eq('pending') - end - end - end - - context 'custom stage with first job allowed to fail' do let(:yaml) do { @@ -284,6 +177,7 @@ describe Ci::Commit, models: true do commit.builds.running_or_pending.each(&:success) expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') + commit.reload expect(commit.status).to eq('success') end @@ -306,6 +200,7 @@ describe Ci::Commit, models: true do commit.builds.running_or_pending.each(&:success) expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') + commit.reload expect(commit.status).to eq('failed') end @@ -329,6 +224,7 @@ describe Ci::Commit, models: true do expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') + commit.reload expect(commit.status).to eq('failed') end @@ -351,6 +247,7 @@ describe Ci::Commit, models: true do commit.builds.running_or_pending.each(&:success) expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') + commit.reload expect(commit.status).to eq('failed') end end @@ -402,4 +299,98 @@ describe Ci::Commit, models: true do expect(commit.coverage).to be_nil end end + + describe '#retryable?' do + subject { commit.retryable? } + + context 'no failed builds' do + before do + FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'success' + end + + it 'be not retryable' do + is_expected.to be_falsey + end + end + + context 'with failed builds' do + before do + FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'running' + FactoryGirl.create :ci_build, name: "rubocop", commit: commit, status: 'failed' + end + + it 'be retryable' do + is_expected.to be_truthy + end + end + end + + describe '#stages' do + let(:commit2) { FactoryGirl.create :ci_commit, project: project } + subject { CommitStatus.where(commit: [commit, commit2]).stages } + + before do + FactoryGirl.create :ci_build, commit: commit2, stage: 'test', stage_idx: 1 + FactoryGirl.create :ci_build, commit: commit, stage: 'build', stage_idx: 0 + end + + it 'return all stages' do + is_expected.to eq(%w(build test)) + end + end + + describe '#update_state' do + it 'execute update_state after touching object' do + expect(commit).to receive(:update_state).and_return(true) + commit.touch + end + + context 'dependent objects' do + let(:commit_status) { build :commit_status, commit: commit } + + it 'execute update_state after saving dependent object' do + expect(commit).to receive(:update_state).and_return(true) + commit_status.save + end + end + + context 'update state' do + let(:current) { Time.now.change(usec: 0) } + let(:build) { FactoryGirl.create :ci_build, :success, commit: commit, started_at: current - 120, finished_at: current - 60 } + + before do + build + end + + [:status, :started_at, :finished_at, :duration].each do |param| + it "update #{param}" do + expect(commit.send(param)).to eq(build.send(param)) + end + end + end + end + + describe '#branch?' do + subject { commit.branch? } + + context 'is not a tag' do + before do + commit.tag = false + end + + it 'return true when tag is set to false' do + is_expected.to be_truthy + end + end + + context 'is not a tag' do + before do + commit.tag = true + end + + it 'return false when tag is set to true' do + is_expected.to be_falsey + end + end + end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 0e9111c8029332a962f3a7cc9d2815c8cdeb569c..ad47e338a337ef7c0f711bd4f6e20e92edee4150 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -163,4 +163,12 @@ eos it { expect(commit.reverts_commit?(another_commit)).to be_truthy } end end + + describe '#ci_commits' do + # TODO: kamil + end + + describe '#status' do + # TODO: kamil + end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 82c68ff6cb1890f9304db93332c1eaa5099421b8..971e6750375f2143a8386dba3b520b9b5ac518fa 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -163,37 +163,73 @@ describe CommitStatus, models: true do end it 'return unique statuses' do - is_expected.to eq([@commit2, @commit3, @commit4, @commit5]) + is_expected.to eq([@commit4, @commit5]) end end - describe :for_ref do - subject { CommitStatus.for_ref('bb').order(:id) } + describe :running_or_pending do + subject { CommitStatus.running_or_pending.order(:id) } before do @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success' + @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed' + @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled' end - it 'return statuses with equal and nil ref set' do - is_expected.to eq([@commit1]) + it 'return statuses that are running or pending' do + is_expected.to eq([@commit1, @commit2]) end end - describe :running_or_pending do - subject { CommitStatus.running_or_pending.order(:id) } + describe '#before_sha' do + subject { commit_status.before_sha } + + context 'when no before_sha is set for ci::commit' do + before { commit.before_sha = nil } + + it 'return blank sha' do + is_expected.to eq(Gitlab::Git::BLANK_SHA) + end + end + + context 'for before_sha set for ci::commit' do + let(:value) { '1234' } + before { commit.before_sha = value } + + it 'return the set value' do + is_expected.to eq(value) + end + end + end + describe '#stages' do before do - @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' - @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' - @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success' - @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed' - @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled' + FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'success' + FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'failed' + FactoryGirl.create :commit_status, commit: commit, stage: 'deploy', stage_idx: 2, status: 'running' + FactoryGirl.create :commit_status, commit: commit, stage: 'test', stage_idx: 1, status: 'success' end - it 'return statuses that are running or pending' do - is_expected.to eq([@commit1, @commit2]) + context 'stages list' do + subject { CommitStatus.where(commit: commit).stages } + + it 'return ordered list of stages' do + is_expected.to eq(%w(build test deploy)) + end + end + + context 'stages with statuses' do + subject { CommitStatus.where(commit: commit).stages_status } + + it 'return list of stages with statuses' do + is_expected.to eq({ + 'build' => 'failed', + 'test' => 'success', + 'deploy' => 'running' + }) + end end end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index b16ccc6e30521826938f997884be8c22e7c56c10..4a4cd093435ef1ab35fcc2050a9c01f96699ace5 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -212,4 +212,34 @@ describe Issue, "Issuable" do expect(issue.downvotes).to eq(1) end end + + describe ".with_label" do + let(:project) { create(:project, :public) } + let(:bug) { create(:label, project: project, title: 'bug') } + let(:feature) { create(:label, project: project, title: 'feature') } + let(:enhancement) { create(:label, project: project, title: 'enhancement') } + let(:issue1) { create(:issue, title: "Bugfix1", project: project) } + let(:issue2) { create(:issue, title: "Bugfix2", project: project) } + let(:issue3) { create(:issue, title: "Feature1", project: project) } + + before(:each) do + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + issue2.labels << enhancement + issue3.labels << feature + end + + it 'finds the correct issue containing just enhancement label' do + expect(Issue.with_label(enhancement.title)).to match_array([issue2]) + end + + it 'finds the correct issues containing the same label' do + expect(Issue.with_label(bug.title)).to match_array([issue1, issue2]) + end + + it 'finds the correct issues containing only both labels' do + expect(Issue.with_label([bug.title, enhancement.title])).to match_array([issue2]) + end + end end diff --git a/spec/lib/ci/status_spec.rb b/spec/models/concerns/statuseable_spec.rb similarity index 90% rename from spec/lib/ci/status_spec.rb rename to spec/models/concerns/statuseable_spec.rb index 47f3df6e3ce008ec265c683b9544fd097b453a6d..dacbd3034c01f83bfee3057861391b2b53593447 100644 --- a/spec/lib/ci/status_spec.rb +++ b/spec/models/concerns/statuseable_spec.rb @@ -1,8 +1,17 @@ require 'spec_helper' -describe Ci::Status do - describe '.get_status' do - subject { described_class.get_status(statuses) } +describe Statuseable do + before do + @object = Object.new + @object.extend(Statuseable::ClassMethods) + end + + describe '.status' do + before do + allow(@object).to receive(:all).and_return(CommitStatus.where(id: statuses)) + end + + subject { @object.status } shared_examples 'build status summary' do context 'all successful' do diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb index 9b144dd1ecccdb81e38d0ed6ea64f2a7710ad5b6..4fc3b065592cbfa7997e6ef15022e469a6bdfe94 100644 --- a/spec/models/external_issue_spec.rb +++ b/spec/models/external_issue_spec.rb @@ -36,4 +36,19 @@ describe ExternalIssue, models: true do expect(issue.title).to eq "External Issue #{issue}" end end + + describe '#reference_link_text' do + context 'if issue id has a prefix' do + it 'returns the issue ID' do + expect(issue.reference_link_text).to eq 'EXT-1234' + end + end + + context 'if issue id is a number' do + let(:issue) { described_class.new('1234', project) } + it 'returns the issue ID prefixed by #' do + expect(issue.reference_link_text).to eq '#1234' + end + end + end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 15052aaca28ed42db4b3765a89e4c9d572636560..060e6599104530840d2fa6bcb26683d50dbec441 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -191,11 +191,36 @@ describe Issue, models: true do end describe '#related_branches' do - it "selects the right branches" do + let(:user) { build(:admin) } + + before do allow(subject.project.repository).to receive(:branch_names). - and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name]) + and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name, "#{subject.iid}-branch"]) + + # Without this stub, the `create(:merge_request)` above fails because it can't find + # the source branch. This seems like a reasonable compromise, in comparison with + # setting up a full repo here. + allow_any_instance_of(MergeRequest).to receive(:create_merge_request_diff) + end - expect(subject.related_branches).to eq([subject.to_branch_name]) + it "selects the right branches when there are no referenced merge requests" do + expect(subject.related_branches(user)).to eq([subject.to_branch_name, "#{subject.iid}-branch"]) + end + + it "selects the right branches when there is a referenced merge request" do + merge_request = create(:merge_request, { description: "Closes ##{subject.iid}", + source_project: subject.project, + source_branch: "#{subject.iid}-branch" }) + merge_request.create_cross_references!(user) + expect(subject.referenced_merge_requests).to_not be_empty + expect(subject.related_branches(user)).to eq([subject.to_branch_name]) + end + + it 'excludes stable branches from the related branches' do + allow(subject.project.repository).to receive(:branch_names). + and_return(["#{subject.iid}-0-stable"]) + + expect(subject.related_branches(user)).to eq [] end end @@ -211,10 +236,19 @@ describe Issue, models: true do end describe "#to_branch_name" do - let(:issue) { create(:issue, title: 'a' * 30) } + let(:issue) { create(:issue, title: 'testing-issue') } + + it 'starts with the issue iid' do + expect(issue.to_branch_name).to match /\A#{issue.iid}-[A-Za-z\-]+\z/ + end + + it "contains the issue title if not confidential" do + expect(issue.to_branch_name).to match /testing-issue\z/ + end - it "starts with the issue iid" do - expect(issue.to_branch_name).to match /-#{issue.iid}\z/ + it "does not contain the issue title if confidential" do + issue = create(:issue, title: 'testing-issue', confidential: true) + expect(issue.to_branch_name).to match /confidential-issue\z/ end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 6f5d912fe5da9e31783479e718f7313f28272209..d7884cea336fa9b5b035b1c63a8f0eb898495edf 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -404,12 +404,12 @@ describe MergeRequest, models: true do describe 'when the source project exists' do it 'returns the latest commit' do commit = double(:commit, id: '123abc') - ci_commit = double(:ci_commit) + ci_commit = double(:ci_commit, ref: 'master') allow(subject).to receive(:last_commit).and_return(commit) expect(subject.source_project).to receive(:ci_commit). - with('123abc'). + with('123abc', 'master'). and_return(ci_commit) expect(subject.ci_commit).to eq(ci_commit) diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..295081e9da1646b136a85f08ed6c284738deabdc --- /dev/null +++ b/spec/models/notification_setting_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +RSpec.describe NotificationSetting, type: :model do + describe "Associations" do + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:source) } + end + + describe "Validation" do + subject { NotificationSetting.new(source_id: 1, source_type: 'Project') } + + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:source) } + it { is_expected.to validate_presence_of(:level) } + it { is_expected.to validate_uniqueness_of(:user_id).scoped_to([:source_id, :source_type]).with_message(/already exists in source/) } + end +end diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index c34b2487ecfa6721dc88bb115d11f0cb991517c4..31b2c90122d94e772f166505ad802b4ddc6e0fec 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -21,74 +21,232 @@ require 'spec_helper' describe BambooService, models: true do - describe "Associations" do + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } end - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project) } - - context "when a password was previously set" do - before do - @bamboo_service = BambooService.create( - project: create(:project), - properties: { - bamboo_url: 'http://gitlab.com', - username: 'mic', - password: "password" - } - ) + describe 'Validations' do + describe '#bamboo_url' do + it 'does not validate the presence of bamboo_url if service is not active' do + bamboo_service = service + bamboo_service.active = false + + expect(bamboo_service).not_to validate_presence_of(:bamboo_url) + end + + it 'validates the presence of bamboo_url if service is active' do + bamboo_service = service + bamboo_service.active = true + + expect(bamboo_service).to validate_presence_of(:bamboo_url) + end + end + + describe '#build_key' do + it 'does not validate the presence of build_key if service is not active' do + bamboo_service = service + bamboo_service.active = false + + expect(bamboo_service).not_to validate_presence_of(:build_key) end - - it "reset password if url changed" do - @bamboo_service.bamboo_url = 'http://gitlab1.com' - @bamboo_service.save - expect(@bamboo_service.password).to be_nil + + it 'validates the presence of build_key if service is active' do + bamboo_service = service + bamboo_service.active = true + + expect(bamboo_service).to validate_presence_of(:build_key) + end + end + + describe '#username' do + it 'does not validate the presence of username if service is not active' do + bamboo_service = service + bamboo_service.active = false + + expect(bamboo_service).not_to validate_presence_of(:username) + end + + it 'does not validate the presence of username if username is nil' do + bamboo_service = service + bamboo_service.active = true + bamboo_service.password = nil + + expect(bamboo_service).not_to validate_presence_of(:username) + end + + it 'validates the presence of username if service is active and username is present' do + bamboo_service = service + bamboo_service.active = true + bamboo_service.password = 'secret' + + expect(bamboo_service).to validate_presence_of(:username) end - - it "does not reset password if username changed" do - @bamboo_service.username = "some_name" - @bamboo_service.save - expect(@bamboo_service.password).to eq("password") + end + + describe '#password' do + it 'does not validate the presence of password if service is not active' do + bamboo_service = service + bamboo_service.active = false + + expect(bamboo_service).not_to validate_presence_of(:password) end - it "does not reset password if new url is set together with password, even if it's the same password" do - @bamboo_service.bamboo_url = 'http://gitlab_edited.com' - @bamboo_service.password = 'password' - @bamboo_service.save - expect(@bamboo_service.password).to eq("password") - expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com") + it 'does not validate the presence of password if username is nil' do + bamboo_service = service + bamboo_service.active = true + bamboo_service.username = nil + + expect(bamboo_service).not_to validate_presence_of(:password) end - it "should reset password if url changed, even if setter called multiple times" do - @bamboo_service.bamboo_url = 'http://gitlab1.com' - @bamboo_service.bamboo_url = 'http://gitlab1.com' - @bamboo_service.save - expect(@bamboo_service.password).to be_nil + it 'validates the presence of password if service is active and username is present' do + bamboo_service = service + bamboo_service.active = true + bamboo_service.username = 'john' + + expect(bamboo_service).to validate_presence_of(:password) end end - - context "when no password was previously set" do - before do - @bamboo_service = BambooService.create( - project: create(:project), - properties: { - bamboo_url: 'http://gitlab.com', - username: 'mic' - } - ) + end + + describe 'Callbacks' do + describe 'before_update :reset_password' do + context 'when a password was previously set' do + it 'resets password if url changed' do + bamboo_service = service + + bamboo_service.bamboo_url = 'http://gitlab1.com' + bamboo_service.save + + expect(bamboo_service.password).to be_nil + end + + it 'does not reset password if username changed' do + bamboo_service = service + + bamboo_service.username = 'some_name' + bamboo_service.save + + expect(bamboo_service.password).to eq('password') + end + + it "does not reset password if new url is set together with password, even if it's the same password" do + bamboo_service = service + + bamboo_service.bamboo_url = 'http://gitlab_edited.com' + bamboo_service.password = 'password' + bamboo_service.save + + expect(bamboo_service.password).to eq('password') + expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com') + end end - it "saves password if new url is set together with password" do - @bamboo_service.bamboo_url = 'http://gitlab_edited.com' - @bamboo_service.password = 'password' - @bamboo_service.save - expect(@bamboo_service.password).to eq("password") - expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com") + it 'saves password if new url is set together with password when no password was previously set' do + bamboo_service = service + bamboo_service.password = nil + + bamboo_service.bamboo_url = 'http://gitlab_edited.com' + bamboo_service.password = 'password' + bamboo_service.save + + expect(bamboo_service.password).to eq('password') + expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com') end + end + end + + describe '#build_page' do + it 'returns a specific URL when status is 500' do + stub_request(status: 500) + + expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo') + end + + it 'returns a specific URL when response has no results' do + stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + + expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo') + end + + it 'returns a build URL when bamboo_url has no trailing slash' do + stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}})) + + expect(service(bamboo_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42') + end + + it 'returns a build URL when bamboo_url has a trailing slash' do + stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}})) + + expect(service(bamboo_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42') + end + end + + describe '#commit_status' do + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) + + expect(service.commit_status('123', 'unused')).to eq(:error) + end + + it 'sets commit status to "pending" when status is 404' do + stub_request(status: 404) + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to "pending" when response has no results' do + stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to "success" when build state contains Success' do + stub_request(build_state: 'YAY Success!') + expect(service.commit_status('123', 'unused')).to eq('success') end + + it 'sets commit status to "failed" when build state contains Failed' do + stub_request(build_state: 'NO Failed!') + + expect(service.commit_status('123', 'unused')).to eq('failed') + end + + it 'sets commit status to "pending" when build state contains Pending' do + stub_request(build_state: 'NO Pending!') + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to :error when build state is unknown' do + stub_request(build_state: 'FOO BAR!') + + expect(service.commit_status('123', 'unused')).to eq(:error) + end + end + + def service(bamboo_url: 'http://gitlab.com') + described_class.create( + project: build_stubbed(:empty_project), + properties: { + bamboo_url: bamboo_url, + username: 'mic', + password: 'password', + build_key: 'foo' + } + ) + end + + def stub_request(status: 200, body: nil, build_state: 'success') + bamboo_full_url = 'http://mic:password@gitlab.com/rest/api/latest/result?label=123&os_authType=basic' + body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}}) + + WebMock.stub_request(:get, bamboo_full_url).to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body + ) end end diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb index 2ccbff553f0198c0399dd24a4e3a4733674281cd..7c23c2efccd8e2f97c57c25e750c98db22bad252 100644 --- a/spec/models/project_services/builds_email_service_spec.rb +++ b/spec/models/project_services/builds_email_service_spec.rb @@ -3,9 +3,10 @@ require 'spec_helper' describe BuildsEmailService do let(:build) { create(:ci_build) } let(:data) { Gitlab::BuildDataBuilder.build(build) } - let(:service) { BuildsEmailService.new } + let!(:project) { create(:project, :public, ci_id: 1) } + let(:service) { described_class.new(project: project, active: true) } - describe :execute do + describe '#execute' do it 'sends email' do service.recipients = 'test@gitlab.com' data[:build_status] = 'failed' @@ -40,4 +41,36 @@ describe BuildsEmailService do service.execute(data) end end + + describe 'validations' do + + context 'when pusher is not added' do + before { service.add_pusher = false } + + it 'does not allow empty recipient input' do + service.recipients = '' + expect(service.valid?).to be false + end + + it 'does allow non-empty recipient input' do + service.recipients = 'test@example.com' + expect(service.valid?).to be true + end + + end + + context 'when pusher is added' do + before { service.add_pusher = true } + + it 'does allow empty recipient input' do + service.recipients = '' + expect(service.valid?).to be true + end + + it 'does allow non-empty recipient input' do + service.recipients = 'test@example.com' + expect(service.valid?).to be true + end + end + end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 91dd92b7c679aa06af80e1c27c365a27ac950039..d878162a220f5072263968ff3d7ac20beb3a9204 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -152,7 +152,7 @@ describe HipchatService, models: true do obj_attr = merge_sample_data[:object_attributes] expect(message).to eq("#{user.name} opened " \ - "<a href=\"#{obj_attr[:url]}\">merge request ##{obj_attr["iid"]}</a> in " \ + "<a href=\"#{obj_attr[:url]}\">merge request !#{obj_attr["iid"]}</a> in " \ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ "<b>Awesome merge request</b>" \ "<pre>please fix</pre>") @@ -202,7 +202,7 @@ describe HipchatService, models: true do title = data[:merge_request]['title'] expect(message).to eq("#{user.name} commented on " \ - "<a href=\"#{obj_attr[:url]}\">merge request ##{merge_id}</a> in " \ + "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ "<b>#{title}</b>" \ "<pre>merge request note</pre>") diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb index dae8bd90922a4416b476b7b6689790cb4ebb23d3..224c7ceabe881867792f93fe00f7b85085415386 100644 --- a/spec/models/project_services/slack_service/merge_message_spec.rb +++ b/spec/models/project_services/slack_service/merge_message_spec.rb @@ -31,7 +31,7 @@ describe SlackService::MergeMessage, models: true do context 'open' do it 'returns a message regarding opening of merge requests' do expect(subject.pretext).to eq( - 'Test User opened <somewhere.com/merge_requests/100|merge request #100> '\ + 'Test User opened <somewhere.com/merge_requests/100|merge request !100> '\ 'in <somewhere.com|project_name>: *Issue title*') expect(subject.attachments).to be_empty end @@ -43,7 +43,7 @@ describe SlackService::MergeMessage, models: true do end it 'returns a message regarding closing of merge requests' do expect(subject.pretext).to eq( - 'Test User closed <somewhere.com/merge_requests/100|merge request #100> '\ + 'Test User closed <somewhere.com/merge_requests/100|merge request !100> '\ 'in <somewhere.com|project_name>: *Issue title*') expect(subject.attachments).to be_empty end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb index 06006b9a4f530d525f28217f30be90550b64a4e1..d37590cab75ea759279b44e888ee0df3efbebaaa 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -63,7 +63,7 @@ describe SlackService::NoteMessage, models: true do it 'returns a message regarding notes on a merge request' do message = SlackService::NoteMessage.new(@args) expect(message.pretext).to eq("Test User commented on " \ - "<url|merge request #30> in <somewhere.com|project_name>: " \ + "<url|merge request !30> in <somewhere.com|project_name>: " \ "*merge request title*") expected_attachments = [ { diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/slack_service/wiki_page_message_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6ecab645b497cc379db9df701f2342a910ad9f17 --- /dev/null +++ b/spec/models/project_services/slack_service/wiki_page_message_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe SlackService::WikiPageMessage, models: true do + subject { described_class.new(args) } + + let(:args) do + { + user: { + name: 'Test User', + username: 'Test User' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + object_attributes: { + title: 'Wiki page title', + url: 'url', + content: 'Wiki page description' + } + } + end + + describe '#pretext' do + context 'when :action == "create"' do + before { args[:object_attributes][:action] = 'create' } + + it 'returns a message that a new wiki page was created' do + expect(subject.pretext).to eq( + 'Test User created <url|wiki page> in <somewhere.com|project_name>: '\ + '*Wiki page title*') + end + end + + context 'when :action == "update"' do + before { args[:object_attributes][:action] = 'update' } + + it 'returns a message that a wiki page was updated' do + expect(subject.pretext).to eq( + 'Test User edited <url|wiki page> in <somewhere.com|project_name>: '\ + '*Wiki page title*') + end + end + end + + describe '#attachments' do + let(:color) { '#345' } + + context 'when :action == "create"' do + before { args[:object_attributes][:action] = 'create' } + + + it 'it returns the attachment for a new wiki page' do + expect(subject.attachments).to eq([ + { + text: "Wiki page description", + color: color, + } + ]) + end + end + + context 'when :action == "update"' do + before { args[:object_attributes][:action] = 'update' } + + it 'it returns the attachment for an updated wiki page' do + expect(subject.attachments).to eq([ + { + text: "Wiki page description", + color: color, + } + ]) + end + end + end +end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index a9e0afad90fc190f28abcb6de5645710c16f4517..478d59be08ba867fd96c7fd11e1c6f1ce5ffb99d 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -75,6 +75,17 @@ describe SlackService, models: true do @merge_request = merge_service.execute @merge_sample_data = merge_service.hook_data(@merge_request, 'open') + + opts = { + title: "Awesome wiki_page", + content: "Some text describing some thing or another", + format: "md", + message: "user created page: Awesome wiki_page" + } + + wiki_page_service = WikiPages::CreateService.new(project, user, opts) + @wiki_page = wiki_page_service.execute + @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') end it "should call Slack API for push events" do @@ -95,6 +106,12 @@ describe SlackService, models: true do expect(WebMock).to have_requested(:post, webhook_url).once end + it "should call Slack API for wiki page events" do + slack.execute(@wiki_page_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + it 'should use the username as an option for slack when configured' do allow(slack).to receive(:username).and_return(username) expect(Slack::Notifier).to receive(:new). diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index f26b47a856c5058091ba31184b1af6ca1ef2853f..bc7423cee6986a39f9cd1d5103799b0ecf3484fe 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -21,73 +21,220 @@ require 'spec_helper' describe TeamcityService, models: true do - describe "Associations" do + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } end - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project) } - - context "when a password was previously set" do - before do - @teamcity_service = TeamcityService.create( - project: create(:project), - properties: { - teamcity_url: 'http://gitlab.com', - username: 'mic', - password: "password" - } - ) + describe 'Validations' do + describe '#teamcity_url' do + it 'does not validate the presence of teamcity_url if service is not active' do + teamcity_service = service + teamcity_service.active = false + + expect(teamcity_service).not_to validate_presence_of(:teamcity_url) end - - it "reset password if url changed" do - @teamcity_service.teamcity_url = 'http://gitlab1.com' - @teamcity_service.save - expect(@teamcity_service.password).to be_nil + + it 'validates the presence of teamcity_url if service is active' do + teamcity_service = service + teamcity_service.active = true + + expect(teamcity_service).to validate_presence_of(:teamcity_url) + end + end + + describe '#build_type' do + it 'does not validate the presence of build_type if service is not active' do + teamcity_service = service + teamcity_service.active = false + + expect(teamcity_service).not_to validate_presence_of(:build_type) + end + + it 'validates the presence of build_type if service is active' do + teamcity_service = service + teamcity_service.active = true + + expect(teamcity_service).to validate_presence_of(:build_type) end - - it "does not reset password if username changed" do - @teamcity_service.username = "some_name" - @teamcity_service.save - expect(@teamcity_service.password).to eq("password") + end + + describe '#username' do + it 'does not validate the presence of username if service is not active' do + teamcity_service = service + teamcity_service.active = false + + expect(teamcity_service).not_to validate_presence_of(:username) end - it "does not reset password if new url is set together with password, even if it's the same password" do - @teamcity_service.teamcity_url = 'http://gitlab_edited.com' - @teamcity_service.password = 'password' - @teamcity_service.save - expect(@teamcity_service.password).to eq("password") - expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com") + it 'does not validate the presence of username if username is nil' do + teamcity_service = service + teamcity_service.active = true + teamcity_service.password = nil + + expect(teamcity_service).not_to validate_presence_of(:username) end - it "should reset password if url changed, even if setter called multiple times" do - @teamcity_service.teamcity_url = 'http://gitlab1.com' - @teamcity_service.teamcity_url = 'http://gitlab1.com' - @teamcity_service.save - expect(@teamcity_service.password).to be_nil + it 'validates the presence of username if service is active and username is present' do + teamcity_service = service + teamcity_service.active = true + teamcity_service.password = 'secret' + + expect(teamcity_service).to validate_presence_of(:username) end end - - context "when no password was previously set" do - before do - @teamcity_service = TeamcityService.create( - project: create(:project), - properties: { - teamcity_url: 'http://gitlab.com', - username: 'mic' - } - ) + + describe '#password' do + it 'does not validate the presence of password if service is not active' do + teamcity_service = service + teamcity_service.active = false + + expect(teamcity_service).not_to validate_presence_of(:password) + end + + it 'does not validate the presence of password if username is nil' do + teamcity_service = service + teamcity_service.active = true + teamcity_service.username = nil + + expect(teamcity_service).not_to validate_presence_of(:password) end - it "saves password if new url is set together with password" do - @teamcity_service.teamcity_url = 'http://gitlab_edited.com' - @teamcity_service.password = 'password' - @teamcity_service.save - expect(@teamcity_service.password).to eq("password") - expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com") + it 'validates the presence of password if service is active and username is present' do + teamcity_service = service + teamcity_service.active = true + teamcity_service.username = 'john' + + expect(teamcity_service).to validate_presence_of(:password) end end end + + describe 'Callbacks' do + describe 'before_update :reset_password' do + context 'when a password was previously set' do + it 'resets password if url changed' do + teamcity_service = service + + teamcity_service.teamcity_url = 'http://gitlab1.com' + teamcity_service.save + + expect(teamcity_service.password).to be_nil + end + + it 'does not reset password if username changed' do + teamcity_service = service + + teamcity_service.username = 'some_name' + teamcity_service.save + + expect(teamcity_service.password).to eq('password') + end + + it "does not reset password if new url is set together with password, even if it's the same password" do + teamcity_service = service + + teamcity_service.teamcity_url = 'http://gitlab_edited.com' + teamcity_service.password = 'password' + teamcity_service.save + + expect(teamcity_service.password).to eq('password') + expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com') + end + end + + it 'saves password if new url is set together with password when no password was previously set' do + teamcity_service = service + teamcity_service.password = nil + + teamcity_service.teamcity_url = 'http://gitlab_edited.com' + teamcity_service.password = 'password' + teamcity_service.save + + expect(teamcity_service.password).to eq('password') + expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com') + end + end + end + + describe '#build_page' do + it 'returns a specific URL when status is 500' do + stub_request(status: 500) + + expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildTypeId=foo') + end + + it 'returns a build URL when teamcity_url has no trailing slash' do + stub_request(body: %Q({"build":{"id":"666"}})) + + expect(service(teamcity_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo') + end + + it 'returns a build URL when teamcity_url has a trailing slash' do + stub_request(body: %Q({"build":{"id":"666"}})) + + expect(service(teamcity_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo') + end + end + + describe '#commit_status' do + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) + + expect(service.commit_status('123', 'unused')).to eq(:error) + end + + it 'sets commit status to "pending" when status is 404' do + stub_request(status: 404) + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to "success" when build status contains SUCCESS' do + stub_request(build_status: 'YAY SUCCESS!') + + expect(service.commit_status('123', 'unused')).to eq('success') + end + + it 'sets commit status to "failed" when build status contains FAILURE' do + stub_request(build_status: 'NO FAILURE!') + + expect(service.commit_status('123', 'unused')).to eq('failed') + end + + it 'sets commit status to "pending" when build status contains Pending' do + stub_request(build_status: 'NO Pending!') + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to :error when build status is unknown' do + stub_request(build_status: 'FOO BAR!') + + expect(service.commit_status('123', 'unused')).to eq(:error) + end + end + + def service(teamcity_url: 'http://gitlab.com') + described_class.create( + project: build_stubbed(:empty_project), + properties: { + teamcity_url: teamcity_url, + username: 'mic', + password: 'password', + build_type: 'foo' + } + ) + end + + def stub_request(status: 200, body: nil, build_status: 'success') + teamcity_full_url = 'http://mic:password@gitlab.com/httpAuth/app/rest/builds/branch:unspecified:any,number:123' + body ||= %Q({"build":{"status":"#{build_status}","id":"666"}}) + + WebMock.stub_request(:get, teamcity_full_url).to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body + ) + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f29c389e0947c8709dd2f45f355449a2d1b501cd..becc743de3113e8baa2a7842fefdfbe4f481c8da 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -441,9 +441,22 @@ describe Project, models: true do describe :ci_commit do let(:project) { create :project } - let(:commit) { create :ci_commit, project: project } + let(:commit) { create :ci_commit, project: project, ref: 'master' } - it { expect(project.ci_commit(commit.sha)).to eq(commit) } + subject { project.ci_commit(commit.sha, 'master') } + + it { is_expected.to eq(commit) } + + context 'return latest' do + let(:commit2) { create :ci_commit, project: project, ref: 'master' } + + before do + commit + commit2 + end + + it { is_expected.to eq(commit2) } + end end describe :builds_enabled do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 4e49c413f2306bdcfeae1852b8f2b8122463ada6..b561aa663d1da757f074ecf7e079eef6a0b15f2b 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -94,6 +94,12 @@ describe Repository, models: true do it { is_expected.to be_an Array } + it 'regex-escapes the query string' do + results = repository.search_files("test\\", 'master') + + expect(results.first).not_to start_with('fatal:') + end + describe 'result' do subject { results.first } @@ -129,22 +135,69 @@ describe Repository, models: true do end - describe "#license" do + describe '#license_blob' do before do - repository.send(:cache).expire(:license) + repository.send(:cache).expire(:license_blob) + repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') end - it 'test selection preference' do - files = [TestBlob.new('file'), TestBlob.new('license'), TestBlob.new('copying')] - expect(repository.tree).to receive(:blobs).and_return(files) + it 'looks in the root_ref only' do + repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown') + repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false) + + expect(repository.license_blob).to be_nil + end + + it 'favors license file with no extension' do + repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false) + repository.commit_file(user, 'LICENSE.md', Licensee::License.new('mit').content, 'Add LICENSE.md', 'master', false) + + expect(repository.license_blob.name).to eq('LICENSE') + end + + it 'favors .md file to .txt' do + repository.commit_file(user, 'LICENSE.md', Licensee::License.new('mit').content, 'Add LICENSE.md', 'master', false) + repository.commit_file(user, 'LICENSE.txt', Licensee::License.new('mit').content, 'Add LICENSE.txt', 'master', false) + + expect(repository.license_blob.name).to eq('LICENSE.md') + end + + it 'favors LICENCE to LICENSE' do + repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false) + repository.commit_file(user, 'LICENCE', Licensee::License.new('mit').content, 'Add LICENCE', 'master', false) + + expect(repository.license_blob.name).to eq('LICENCE') + end - expect(repository.license.name).to eq('license') + it 'favors LICENSE to COPYING' do + repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false) + repository.commit_file(user, 'COPYING', Licensee::License.new('mit').content, 'Add COPYING', 'master', false) + + expect(repository.license_blob.name).to eq('LICENSE') end - it 'also accepts licence instead of license' do - expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('licence')]) + it 'favors LICENCE to COPYING' do + repository.commit_file(user, 'LICENCE', Licensee::License.new('mit').content, 'Add LICENCE', 'master', false) + repository.commit_file(user, 'COPYING', Licensee::License.new('mit').content, 'Add COPYING', 'master', false) - expect(repository.license.name).to eq('licence') + expect(repository.license_blob.name).to eq('LICENCE') + end + end + + describe '#license_key' do + before do + repository.send(:cache).expire(:license_key) + repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') + end + + it 'returns "no-license" when no license is detected' do + expect(repository.license_key).to eq('no-license') + end + + it 'returns the license key' do + repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false) + + expect(repository.license_key).to eq('mit') end end @@ -393,6 +446,8 @@ describe Repository, models: true do describe '#expire_cache' do it 'expires all caches' do expect(repository).to receive(:expire_branch_cache) + expect(repository).to receive(:expire_branch_count_cache) + expect(repository).to receive(:expire_tag_count_cache) repository.expire_cache end @@ -533,6 +588,41 @@ describe Repository, models: true do end end + describe '#cherry_pick' do + let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') } + let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } + let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') } + + context 'when there is a conflict' do + it 'should abort the operation' do + expect(repository.cherry_pick(user, conflict_commit, 'master')).to eq(false) + end + end + + context 'when commit was already cherry-picked' do + it 'should abort the operation' do + repository.cherry_pick(user, pickable_commit, 'master') + + expect(repository.cherry_pick(user, pickable_commit, 'master')).to eq(false) + end + end + + context 'when commit can be cherry-picked' do + it 'should cherry-pick the changes' do + expect(repository.cherry_pick(user, pickable_commit, 'master')).to be_truthy + end + end + + context 'cherry-picking a merge commit' do + it 'should cherry-pick the changes' do + expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).to be_nil + + repository.cherry_pick(user, pickable_merge, 'master') + expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).not_to be_nil + end + end + end + describe '#before_delete' do describe 'when a repository does not exist' do before do @@ -762,11 +852,9 @@ describe Repository, models: true do describe '#rm_tag' do it 'removes a tag' do expect(repository).to receive(:before_remove_tag) + expect(repository.rugged.tags).to receive(:delete).with('v1.1.0') - expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag). - with(repository.path_with_namespace, '8.5') - - repository.rm_tag('8.5') + repository.rm_tag('v1.1.0') end end @@ -904,9 +992,32 @@ describe Repository, models: true do end end + describe '.clean_old_archives' do + let(:path) { Gitlab.config.gitlab.repository_downloads_path } + + context 'when the downloads directory does not exist' do + it 'does not remove any archives' do + expect(File).to receive(:directory?).with(path).and_return(false) + + expect(Gitlab::Popen).not_to receive(:popen) + + described_class.clean_old_archives + end + end + + context 'when the downloads directory exists' do + it 'removes old archives' do + expect(File).to receive(:directory?).with(path).and_return(true) + + expect(Gitlab::Popen).to receive(:popen) + + described_class.clean_old_archives + end + end + end + def create_remote_branch(remote_name, branch_name, target) rugged = repository.rugged rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target) end - end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 967c34800d00cb174cc5169151fe4bbe3fd06cb1..5ead735be48aa29a02af52aa2be114ffadcdda1e 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -59,7 +59,7 @@ describe API::API, api: true do describe 'GET /projects/:id/repository/commits/:sha/builds' do before do - project.ensure_ci_commit(commit.sha) + project.ensure_ci_commit(commit.sha, 'master') get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", api_user) end diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb index 429a24109fdbbcf9891ce6c640e6e4363baf3a95..f3785b19362c889b27e366f72418dd7375ff844a 100644 --- a/spec/requests/api/commit_status_spec.rb +++ b/spec/requests/api/commit_status_spec.rb @@ -16,7 +16,8 @@ describe API::CommitStatus, api: true do let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } context 'ci commit exists' do - let!(:ci_commit) { project.ensure_ci_commit(commit.id) } + let!(:master) { project.ci_commits.create(sha: commit.id, ref: 'master') } + let!(:develop) { project.ci_commits.create(sha: commit.id, ref: 'develop') } it_behaves_like 'a paginated resources' do let(:request) { get api(get_url, reporter) } @@ -25,16 +26,16 @@ describe API::CommitStatus, api: true do context "reporter user" do let(:statuses_id) { json_response.map { |status| status['id'] } } - def create_status(opts = {}) - create(:commit_status, { commit: ci_commit }.merge(opts)) + def create_status(commit, opts = {}) + create(:commit_status, { commit: commit, ref: commit.ref }.merge(opts)) end - let!(:status1) { create_status(status: 'running') } - let!(:status2) { create_status(name: 'coverage', status: 'pending') } - let!(:status3) { create_status(ref: 'develop', status: 'running', allow_failure: true) } - let!(:status4) { create_status(name: 'coverage', status: 'success') } - let!(:status5) { create_status(name: 'coverage', ref: 'develop', status: 'success') } - let!(:status6) { create_status(status: 'success') } + let!(:status1) { create_status(master, status: 'running') } + let!(:status2) { create_status(master, name: 'coverage', status: 'pending') } + let!(:status3) { create_status(develop, status: 'running', allow_failure: true) } + let!(:status4) { create_status(master, name: 'coverage', status: 'success') } + let!(:status5) { create_status(develop, name: 'coverage', status: 'success') } + let!(:status6) { create_status(master, status: 'success') } context 'latest commit statuses' do before { get api(get_url, reporter) } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 7ff21175c1bad05ca8420474157b77da536fe2de..e28998d51b5d41011bf30a43cbb1d87782cb92d0 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -48,14 +48,14 @@ describe API::API, api: true do expect(response.status).to eq(404) end - it "should return not_found for CI status" do + it "should return nil for commit without CI" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) expect(response.status).to eq(200) - expect(json_response['status']).to eq('not_found') + expect(json_response['status']).to be_nil end it "should return status for CI" do - ci_commit = project.ensure_ci_commit(project.repository.commit.sha) + ci_commit = project.ensure_ci_commit(project.repository.commit.sha, 'master') get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) expect(response.status).to eq(200) expect(json_response['status']).to eq(ci_commit.status) diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb index 3e8b4aa1f88d455f9d9b9ce164803d4720ea1a38..96d89e69209cd91c100ddbd25b2ccede1b7dd86a 100644 --- a/spec/requests/api/group_members_spec.rb +++ b/spec/requests/api/group_members_spec.rb @@ -42,9 +42,10 @@ describe API::API, api: true do end end - it "users not part of the group should get access error" do + it 'users not part of the group should get access error' do get api("/groups/#{group_with_members.id}/members", stranger) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end end @@ -165,12 +166,13 @@ describe API::API, api: true do end end - describe "DELETE /groups/:id/members/:user_id" do - context "when not a member of the group" do + describe 'DELETE /groups/:id/members/:user_id' do + context 'when not a member of the group' do it "should not delete guest's membership of group_with_members" do random_user = create(:user) delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 41c9cacd455d051044cde40db64db8e16ebdb793..37ddab83c30eea4403d5a9aa18da3b96cad16418 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -61,7 +61,8 @@ describe API::API, api: true do it "should not return a group not attached to user1" do get api("/groups/#{group2.id}", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end @@ -92,9 +93,54 @@ describe API::API, api: true do it 'should not return a group not attached to user1' do get api("/groups/#{group2.path}", user1) + + expect(response.status).to eq(404) + end + end + end + + describe 'PUT /groups/:id' do + let(:new_group_name) { 'New Group'} + + context 'when authenticated as the group owner' do + it 'updates the group' do + put api("/groups/#{group1.id}", user1), name: new_group_name + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(new_group_name) + end + + it 'returns 404 for a non existing group' do + put api('/groups/1328', user1) + + expect(response.status).to eq(404) + end + end + + context 'when authenticated as the admin' do + it 'updates the group' do + put api("/groups/#{group1.id}", admin), name: new_group_name + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(new_group_name) + end + end + + context 'when authenticated as an user that can see the group' do + it 'does not updates the group' do + put api("/groups/#{group1.id}", user2), name: new_group_name + expect(response.status).to eq(403) end end + + context 'when authenticated as an user that cannot see the group' do + it 'returns 404 when trying to update the group' do + put api("/groups/#{group2.id}", user1), name: new_group_name + + expect(response.status).to eq(404) + end + end end describe "GET /groups/:id/projects" do @@ -113,7 +159,8 @@ describe API::API, api: true do it "should not return a group not attached to user1" do get api("/groups/#{group2.id}/projects", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end @@ -145,7 +192,8 @@ describe API::API, api: true do it 'should not return a group not attached to user1' do get api("/groups/#{group2.path}/projects", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end end @@ -203,7 +251,8 @@ describe API::API, api: true do it "should not remove a group not attached to user1" do delete api("/groups/#{group2.id}", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 822d3ad30174e889a9ac521585f10148dfc596ab..f88e39cad9eeab0e43fdf6dd75bc641f3ae7edfe 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -3,11 +3,12 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } + let(:user2) { create(:user) } let(:non_member) { create(:user) } let(:author) { create(:author) } let(:assignee) { create(:assignee) } let(:admin) { create(:user, :admin) } - let!(:project) { create(:project, :public, namespace: user.namespace ) } + let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } let!(:closed_issue) do create :closed_issue, author: user, @@ -320,13 +321,13 @@ describe API::API, api: true do end context 'when an admin or owner makes the request' do - it "accepts the creation date to be set" do + it 'accepts the creation date to be set' do + creation_time = 2.weeks.ago post api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: 'label, label2', created_at: 2.weeks.ago + title: 'new issue', labels: 'label, label2', created_at: creation_time expect(response.status).to eq(201) - # this take about a second, so probably not equal - expect(Time.parse(json_response['created_at'])).to be <= 2.weeks.ago + expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time) end end end @@ -477,6 +478,18 @@ describe API::API, api: true do expect(json_response['labels']).to include 'label2' expect(json_response['state']).to eq "closed" end + + context 'when an admin or owner makes the request' do + it 'accepts the update date to be set' do + update_time = 2.weeks.ago + put api("/projects/#{project.id}/issues/#{issue.id}", user), + labels: 'label3', state_event: 'close', updated_at: update_time + expect(response.status).to eq(200) + + expect(json_response['labels']).to include 'label3' + expect(Time.parse(json_response['updated_at'])).to be_within(1.second).of(update_time) + end + end end describe "DELETE /projects/:id/issues/:issue_id" do @@ -501,4 +514,114 @@ describe API::API, api: true do end end end + + describe '/projects/:id/issues/:issue_id/move' do + let!(:target_project) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace ) } + let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) } + + it 'moves an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project.id + + expect(response.status).to eq(201) + expect(json_response['project_id']).to eq(target_project.id) + end + + context 'when source and target projects are the same' do + it 'returns 400 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: project.id + + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Cannot move issue to project it originates from!') + end + end + + context 'when the user does not have the permission to move issues' do + it 'returns 400 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project2.id + + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!') + end + end + + it 'moves the issue to another namespace if I am admin' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", admin), + to_project_id: target_project2.id + + expect(response.status).to eq(201) + expect(json_response['project_id']).to eq(target_project2.id) + end + + context 'when issue does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/123/move", user), + to_project_id: target_project.id + + expect(response.status).to eq(404) + end + end + + context 'when source project does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/123/issues/#{issue.id}/move", user), + to_project_id: target_project.id + + expect(response.status).to eq(404) + end + end + + context 'when target project does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: 123 + + expect(response.status).to eq(404) + end + end + end + + describe 'POST :id/issues/:issue_id/subscription' do + it 'subscribes to an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) + + expect(response.status).to eq(201) + expect(json_response['subscribed']).to eq(true) + end + + it 'returns 304 if already subscribed' do + post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) + + expect(response.status).to eq(304) + end + + it 'returns 404 if the issue is not found' do + post api("/projects/#{project.id}/issues/123/subscription", user) + + expect(response.status).to eq(404) + end + end + + describe 'DELETE :id/issues/:issue_id/subscription' do + it 'unsubscribes from an issue' do + delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) + + expect(response.status).to eq(200) + expect(json_response['subscribed']).to eq(false) + end + + it 'returns 304 if not subscribed' do + delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) + + expect(response.status).to eq(304) + end + + it 'returns 404 if the issue is not found' do + delete api("/projects/#{project.id}/issues/123/subscription", user) + + expect(response.status).to eq(404) + end + end end diff --git a/spec/requests/api/licenses_spec.rb b/spec/requests/api/licenses_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c17dcb222a9d36b5bdb8d0319c945d46f53fc424 --- /dev/null +++ b/spec/requests/api/licenses_spec.rb @@ -0,0 +1,136 @@ +require 'spec_helper' + +describe API::Licenses, api: true do + include ApiHelpers + + describe 'Entity' do + before { get api('/licenses/mit') } + + it { expect(json_response['key']).to eq('mit') } + it { expect(json_response['name']).to eq('MIT License') } + it { expect(json_response['nickname']).to be_nil } + it { expect(json_response['popular']).to be true } + it { expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') } + it { expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') } + it { expect(json_response['description']).to include('A permissive license that is short and to the point.') } + it { expect(json_response['conditions']).to eq(%w[include-copyright]) } + it { expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) } + it { expect(json_response['limitations']).to eq(%w[no-liability]) } + it { expect(json_response['content']).to include('The MIT License (MIT)') } + end + + describe 'GET /licenses' do + it 'returns a list of available license templates' do + get api('/licenses') + + expect(response.status).to eq(200) + 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') + end + + describe 'the popular parameter' do + context 'with popular=1' do + it 'returns a list of available popular license templates' do + get api('/licenses?popular=1') + + expect(response.status).to eq(200) + 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') + end + end + end + end + + describe 'GET /licenses/:key' do + context 'with :project and :fullname given' do + before do + get api("/licenses/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}") + end + + context 'for the mit license' do + let(:license_type) { 'mit' } + + it 'returns the license text' do + expect(json_response['content']).to include('The MIT License (MIT)') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include('Copyright (c) 2016 Anton') + end + end + + context 'for the agpl-3.0 license' do + let(:license_type) { 'agpl-3.0' } + + it 'returns the license text' do + expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include('My Awesome Project') + expect(json_response['content']).to include('Copyright (C) 2016 Anton') + end + end + + context 'for the gpl-3.0 license' do + let(:license_type) { 'gpl-3.0' } + + it 'returns the license text' do + expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include('My Awesome Project') + expect(json_response['content']).to include('Copyright (C) 2016 Anton') + end + end + + context 'for the gpl-2.0 license' do + let(:license_type) { 'gpl-2.0' } + + it 'returns the license text' do + expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include('My Awesome Project') + expect(json_response['content']).to include('Copyright (C) 2016 Anton') + end + end + + context 'for the apache-2.0 license' do + let(:license_type) { 'apache-2.0' } + + it 'returns the license text' do + expect(json_response['content']).to include('Apache License') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include('Copyright 2016 Anton') + end + end + + context 'for an uknown license' do + let(:license_type) { 'muth-over9000' } + + it 'returns a 404' do + expect(response.status).to eq(404) + end + end + end + + context 'with no :fullname given' do + context 'with an authenticated user' do + let(:user) { create(:user) } + + it 'replaces the copyright owner placeholder with the name of the current user' do + get api('/licenses/mit', user) + + expect(json_response['content']).to include("Copyright (c) 2016 #{user.name}") + end + end + end + end +end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 25fa30b2f21d1cb1ac552186e59f8a67ecbf3088..1fa7e76894fb17a3ca33b08ded7b22c2fa419e18 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -516,6 +516,48 @@ describe API::API, api: true do end end + describe 'POST :id/merge_requests/:merge_request_id/subscription' do + it 'subscribes to a merge request' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) + + expect(response.status).to eq(201) + expect(json_response['subscribed']).to eq(true) + end + + it 'returns 304 if already subscribed' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) + + expect(response.status).to eq(304) + end + + it 'returns 404 if the merge request is not found' do + post api("/projects/#{project.id}/merge_requests/123/subscription", user) + + expect(response.status).to eq(404) + end + end + + describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do + it 'unsubscribes from a merge request' do + delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) + + expect(response.status).to eq(200) + expect(json_response['subscribed']).to eq(false) + end + + it 'returns 304 if not subscribed' do + delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) + + expect(response.status).to eq(304) + end + + it 'returns 404 if the merge request is not found' do + post api("/projects/#{project.id}/merge_requests/123/subscription", user) + + expect(response.status).to eq(404) + end + end + def mr_with_later_created_and_updated_at_time merge_request merge_request.created_at += 1.hour diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index a467bc935afbe536c943d4ca85c483f7e1256fed..ec9eda0a2edd29966c9766eae2ce3d4a95c295a1 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -158,6 +158,19 @@ describe API::API, api: true do post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' expect(response.status).to eq(401) end + + context 'when an admin or owner makes the request' do + it 'accepts the creation date to be set' do + creation_time = 2.weeks.ago + post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + body: 'hi!', created_at: creation_time + expect(response.status).to eq(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) + expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time) + end + end + end context "when noteable is a Snippet" do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index be2034e0f399118cac3f8c9d814106f02a83d670..fccd08bd6dac024f62463525b277d72591ec73f2 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1020,6 +1020,54 @@ describe API::API, api: true do end end + describe 'POST /projects/:id/star' do + context 'on an unstarred project' do + it 'stars the project' do + expect { post api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1) + + expect(response.status).to eq(201) + expect(json_response['star_count']).to eq(1) + end + end + + context 'on a starred project' do + before do + user.toggle_star(project) + project.reload + end + + it 'does not modify the star count' do + expect { post api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + + expect(response.status).to eq(304) + end + end + end + + describe 'DELETE /projects/:id/star' do + context 'on a starred project' do + before do + user.toggle_star(project) + project.reload + end + + it 'unstars the project' do + expect { delete api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) + + expect(response.status).to eq(200) + 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(response.status).to eq(304) + end + end + end + describe 'DELETE /projects/:id' do context 'when authenticated as user' do it 'should remove project' do diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 9f9c3b1cf4c7414c62e2e9e61c379b6c6ff3c328..edcb2bedbf793131442a0b26070b5582397939c9 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -32,9 +32,11 @@ describe API::API, api: true do it "should return an array of project tags with release info" do get api("/projects/#{project.id}/repository/tags", user) + expect(response.status).to eq(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 diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 679227bf8815f7aa06e0deb14b73355f3a7e225f..40b24c125b54e4410792c6e1196578574a622b37 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -20,6 +20,24 @@ describe API::API, api: true do end context "when authenticated" do + #These specs are written just in case API authentication is not required anymore + context "when public level is restricted" do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + allow_any_instance_of(API::Helpers).to receive(:authenticate!).and_return(true) + end + + it "renders 403" do + get api("/users") + expect(response.status).to eq(403) + end + + it "renders 404" do + get api("/users/#{user.id}") + expect(response.status).to eq(404) + end + end + it "should return an array of users" do get api("/users", user) expect(response.status).to eq(200) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 57d7eb927fdfa6e71ae28d4765093b81ccb748b7..dfd361a2cdd27d8a45887ae98798778320980623 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -20,8 +20,8 @@ describe Ci::API::API do describe "POST /builds/register" do it "should start a build" do - commit = FactoryGirl.create(:ci_commit, project: project) - commit.create_builds('master', false, nil) + commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') + commit.create_builds(nil) build = commit.builds.first post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -56,8 +56,8 @@ describe Ci::API::API do end it "returns options" do - commit = FactoryGirl.create(:ci_commit, project: project) - commit.create_builds('master', false, nil) + commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') + commit.create_builds(nil) post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -66,8 +66,8 @@ describe Ci::API::API do end it "returns variables" do - commit = FactoryGirl.create(:ci_commit, project: project) - commit.create_builds('master', false, nil) + commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') + commit.create_builds(nil) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -83,10 +83,10 @@ describe Ci::API::API do it "returns variables for triggers" do trigger = FactoryGirl.create(:ci_trigger, project: project) - commit = FactoryGirl.create(:ci_commit, project: project) + commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) - commit.create_builds('master', false, nil, trigger_request) + commit.create_builds(nil, trigger_request) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -103,8 +103,8 @@ describe Ci::API::API do end it "returns dependent builds" do - commit = FactoryGirl.create(:ci_commit, project: project) - commit.create_builds('master', false, nil, nil) + commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') + commit.create_builds(nil, nil) commit.builds.where(stage: 'test').each(&:success) post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -156,6 +156,52 @@ describe Ci::API::API do end end + describe 'PATCH /builds/:id/trace.txt' do + let(:build) { create(:ci_build, :trace, runner_id: runner.id) } + let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } } + let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } + + before do + build.run! + patch ci_api("/builds/#{build.id}/trace.txt"), ' appended', headers_with_range + end + + context 'when request is valid' do + it { expect(response.status).to eq 202 } + it { expect(build.reload.trace).to eq 'BUILD TRACE appended' } + it { expect(response.header).to have_key 'Range' } + it { expect(response.header).to have_key 'Build-Status' } + end + + context 'when content-range start is too big' do + let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) } + + it { expect(response.status).to eq 416 } + it { expect(response.header).to have_key 'Range' } + it { expect(response.header['Range']).to eq '0-11' } + end + + context 'when content-range start is too small' do + let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) } + + it { expect(response.status).to eq 416 } + it { expect(response.header).to have_key 'Range' } + it { expect(response.header['Range']).to eq '0-11' } + end + + context 'when Content-Range header is missing' do + let(:headers_with_range) { headers.merge({}) } + + it { expect(response.status).to eq 400 } + end + + context 'when build has been errased' do + let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } + + it { expect(response.status).to eq 403 } + end + end + context "Artifacts" do let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb index 1fca362868601e4b1cd6375dd63358ce5d1f0aa9..ecc3a88a2621ea215f686867fc6b46b0316bc891 100644 --- a/spec/services/ci/create_builds_service_spec.rb +++ b/spec/services/ci/create_builds_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::CreateBuildsService, services: true do - let(:commit) { create(:ci_commit) } + let(:commit) { create(:ci_commit, ref: 'master') } let(:user) { create(:user) } describe '#execute' do @@ -9,7 +9,7 @@ describe Ci::CreateBuildsService, services: true do # subject do - described_class.new.execute(commit, 'test', 'master', nil, user, nil, status) + described_class.new(commit).execute(commit, nil, user, status) end context 'next builds available' do diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index 870861ad20a8cade2758bffe0a7386c2ae159eb3..4cc4b3870d1943e3dcd8a2c4686315ce07ee505f 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -5,7 +5,7 @@ module Ci let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } - let(:commit) { project.ensure_ci_commit(commit_sha) } + let(:commit) { project.ensure_ci_commit(commit_sha, 'master') } let(:build) { FactoryGirl.create(:ci_build, commit: commit) } describe :execute do diff --git a/spec/services/delete_tag_service_spec.rb b/spec/services/delete_tag_service_spec.rb index 5b7ba5218123f87c30e54f343451179cce605add..477551f5036c8344a607518777df5bc4a802f8c8 100644 --- a/spec/services/delete_tag_service_spec.rb +++ b/spec/services/delete_tag_service_spec.rb @@ -6,21 +6,12 @@ describe DeleteTagService, services: true do let(:user) { create(:user) } let(:service) { described_class.new(project, user) } - let(:tag) { double(:tag, name: '8.5', target: 'abc123') } - describe '#execute' do - before do - allow(repository).to receive(:find_tag).and_return(tag) - end - it 'removes the tag' do - expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag). - and_return(true) - expect(repository).to receive(:before_remove_tag) expect(service).to receive(:success) - service.execute('8.5') + service.execute('v1.1.0') end end end diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index cc780587e74a01b54ad5b67b4ff15016abdf01ca..a63656e626801a040eaad3253a1f82bf6d38b7ce 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -5,19 +5,17 @@ describe GitTagPushService, services: true do let(:user) { create :user } let(:project) { create :project } - let(:service) { GitTagPushService.new } + let(:service) { GitTagPushService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) } - before do - @oldrev = Gitlab::Git::BLANK_SHA - @newrev = "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" # gitlab-test: git rev-parse refs/tags/v1.1.0 - @ref = 'refs/tags/v1.1.0' - end + let(:oldrev) { Gitlab::Git::BLANK_SHA } + let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0 + let(:ref) { 'refs/tags/v1.1.0' } describe "Git Tag Push Data" do before do - service.execute(project, user, @oldrev, @newrev, @ref) + service.execute @push_data = service.push_data - @tag_name = Gitlab::Git.ref_name(@ref) + @tag_name = Gitlab::Git.ref_name(ref) @tag = project.repository.find_tag(@tag_name) @commit = project.commit(@tag.target) end @@ -25,9 +23,9 @@ describe GitTagPushService, services: true do subject { @push_data } it { is_expected.to include(object_kind: 'tag_push') } - it { is_expected.to include(ref: @ref) } - it { is_expected.to include(before: @oldrev) } - it { is_expected.to include(after: @newrev) } + it { is_expected.to include(ref: ref) } + it { is_expected.to include(before: oldrev) } + it { is_expected.to include(after: newrev) } it { is_expected.to include(message: @tag.message) } it { is_expected.to include(user_id: user.id) } it { is_expected.to include(user_name: user.name) } @@ -80,9 +78,11 @@ describe GitTagPushService, services: true do describe "Webhooks" do context "execute webhooks" do + let(:service) { GitTagPushService.new(project, user, oldrev: 'oldrev', newrev: 'newrev', ref: 'refs/tags/v1.0.0') } + it "when pushing tags" do expect(project).to receive(:execute_hooks) - service.execute(project, user, 'oldrev', 'newrev', 'refs/tags/v1.0.0') + service.execute end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 0f2aa3ae73ccd959b29abf3b14ee4c8638a60e17..d7c72dc08118672c76097fdaa79e59ad27b7330f 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -88,12 +88,9 @@ describe NotificationService, services: true do note.project.namespace_id = group.id note.project.group.add_user(@u_watcher, GroupMember::MASTER) note.project.save - user_project = note.project.project_members.find_by_user_id(@u_watcher.id) - user_project.notification_level = Notification::N_PARTICIPATING - user_project.save - group_member = note.project.group.group_members.find_by_user_id(@u_watcher.id) - group_member.notification_level = Notification::N_GLOBAL - group_member.save + + @u_watcher.notification_settings_for(note.project).participating! + @u_watcher.notification_settings_for(note.project.group).global! ActionMailer::Base.deliveries.clear end @@ -215,7 +212,7 @@ describe NotificationService, services: true do end it do - @u_committer.update_attributes(notification_level: Notification::N_MENTION) + @u_committer.update_attributes(notification_level: :mention) notification.new_note(note) should_not_email(@u_committer) end @@ -246,7 +243,7 @@ describe NotificationService, services: true do end it do - issue.assignee.update_attributes(notification_level: Notification::N_MENTION) + issue.assignee.update_attributes(notification_level: :mention) notification.new_issue(issue, @u_disabled) should_not_email(issue.assignee) @@ -596,13 +593,13 @@ describe NotificationService, services: true do end def build_team(project) - @u_watcher = create(:user, notification_level: Notification::N_WATCH) - @u_participating = create(:user, notification_level: Notification::N_PARTICIPATING) - @u_participant_mentioned = create(:user, username: 'participant', notification_level: Notification::N_PARTICIPATING) - @u_disabled = create(:user, notification_level: Notification::N_DISABLED) - @u_mentioned = create(:user, username: 'mention', notification_level: Notification::N_MENTION) + @u_watcher = create(:user, notification_level: :watch) + @u_participating = create(:user, notification_level: :participating) + @u_participant_mentioned = create(:user, username: 'participant', notification_level: :participating) + @u_disabled = create(:user, notification_level: :disabled) + @u_mentioned = create(:user, username: 'mention', notification_level: :mention) @u_committer = create(:user, username: 'committer') - @u_not_mentioned = create(:user, username: 'regular', notification_level: Notification::N_PARTICIPATING) + @u_not_mentioned = create(:user, username: 'regular', notification_level: :participating) @u_outsider_mentioned = create(:user, username: 'outsider') project.team << [@u_watcher, :master] @@ -617,8 +614,8 @@ describe NotificationService, services: true do def add_users_with_subscription(project, issuable) @subscriber = create :user @unsubscriber = create :user - @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: Notification::N_PARTICIPATING) - @watcher_and_subscriber = create(:user, notification_level: Notification::N_WATCH) + @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: :participating) + @watcher_and_subscriber = create(:user, notification_level: :watch) project.team << [@subscribed_participant, :master] project.team << [@subscriber, :master] diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index c46259431aae3da5878fecb68a896329bb941373..06017317339ea0931bd8ffdda405294bdeba8772 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -38,4 +38,27 @@ describe Projects::TransferService, services: true do def transfer_project(project, user, new_namespace) Projects::TransferService.new(project, user).execute(new_namespace) end + + context 'visibility level' do + let(:internal_group) { create(:group, :internal) } + + before { internal_group.add_owner(user) } + + context 'when namespace visibility level < project visibility level' do + let(:public_project) { create(:project, :public, namespace: user.namespace) } + + before { transfer_project(public_project, user, internal_group) } + + it { expect(public_project.visibility_level).to eq(internal_group.visibility_level) } + end + + context 'when namespace visibility level > project visibility level' do + let(:private_project) { create(:project, :private, namespace: user.namespace) } + + before { transfer_project(private_project, user, internal_group) } + + it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) } + end + end + end diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml index a5b256bd3ec8e54b4f90ad59689abdc90e8f8196..e55a61b2b94d73a572f87d61728e783832fe2e08 100644 --- a/spec/support/gitlab_stubs/gitlab_ci.yml +++ b/spec/support/gitlab_stubs/gitlab_ci.yml @@ -4,7 +4,7 @@ services: before_script: - gem install bundler - - bundle install + - bundle install - bundle exec rake db:create variables: @@ -17,7 +17,7 @@ types: rspec: script: "rake spec" - tags: + tags: - ruby - postgres only: @@ -26,27 +26,32 @@ rspec: spinach: script: "rake spinach" allow_failure: true - tags: + tags: - ruby - mysql except: - tags staging: + variables: + KEY1: value1 + KEY2: value2 script: "cap deploy stating" type: deploy - tags: + tags: - ruby - mysql except: - stable production: + variables: + DB_NAME: mysql type: deploy - script: + script: - cap deploy production - cap notify - tags: + tags: - ruby - mysql only: diff --git a/spec/support/project_hook_data_shared_example.rb b/spec/support/project_hook_data_shared_example.rb index 422083875d70ea7d12da17207928142196fca3cd..7dbaa6a64593f20dcb1bac5dcac540996eb8ac15 100644 --- a/spec/support/project_hook_data_shared_example.rb +++ b/spec/support/project_hook_data_shared_example.rb @@ -1,4 +1,4 @@ -RSpec.shared_examples 'project hook data' do |project_key: :project| +RSpec.shared_examples 'project hook data with deprecateds' do |project_key: :project| it 'contains project data' do expect(data[project_key][:name]).to eq(project.name) expect(data[project_key][:description]).to eq(project.description) @@ -17,6 +17,21 @@ RSpec.shared_examples 'project hook data' do |project_key: :project| end end +RSpec.shared_examples 'project hook data' do |project_key: :project| + it 'contains project data' do + expect(data[project_key][:name]).to eq(project.name) + expect(data[project_key][:description]).to eq(project.description) + expect(data[project_key][:web_url]).to eq(project.web_url) + expect(data[project_key][:avatar_url]).to eq(project.avatar_url) + expect(data[project_key][:git_http_url]).to eq(project.http_url_to_repo) + expect(data[project_key][:git_ssh_url]).to eq(project.ssh_url_to_repo) + expect(data[project_key][:namespace]).to eq(project.namespace.name) + expect(data[project_key][:visibility_level]).to eq(project.visibility_level) + expect(data[project_key][:path_with_namespace]).to eq(project.path_with_namespace) + expect(data[project_key][:default_branch]).to eq(project.default_branch) + end +end + RSpec.shared_examples 'deprecated repository hook data' do |project_key: :project| it 'contains deprecated repository data' do expect(data[:repository][:name]).to eq(project.name) diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb index aa8258d6dad6ef0904684dd8a3869da479010ae4..73f375c481bf9ed5b7af6745f90828d9afbdb4bb 100644 --- a/spec/support/repo_helpers.rb +++ b/spec/support/repo_helpers.rb @@ -42,7 +42,7 @@ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> eos ) end - + def another_sample_commit OpenStruct.new( id: "e56497bb5f03a90a51293fc6d516788730953899", diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 0265dbe9c666707bab07214ff8b696b396326c41..94ff34579029b016496a5a4a88c238dd115446af 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -4,6 +4,9 @@ describe PostReceive do let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" } let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") } let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } + let(:project) { create(:project) } + let(:key) { create(:key, user: project.owner) } + let(:key_id) { key.shell_id } context "as a resque worker" do it "reponds to #perform" do @@ -11,11 +14,43 @@ describe PostReceive do end end - context "webhook" do - let(:project) { create(:project) } - let(:key) { create(:key, user: project.owner) } - let(:key_id) { key.shell_id } + describe "#process_project_changes" do + before do + allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) + end + context "branches" do + let(:changes) { "123456 789012 refs/heads/tést" } + + it "should call GitTagPushService" do + expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) + expect_any_instance_of(GitTagPushService).not_to receive(:execute) + PostReceive.new.perform(pwd(project), key_id, base64_changes) + end + end + + context "tags" do + let(:changes) { "123456 789012 refs/tags/tag" } + + it "should call GitTagPushService" do + expect_any_instance_of(GitPushService).not_to receive(:execute) + expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) + PostReceive.new.perform(pwd(project), key_id, base64_changes) + end + end + + context "merge-requests" do + let(:changes) { "123456 789012 refs/merge-requests/123" } + + it "should not call any of the services" do + expect_any_instance_of(GitPushService).not_to receive(:execute) + expect_any_instance_of(GitTagPushService).not_to receive(:execute) + PostReceive.new.perform(pwd(project), key_id, base64_changes) + end + end + end + + context "webhook" do it "fetches the correct project" do expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project) PostReceive.new.perform(pwd(project), key_id, base64_changes) diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f486e45ddad67a305aa46cc461406998a2eee759 --- /dev/null +++ b/spec/workers/repository_check/batch_worker_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe RepositoryCheck::BatchWorker do + subject { described_class.new } + + it 'prefers projects that have never been checked' do + projects = create_list(:project, 3) + projects[0].update_column(:last_repository_check_at, 4.months.ago) + projects[2].update_column(:last_repository_check_at, 3.months.ago) + + expect(subject.perform).to eq(projects.values_at(1, 0, 2).map(&:id)) + end + + it 'sorts projects by last_repository_check_at' do + projects = create_list(:project, 3) + projects[0].update_column(:last_repository_check_at, 2.months.ago) + projects[1].update_column(:last_repository_check_at, 4.months.ago) + projects[2].update_column(:last_repository_check_at, 3.months.ago) + + expect(subject.perform).to eq(projects.values_at(1, 2, 0).map(&:id)) + end + + it 'excludes projects that were checked recently' do + projects = create_list(:project, 3) + projects[0].update_column(:last_repository_check_at, 2.days.ago) + projects[1].update_column(:last_repository_check_at, 2.months.ago) + projects[2].update_column(:last_repository_check_at, 3.days.ago) + + expect(subject.perform).to eq([projects[1].id]) + end + + it 'does nothing when repository checks are disabled' do + create(:empty_project) + current_settings = double('settings', repository_checks_enabled: false) + expect(subject).to receive(:current_settings) { current_settings } + + expect(subject.perform).to eq(nil) + end +end diff --git a/spec/workers/repository_check/clear_worker_spec.rb b/spec/workers/repository_check/clear_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a3b70c74787f91304a3c3c561a4969318fd0b913 --- /dev/null +++ b/spec/workers/repository_check/clear_worker_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe RepositoryCheck::ClearWorker do + it 'clears repository check columns' do + project = create(:empty_project) + project.update_columns( + last_repository_check_failed: true, + last_repository_check_at: Time.now, + ) + + described_class.new.perform + project.reload + + expect(project.last_repository_check_failed).to be_nil + expect(project.last_repository_check_at).to be_nil + end +end diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..087e4c667d8caed39073712d34b4c3d6dd88ef1b --- /dev/null +++ b/spec/workers/repository_check/single_repository_worker_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require 'fileutils' + +describe RepositoryCheck::SingleRepositoryWorker do + subject { described_class.new } + + it 'fails if the wiki repository is broken' do + project = create(:project_empty_repo, wiki_enabled: true) + project.create_wiki + + # Test sanity: everything should be fine before the wiki repo is broken + subject.perform(project.id) + expect(project.reload.last_repository_check_failed).to eq(false) + + destroy_wiki(project) + subject.perform(project.id) + + expect(project.reload.last_repository_check_failed).to eq(true) + end + + it 'skips wikis when disabled' do + project = create(:project_empty_repo, wiki_enabled: false) + # Make sure the test would fail if it checked the wiki repo + destroy_wiki(project) + + subject.perform(project.id) + + expect(project.reload.last_repository_check_failed).to eq(false) + end + + def destroy_wiki(project) + FileUtils.rm_rf(project.wiki.repository.path_to_repo) + end +end diff --git a/vendor/assets/javascripts/date.format.js b/vendor/assets/javascripts/date.format.js new file mode 100644 index 0000000000000000000000000000000000000000..f5dc4abcd8019c37236bc455e6150c02cc917a51 --- /dev/null +++ b/vendor/assets/javascripts/date.format.js @@ -0,0 +1,125 @@ +/* + * Date Format 1.2.3 + * (c) 2007-2009 Steven Levithan <stevenlevithan.com> + * MIT license + * + * Includes enhancements by Scott Trenda <scott.trenda.net> + * and Kris Kowal <cixar.com/~kris.kowal/> + * + * Accepts a date, a mask, or a date and a mask. + * Returns a formatted version of the given date. + * The date defaults to the current date/time. + * The mask defaults to dateFormat.masks.default. + */ + +var dateFormat = function () { + var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, + timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, + timezoneClip = /[^-+\dA-Z]/g, + pad = function (val, len) { + val = String(val); + len = len || 2; + while (val.length < len) val = "0" + val; + return val; + }; + + // Regexes and supporting functions are cached through closure + return function (date, mask, utc) { + var dF = dateFormat; + + // You can't provide utc if you skip other args (use the "UTC:" mask prefix) + if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { + mask = date; + date = undefined; + } + + // Passing date through Date applies Date.parse, if necessary + date = date ? new Date(date) : new Date; + if (isNaN(date)) throw SyntaxError("invalid date"); + + mask = String(dF.masks[mask] || mask || dF.masks["default"]); + + // Allow setting the utc argument via the mask + if (mask.slice(0, 4) == "UTC:") { + mask = mask.slice(4); + utc = true; + } + + var _ = utc ? "getUTC" : "get", + d = date[_ + "Date"](), + D = date[_ + "Day"](), + m = date[_ + "Month"](), + y = date[_ + "FullYear"](), + H = date[_ + "Hours"](), + M = date[_ + "Minutes"](), + s = date[_ + "Seconds"](), + L = date[_ + "Milliseconds"](), + o = utc ? 0 : date.getTimezoneOffset(), + flags = { + d: d, + dd: pad(d), + ddd: dF.i18n.dayNames[D], + dddd: dF.i18n.dayNames[D + 7], + m: m + 1, + mm: pad(m + 1), + mmm: dF.i18n.monthNames[m], + mmmm: dF.i18n.monthNames[m + 12], + yy: String(y).slice(2), + yyyy: y, + h: H % 12 || 12, + hh: pad(H % 12 || 12), + H: H, + HH: pad(H), + M: M, + MM: pad(M), + s: s, + ss: pad(s), + l: pad(L, 3), + L: pad(L > 99 ? Math.round(L / 10) : L), + t: H < 12 ? "a" : "p", + tt: H < 12 ? "am" : "pm", + T: H < 12 ? "A" : "P", + TT: H < 12 ? "AM" : "PM", + Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), + o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), + S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] + }; + + return mask.replace(token, function ($0) { + return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); + }); + }; +}(); + +// Some common format strings +dateFormat.masks = { + "default": "ddd mmm dd yyyy HH:MM:ss", + shortDate: "m/d/yy", + mediumDate: "mmm d, yyyy", + longDate: "mmmm d, yyyy", + fullDate: "dddd, mmmm d, yyyy", + shortTime: "h:MM TT", + mediumTime: "h:MM:ss TT", + longTime: "h:MM:ss TT Z", + isoDate: "yyyy-mm-dd", + isoTime: "HH:MM:ss", + isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", + isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" +}; + +// Internationalization strings +dateFormat.i18n = { + dayNames: [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ], + monthNames: [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + ] +}; + +// For convenience... +Date.prototype.format = function (mask, utc) { + return dateFormat(this, mask, utc); +};