diff --git a/CHANGELOG b/CHANGELOG index bb5bde9b08..c09e147833 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,19 +1,29 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.9.0 (unreleased) + - Fix builds API response not including commit data + - Fix error when CI job variables key specified but not defined + - Fix pipeline status when there are no builds in pipeline - Fix Error 500 when using closes_issues API with an external issue tracker - Add more information into RSS feed for issues (Alexander Matyushentsev) - Bulk assign/unassign labels to issues. - Ability to prioritize labels !4009 / !3205 (Thijs Wouters) + - Show Star and Fork buttons on mobile. + - Performance improvements on RelativeLinkFilter - Fix endless redirections when accessing user OAuth applications when they are disabled - Allow enabling wiki page events from Webhook management UI - Bump rouge to 1.11.0 - Fix issue with arrow keys not working in search autocomplete dropdown - Fix an issue where note polling stopped working if a window was in the background during a refresh. + - Pre-processing Markdown now only happens when needed - Make EmailsOnPushWorker use Sidekiq mailers queue + - Redesign all Devise emails. !4297 + - Don't show 'Leave Project' to group members - Fix wiki page events' webhook to point to the wiki repository + - Add a border around images to differentiate them from the background. - Don't show tags for revert and cherry-pick operations + - Show image ID on registry page - Fix issue todo not remove when leave project !4150 (Long Nguyen) - Allow customisable text on the 'nearly there' page after a user signs up - Bump recaptcha gem to 3.0.0 to remove deprecated stoken support @@ -22,12 +32,18 @@ v 8.9.0 (unreleased) - Added descriptions to notification settings dropdown - Improve note validation to prevent errors when creating invalid note via API - Reduce number of fog gem dependencies + - Add number of merge requests for a given milestone to the milestones view. + - Implement a fair usage of shared runners - Remove project notification settings associated with deleted projects - Fix 404 page when viewing TODOs that contain milestones or labels in different projects - Add a metric for the number of new Redis connections created by a transaction - Fix Error 500 when viewing a blob with binary characters after the 1024-byte mark - Redesign navigation for project pages + - Fix images in sign-up confirmation email + - Added shortcut 'y' for copying a files content hash URL #14470 - Fix groups API to list only user's accessible projects + - Fix horizontal scrollbar for long commit message. + - GitLab Performance Monitoring now tracks the total method execution time and call count per method - Add Environments and Deployments - Redesign account and email confirmation emails - Don't fail builds for projects that are deleted @@ -35,30 +51,47 @@ v 8.9.0 (unreleased) - `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix - Bump nokogiri to 1.6.8 - Use gitlab-shell v3.0.0 + - Fixed alignment of download dropdown in merge requests - Upgrade to jQuery 2 + - Adds selected branch name to the dropdown toggle + - Add API endpoint for Sidekiq Metrics !4653 + - Refactoring Award Emoji with API support for Issues and MergeRequests - Use Knapsack to evenly distribute tests across multiple nodes - Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged - Don't allow MRs to be merged when commits were added since the last review / page load - Add DB index on users.state + - Limit email on push diff size to 30 files / 150 KB - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database - Changed the Slack build message to use the singular duration if necessary (Aran Koning) - Links from a wiki page to other wiki pages should be rewritten as expected - Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos) + - Added navigation shortcuts to the project pipelines, milestones, builds and forks page. !4393 - Fix issues filter when ordering by milestone + - Disable SAML account unlink feature - Added artifacts:when to .gitlab-ci.yml - this requires GitLab Runner 1.3 - Bamboo Service: Fix missing credentials & URL handling when base URL contains a path (Benjamin Schmid) - TeamCity Service: Fix URL handling when base URL contains a path - Todos will display target state if issuable target is 'Closed' or 'Merged' + - Validate only and except regexp - Fix bug when sorting issues by milestone due date and filtering by two or more labels + - POST to API /projects/:id/runners/:runner_id would give 409 if the runner was already enabled for this project - Add support for using Yubikeys (U2F) for two-factor authentication - Link to blank group icon doesn't throw a 404 anymore - Remove 'main language' feature + - Toggle whitespace button now available for compare branches diffs #17881 - Pipelines can be canceled only when there are running builds + - Allow authentication using personal access tokens - Use downcased path to container repository as this is expected path by Docker + - Allow to use CI token to fetch LFS objects + - Custom notification settings - Projects pending deletion will render a 404 page - Measure queue duration between gitlab-workhorse and Rails + - Added Gfm autocomplete for labels + - Added edit note 'up' shortcut documentation to the help panel and docs screenshot #18114 - Make Omniauth providers specs to not modify global configuration + - Remove unused JiraIssue class and replace references with ExternalIssue. !4659 (Ilan Shamir) - Make authentication service for Container Registry to be compatible with < Docker 1.11 + - Make it possible to lock a runner from being enabled for other projects - Add Application Setting to configure Container Registry token expire delay (default 5min) - Cache assigned issue and merge request counts in sidebar nav - Use Knapsack only in CI environment @@ -74,8 +107,11 @@ v 8.9.0 (unreleased) - Replace Colorize with Rainbow for coloring console output in Rake tasks. - Add workhorse controller and API helpers - An indicator is now displayed at the top of the comment field for confidential issues. + - Show categorised search queries in the search autocomplete - RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented + - Dropdown for `.gitlab-ci.yml` templates - Improve issuables APIs performance when accessing notes !4471 + - Add sorting dropdown to tags page !4423 - External links now open in a new tab - Prevent default actions of disabled buttons and links - Markdown editor now correctly resets the input value on edit cancellation !4175 @@ -83,6 +119,7 @@ v 8.9.0 (unreleased) - Improved UX of date pickers on issue & milestone forms - Cache on the database if a project has an active external issue tracker. - Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav + - GitLab project import and export functionality - All classes in the Banzai::ReferenceParser namespace are now instrumented - Remove deprecated issues_tracker and issues_tracker_id from project model - Allow users to create confidential issues in private projects @@ -108,9 +145,37 @@ v 8.8.5 (unreleased) - Fix importer for GitHub comments on diff - Disable Webhooks before proceeding with the GitHub import - Fix incremental trace upload API when using multi-byte UTF-8 chars in trace + - Set inverse_of for Project/Service association to reduce the number of queries + - Update tanuki logo highlight/loading colors + - Remove explicit Gitlab::Metrics.action assignments, are already automatic. + - Use Git cached counters for branches and tags on project page + - Cache participable participants in an instance variable. + - Filter parameters for request_uri value on instrumented transactions. + - Remove duplicated keys add UNIQUE index to keys fingerprint column + - ExtractsPath get ref_names from repository cache, if not there access git. + - Cache user todo counts from TodoService + - Ensure Todos counters doesn't count Todos for projects pending delete + - Add tooltip to pin/unpin navbar + - Add left/right arrows horizontal navigation + - Add new sub nav style to Wiki and Graphs sub navigation + +v 8.8.5 + - Import GitHub repositories respecting the API rate limit !4166 + - Fix todos page throwing errors when you have a project pending deletion !4300 + - Disable Webhooks before proceeding with the GitHub import !4470 + - Fix importer for GitHub comments on diff !4488 + - Adjust the SAML control flow to allow LDAP identities to be added to an existing SAML user !4498 + - Fix incremental trace upload API when using multi-byte UTF-8 chars in trace !4541 + - Prevent unauthorized access for projects build traces + - Forbid scripting for wiki files + - Only show notes through JSON on confidential issues that the user has access to + - Banzai::Filter::UploadLinkFilter use XPath instead CSS expressions + - Banzai::Filter::ExternalLinkFilter use XPath instead CSS expressions v 8.8.4 - Fix LDAP-based login for users with 2FA enabled. !4493 + - Added descriptions to notification settings dropdown + - Due date can be removed from milestones v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 @@ -226,6 +291,9 @@ v 8.8.0 v 8.7.7 - Fix import by `Any Git URL` broken if the URL contains a space + - Prevent unauthorized access to other projects build traces + - Forbid scripting for wiki files + - Only show notes through JSON on confidential issues that the user has access to v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) @@ -388,6 +456,11 @@ v 8.7.0 - Add RAW build trace output and button on build page - Add incremental build trace update into CI API +v 8.6.9 + - Prevent unauthorized access to other projects build traces + - Forbid scripting for wiki files + - Only show notes through JSON on confidential issues that the user has access to + v 8.6.8 - Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via notes API @@ -542,6 +615,10 @@ 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.13 + - Prevent unauthorized access to other projects build traces + - Forbid scripting for wiki files + v 8.5.12 - Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via notes API @@ -703,6 +780,10 @@ v 8.5.0 - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Add Todos +v 8.4.11 + - Prevent unauthorized access to other projects build traces + - Forbid scripting for wiki files + v 8.4.10 - Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via notes API @@ -839,6 +920,10 @@ 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.10 + - Prevent unauthorized access to other projects build traces + - Forbid scripting for wiki files + v 8.3.9 - Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via notes API @@ -957,6 +1042,10 @@ v 8.3.0 - Expose Git's version in the admin area - Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye) +v 8.2.6 + - Prevent unauthorized access to other projects build traces + - Forbid scripting for wiki files + v 8.2.5 - Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via notes API diff --git a/Gemfile b/Gemfile index 3b28789300..092ea9d69b 100644 --- a/Gemfile +++ b/Gemfile @@ -48,11 +48,11 @@ gem 'attr_encrypted', '~> 3.0.0' gem 'u2f', '~> 0.2.1' # Browser detection -gem "browser", '~> 2.0.3' +gem "browser", '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 10.0' +gem "gitlab_git", '~> 10.2' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes @@ -221,13 +221,12 @@ gem 'jquery-turbolinks', '~> 2.1.0' gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.3.0' -gem 'font-awesome-rails', '~> 4.2' +gem 'font-awesome-rails', '~> 4.6.1' gem 'gitlab_emoji', '~> 0.3.0' gem 'gon', '~> 6.0.1' gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-rails', '~> 4.1.0' gem 'jquery-ui-rails', '~> 5.0.0' -gem 'raphael-rails', '~> 2.1.2' gem 'request_store', '~> 1.3.0' gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' @@ -331,7 +330,7 @@ gem "newrelic_rpm", '~> 3.14' gem 'octokit', '~> 4.3.0' -gem "mail_room", "~> 0.7" +gem "mail_room", "~> 0.8" gem 'email_reply_parser', '~> 0.5.8' diff --git a/Gemfile.lock b/Gemfile.lock index c1c8c175b1..ba16e4bf33 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,7 +98,7 @@ GEM autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) brakeman (3.3.2) - browser (2.0.3) + browser (2.2.0) builder (3.2.2) bullet (5.0.0) activesupport (>= 3.0.0) @@ -246,7 +246,7 @@ GEM fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) - font-awesome-rails (4.5.0.1) + font-awesome-rails (4.6.1.0) railties (>= 3.2, < 5.1) foreman (0.78.0) thor (~> 0.19.1) @@ -277,7 +277,7 @@ GEM posix-spawn (~> 0.3) gitlab_emoji (0.3.1) gemojione (~> 2.2, >= 2.2.1) - gitlab_git (10.1.3) + gitlab_git (10.2.0) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -398,7 +398,7 @@ GEM systemu (~> 2.6.2) mail (2.6.4) mime-types (>= 1.16, < 4) - mail_room (0.7.0) + mail_room (0.8.0) method_source (0.8.2) mime-types (2.99.2) mimemagic (0.3.0) @@ -556,7 +556,6 @@ GEM rainbow (2.1.0) raindrops (0.15.0) rake (10.5.0) - raphael-rails (2.1.2) rb-fsevent (0.9.6) rb-inotify (0.9.5) ffi (>= 0.5.0) @@ -834,7 +833,7 @@ DEPENDENCIES binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) brakeman (~> 3.3.0) - browser (~> 2.0.3) + browser (~> 2.2) bullet bundler-audit byebug @@ -867,7 +866,7 @@ DEPENDENCIES fog-google (~> 0.3) fog-local (~> 0.3) fog-openstack (~> 0.1) - font-awesome-rails (~> 4.2) + font-awesome-rails (~> 4.6.1) foreman fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) @@ -875,7 +874,7 @@ DEPENDENCIES github-markup (~> 1.3.1) gitlab-flowdock-git-hook (~> 1.0.1) gitlab_emoji (~> 0.3.0) - gitlab_git (~> 10.0) + gitlab_git (~> 10.2) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.1.0) @@ -900,7 +899,7 @@ DEPENDENCIES license_finder licensee (~> 8.0.0) loofah (~> 2.0.3) - mail_room (~> 0.7) + mail_room (~> 0.8) method_source (~> 0.8) minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) @@ -938,7 +937,6 @@ DEPENDENCIES rails (= 4.2.6) rails-deprecated_sanitizer (~> 1.0.3) rainbow (~> 2.1.0) - raphael-rails (~> 2.1.2) rblineprof rdoc (~> 3.6) recaptcha (~> 3.0) diff --git a/VERSION b/VERSION index 141cafb8e0..e5c15102d9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.9.0-rc4 +8.9.0 diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee index 365a062bb8..6d8faba40d 100644 --- a/app/assets/javascripts/LabelManager.js.coffee +++ b/app/assets/javascripts/LabelManager.js.coffee @@ -27,6 +27,11 @@ class @LabelManager $btn = $(e.currentTarget) $label = $("##{$btn.data('domId')}") action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add' + + # Make sure tooltip will hide + $tooltip = $ "##{$btn.find('.has-tooltip:visible').attr('aria-describedby')}" + $tooltip.tooltip 'destroy' + _this.toggleLabelPriority($label, action) toggleLabelPriority: ($label, action, persistState = true) -> @@ -42,10 +47,10 @@ class @LabelManager $from = @prioritizedLabels if $from.find('li').length is 1 - $from.find('.empty-message').show() + $from.find('.empty-message').removeClass('hidden') if not $target.find('li').length - $target.find('.empty-message').hide() + $target.find('.empty-message').addClass('hidden') $label.detach().appendTo($target) @@ -54,6 +59,9 @@ class @LabelManager if action is 'remove' xhr = $.ajax url: url, type: 'DELETE' + + # Restore empty message + $from.find('.empty-message').removeClass('hidden') unless $from.find('li').length else xhr = @savePrioritySort($label, action) diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 3f61ea1eaf..cf46f15a15 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -7,6 +7,7 @@ labelsPath: "/api/:version/projects/:id/labels" licensePath: "/api/:version/licenses/:key" gitignorePath: "/api/:version/gitignores/:key" + gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key" group: (group_id, callback) -> url = Api.buildUrl(Api.groupPath) @@ -110,6 +111,12 @@ $.get url, (gitignore) -> callback(gitignore) + gitlabCiYml: (key, callback) -> + url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key) + + $.get url, (file) -> + callback(file) + 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 6c16f89cef..397892d15c 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -32,10 +32,6 @@ #= require bootstrap/tooltip #= require bootstrap/popover #= require select2 -#= require raphael -#= require g.raphael -#= require g.bar -#= require branch-graph #= require ace/ace #= require ace/ext-searchbox #= require underscore @@ -128,7 +124,7 @@ $ -> gl.utils.preventDisabledButtons() bootstrapBreakpoint = bp.getBreakpointSize() - $(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF") + $(".nav-sidebar").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF") # Click a .js-select-on-focus field, select the contents $(".js-select-on-focus").on "focusin", -> @@ -258,3 +254,47 @@ $ -> gl.awardsHandler = new AwardsHandler() checkInitialSidebarSize() new Aside() + + # Sidenav pinning + if $(window).width() < 1440 and $.cookie('pin_nav') is 'true' + $.cookie('pin_nav', 'false', { path: '/' }) + $('.page-with-sidebar') + .toggleClass('page-sidebar-collapsed page-sidebar-expanded') + .removeClass('page-sidebar-pinned') + $('.navbar-fixed-top').removeClass('header-pinned-nav') + + $(document) + .off 'click', '.js-nav-pin' + .on 'click', '.js-nav-pin', (e) -> + e.preventDefault() + + $pinBtn = $(e.currentTarget) + $page = $ '.page-with-sidebar' + $topNav = $ '.navbar-fixed-top' + $tooltip = $ "##{$pinBtn.attr('aria-describedby')}" + doPinNav = not $page.is('.page-sidebar-pinned') + tooltipText = 'Pin navigation' + + $(this).toggleClass 'is-active' + + if doPinNav + $page.addClass('page-sidebar-pinned') + $topNav.addClass('header-pinned-nav') + else + $tooltip.remove() # Remove it immediately when collapsing the sidebar + $page.removeClass('page-sidebar-pinned') + .toggleClass('page-sidebar-collapsed page-sidebar-expanded') + $topNav.removeClass('header-pinned-nav') + .toggleClass('header-collapsed header-expanded') + + # Save settings + $.cookie 'pin_nav', doPinNav, { path: '/' } + + if $.cookie('pin_nav') is 'true' or doPinNav + tooltipText = 'Unpin navigation' + + # Update tooltip text immediately + $tooltip.find('.tooltip-inner').text(tooltipText) + + # Persist tooltip title + $pinBtn.attr('title', tooltipText).tooltip('fixTitle') diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 136db8ee14..030f156486 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -40,7 +40,7 @@ class @AwardsHandler $menu = $ '.emoji-menu' if $addBtn.hasClass 'js-note-emoji' - $addBtn.parents('.note').find('.js-awards-block').addClass 'current' + $addBtn.closest('.note').find('.js-awards-block').addClass 'current' else $addBtn.closest('.js-awards-block').addClass 'current' diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.coffee b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee new file mode 100644 index 0000000000..d9a03d0552 --- /dev/null +++ b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee @@ -0,0 +1,23 @@ +#= require blob/template_selector + +class @BlobCiYamlSelector extends TemplateSelector + requestFile: (query) -> + Api.gitlabCiYml query.name, @requestFileSuccess.bind(@) + +class @BlobCiYamlSelectors + constructor: (opts) -> + { + @$dropdowns = $('.js-gitlab-ci-yml-selector') + @editor + } = opts + + @$dropdowns.each (i, dropdown) => + $dropdown = $(dropdown) + + new BlobCiYamlSelector( + pattern: /(.gitlab-ci.yml)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), + dropdown: $dropdown, + editor: @editor + ) diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee index cc8a497d08..8d0e3f363d 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee @@ -1,58 +1,5 @@ -class @BlobGitignoreSelector - constructor: (opts) -> - { - @dropdown - @editor - @$wrapper = @dropdown.closest('.gitignore-selector') - @$filenameInput = $('#file_name') - @data = @dropdown.data('filenames') - } = opts +#= require blob/template_selector - @dropdown.glDropdown( - data: @data, - filterable: true, - selectable: true, - search: - fields: ['name'] - clicked: @onClick - text: (gitignore) -> - gitignore.name - ) - - @toggleGitignoreSelector() - @bindEvents() - - bindEvents: -> - @$filenameInput - .on 'keyup blur', (e) => - @toggleGitignoreSelector() - - toggleGitignoreSelector: -> - filename = @$filenameInput.val() or $('.editor-file-name').text().trim() - @$wrapper.toggleClass 'hidden', filename isnt '.gitignore' - - onClick: (item, el, e) => - e.preventDefault() - @requestIgnoreFile(item.name) - - requestIgnoreFile: (name) -> - Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@) - - requestIgnoreFileSuccess: (gitignore) -> - @editor.setValue(gitignore.content, 1) - @editor.focus() - -class @BlobGitignoreSelectors - constructor: (opts) -> - { - @$dropdowns = $('.js-gitignore-selector') - @editor - } = opts - - @$dropdowns.each (i, dropdown) => - $dropdown = $(dropdown) - - new BlobGitignoreSelector( - dropdown: $dropdown, - editor: @editor - ) +class @BlobGitignoreSelector extends TemplateSelector + requestFile: (query) -> + Api.gitignoreText query.name, @requestFileSuccess.bind(@) diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee new file mode 100644 index 0000000000..a719ba2512 --- /dev/null +++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee @@ -0,0 +1,17 @@ +class @BlobGitignoreSelectors + constructor: (opts) -> + { + @$dropdowns = $('.js-gitignore-selector') + @editor + } = opts + + @$dropdowns.each (i, dropdown) => + $dropdown = $(dropdown) + + new BlobGitignoreSelector( + pattern: /(.gitignore)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitignore-selector-wrap'), + dropdown: $dropdown, + editor: @editor + ) diff --git a/app/assets/javascripts/blob/blob_license_selector.js.coffee b/app/assets/javascripts/blob/blob_license_selector.js.coffee index e17eaa75dc..a3cc8dd844 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js.coffee +++ b/app/assets/javascripts/blob/blob_license_selector.js.coffee @@ -1,30 +1,9 @@ -class @BlobLicenseSelector - licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i +#= require blob/template_selector - constructor: (editor) -> - @$licenseSelector = $('.js-license-selector') - $fileNameInput = $('#file_name') +class @BlobLicenseSelector extends TemplateSelector + requestFile: (query) -> + data = + project: @dropdown.data('project') + fullname: @dropdown.data('fullname') - 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() + Api.licenseText query.id, data, @requestFileSuccess.bind(@) diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.coffee b/app/assets/javascripts/blob/blob_license_selectors.js.coffee new file mode 100644 index 0000000000..6843873310 --- /dev/null +++ b/app/assets/javascripts/blob/blob_license_selectors.js.coffee @@ -0,0 +1,17 @@ +class @BlobLicenseSelectors + constructor: (opts) -> + { + @$dropdowns = $('.js-license-selector') + @editor + } = opts + + @$dropdowns.each (i, dropdown) => + $dropdown = $(dropdown) + + new BlobLicenseSelector( + pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-license-selector-wrap'), + dropdown: $dropdown, + editor: @editor + ) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index 79141e768b..19e584519d 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -12,8 +12,10 @@ class @EditBlob $("#file-content").val(@editor.getValue()) @initModePanesAndLinks() - new BlobLicenseSelector(@editor) - new BlobGitignoreSelectors(editor: @editor) + + new BlobLicenseSelectors { @editor } + new BlobGitignoreSelectors { @editor } + new BlobCiYamlSelectors { @editor } initModePanesAndLinks: -> @$editModePanes = $(".js-edit-mode-pane") diff --git a/app/assets/javascripts/blob/template_selector.js.coffee b/app/assets/javascripts/blob/template_selector.js.coffee new file mode 100644 index 0000000000..e76e303189 --- /dev/null +++ b/app/assets/javascripts/blob/template_selector.js.coffee @@ -0,0 +1,56 @@ +class @TemplateSelector + constructor: (opts = {}) -> + { + @dropdown, + @data, + @pattern, + @wrapper, + @editor, + @fileEndpoint, + @$input = $('#file_name') + } = opts + + @buildDropdown() + @bindEvents() + @onFilenameUpdate() + + buildDropdown: -> + @dropdown.glDropdown( + data: @data, + filterable: true, + selectable: true, + search: + fields: ['name'] + clicked: @onClick + text: (item) -> + item.name + ) + + bindEvents: -> + @$input.on('keyup blur', (e) => + @onFilenameUpdate() + ) + + onFilenameUpdate: -> + return unless @$input.length + + filenameMatches = @pattern.test(@$input.val().trim()) + + if not filenameMatches + @wrapper.addClass('hidden') + return + + @wrapper.removeClass('hidden') + + onClick: (item, el, e) => + e.preventDefault() + @requestFile(item) + + requestFile: (item) -> + # To be implemented on the extending class + # e.g. + # Api.gitignoreText item.name, @requestFileSuccess.bind(@) + + requestFileSuccess: (file) -> + @editor.setValue(file.content, 1) + @editor.focus() diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 8b39e6b090..7fbff9214c 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -29,6 +29,7 @@ class Dispatcher new Todos() when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() + new DueDateSelect() new GLForm($('.milestone-form')) when 'groups:milestones:new' new ZenMode() @@ -72,13 +73,12 @@ class Dispatcher new Diff() new ZenMode() shortcut_handler = new ShortcutsNavigation() - when 'projects:commits:show' - shortcut_handler = new ShortcutsNavigation() - when 'projects:activity' + when 'projects:commits:show', 'projects:activity' shortcut_handler = new ShortcutsNavigation() when 'projects:show' shortcut_handler = new ShortcutsNavigation() + new NotificationsForm() new TreeView() if $('#tree-slider').length when 'groups:activity' new Activities() @@ -100,6 +100,7 @@ class Dispatcher when 'projects:blob:show', 'projects:blame:show' new LineHighlighter() shortcut_handler = new ShortcutsNavigation() + new ShortcutsBlob true when 'projects:labels:new', 'projects:labels:edit' new Labels() when 'projects:labels:index' @@ -129,19 +130,21 @@ class Dispatcher shortcut_handler = new ShortcutsDashboardNavigation() when 'profiles' new Profile() + new NotificationsForm() + new NotificationsDropdown() when 'projects' new Project() new ProjectAvatar() switch path[1] - when 'compare' - shortcut_handler = new ShortcutsNavigation() when 'edit' shortcut_handler = new ShortcutsNavigation() new ProjectNew() when 'new' new ProjectNew() when 'show' + new ProjectNew() new ProjectShow() + new NotificationsDropdown() when 'wikis' new Wikis() shortcut_handler = new ShortcutsNavigation() @@ -150,9 +153,9 @@ class Dispatcher when 'snippets' shortcut_handler = new ShortcutsNavigation() new ZenMode() if path[2] == 'show' - when 'labels', 'graphs' - shortcut_handler = new ShortcutsNavigation() - when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' + when 'labels', 'graphs', 'compare', 'pipelines', 'forks', \ + 'milestones', 'project_members', 'deploy_keys', 'builds', \ + 'hooks', 'services', 'protected_branches' shortcut_handler = new ShortcutsNavigation() # If we haven't installed a custom shortcut handler, install the default one diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee index 3d009a96d0..d65c018dad 100644 --- a/app/assets/javascripts/due_date_select.js.coffee +++ b/app/assets/javascripts/due_date_select.js.coffee @@ -1,5 +1,21 @@ class @DueDateSelect constructor: -> + # Milestone edit/new form + $datePicker = $('.datepicker') + + if $datePicker.length + $dueDate = $('#milestone_due_date') + $datePicker.datepicker + dateFormat: 'yy-mm-dd' + onSelect: (dateText, inst) -> + $dueDate.val(dateText) + .datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val())) + + $('.js-clear-due-date').on 'click', (e) -> + e.preventDefault() + $.datepicker._clearDate($datePicker) + + # Issuable sidebar $loading = $('.js-issuable-update .due_date') .find('.block-loading') .hide() @@ -32,7 +48,7 @@ class @DueDateSelect date = new Date value.replace(new RegExp('-', 'g'), ',') mediumDate = $.datepicker.formatDate 'M d, yy', date else - mediumDate = 'None' + mediumDate = 'No due date' data = {} data[abilityName] = {} @@ -50,7 +66,8 @@ class @DueDateSelect $selectbox.hide() $value.css('display', '') - $valueContent.html(mediumDate) + cssClass = if Date.parse(mediumDate) then 'bold' else 'no-value' + $valueContent.html("#{mediumDate}") $sidebarValue.html(mediumDate) if value isnt '' diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 76c3083232..190bb38504 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -15,6 +15,9 @@ GitLab.GfmAutoComplete = Members: template: '
Hello <%= @resource.email %>!
- -Someone has requested a link to change your password, and you can do this through the link below.
- -<%= link_to 'Change your password', edit_password_url(@resource, reset_password_token: @token) %>
- -If you didn't request this, please ignore this email.
-Your password won't change until you access the link above and create a new one.
diff --git a/app/views/devise/mailer/reset_password_instructions.html.haml b/app/views/devise/mailer/reset_password_instructions.html.haml new file mode 100644 index 0000000000..e91c952252 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.haml @@ -0,0 +1,12 @@ +.center + #content + %h2 Hello, #{@resource.name}! + %p + Someone, hopefully you, has requested to reset the password for your + GitLab account on #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}. + %p + If you did not perform this request, you can safely ignore this email. + %p + Otherwise, click the link below to complete the process. + #cta + = link_to('Reset password', edit_password_url(@resource, reset_password_token: @token)) diff --git a/app/views/devise/mailer/reset_password_instructions.text.erb b/app/views/devise/mailer/reset_password_instructions.text.erb new file mode 100644 index 0000000000..116313ee11 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.text.erb @@ -0,0 +1,10 @@ +Hello, <%= @resource.name %>! + +Someone, hopefully you, has requested to reset the password for your GitLab +account on <%= Gitlab.config.gitlab.url %> + +If you did not perform this request, you can safely ignore this email. + +Otherwise, click the link below to complete the process: + +<%= edit_password_url(@resource, reset_password_token: @token) %> diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml index 52b327e20c..9990d1ccac 100644 --- a/app/views/devise/mailer/unlock_instructions.html.haml +++ b/app/views/devise/mailer/unlock_instructions.html.haml @@ -1,10 +1,9 @@ -%p -Hello #{@resource.name}! - -%p - Your GitLab account has been locked due to an excessive amount of unsuccessful - sign in attempts. Your account will automatically unlock in - = time_ago_in_words(Devise.unlock_in.from_now) - or you may click the link below to unlock now. - -%p= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) +.center + #content + %h2 Hello, #{@resource.name}! + %p + Your GitLab account has been locked due to an excessive amount of unsuccessful + sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)} + or you may click the link below to unlock now. + #cta + = link_to('Unlock account', unlock_url(@resource, unlock_token: @token)) diff --git a/app/views/devise/mailer/unlock_instructions.text.erb b/app/views/devise/mailer/unlock_instructions.text.erb new file mode 100644 index 0000000000..3aea3e2014 --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.text.erb @@ -0,0 +1,7 @@ +Hello, <%= @resource.name %>! + +Your GitLab account has been locked due to an excessive amount of unsuccessful +sign in attempts. Your account will automatically unlock in <%= time_ago_in_words(Devise.unlock_in.from_now) %> +or you may click the link below to unlock now. + +<%= unlock_url(@resource, unlock_token: @token) %> diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml index b0b3a51ce5..da71de4cd1 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,2 +1,2 @@ :plain - $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member))}'); + $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}'); diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 01648047ce..8cc0b59ede 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -28,8 +28,12 @@ .key ⌘ shift p - else .key ctrl shift p - %td Toggle Markdown preview + %tr + %td.shortcut + .key + %i.fa.fa-arrow-up + %td Edit last comment (when focused on an empty textarea) %tbody %tr %th diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 6c4a9d68d1..7486b1423e 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -6,7 +6,7 @@ %p %i.fa.fa-warning - To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process. + To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process, but only if you have admin access to the GitHub repository. %p.light Select projects you want to import. diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml new file mode 100644 index 0000000000..44e2653ca4 --- /dev/null +++ b/app/views/import/gitlab_projects/new.html.haml @@ -0,0 +1,25 @@ +- page_title "GitLab Import" +- header_title "Projects", root_path +%h3.page-title + = icon('gitlab') + Import an exported GitLab project +%hr + += form_tag import_gitlab_project_path, class: 'form-horizontal', multipart: true do + %p + Project will be imported as + %strong + #{@namespace_name}/#{@path} + + %p + To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here. + .form-group + = hidden_field_tag :namespace_id, @namespace_id + = hidden_field_tag :path, @path + = label_tag :file, class: 'control-label' do + %span GitLab project export + .col-sm-10 + = file_field_tag :file, class: '' + + .form-actions + = submit_tag 'Import project', class: 'btn btn-create' diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml index e4fab89737..8c140a5943 100644 --- a/app/views/layouts/_collapse_button.html.haml +++ b/app/views/layouts/_collapse_button.html.haml @@ -1 +1,3 @@ -= link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open/Close" += link_to '#', class: 'nav-header-btn text-center toggle-nav-collapse', title: "Open/Close" do + %span.sr-only Toggle navigation + = icon('bars') diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index f89e858279..2234bf79c8 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,6 +1,6 @@ -.page-with-sidebar.page-sidebar-collapsed{ class: "#{page_gutter_class}" } +.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } - + = render partial: 'layouts/collapse_button' - if defined?(sidebar) && sidebar = render "layouts/nav/#{sidebar}" - elsif current_user @@ -8,13 +8,14 @@ - else = render 'layouts/nav/explore' - .collapse-nav - = render partial: 'layouts/collapse_button' - if current_user = link_to current_user, class: 'sidebar-user', title: "Profile", data: {user: current_user.username} do = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' .username = current_user.username + = link_to '#', class: "nav-header-btn text-center pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do + %span.sr-only Toggle navigation pinning + = icon('thumb-tack') - if defined?(nav) && nav .layout-nav .container-fluid diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index b49207fc31..245b9c3b4d 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -36,6 +36,31 @@ - else = hidden_field_tag :search_code, true + :javascript + gl.projectOptions = gl.projectOptions || {}; + gl.projectOptions["#{j(@project.path)}"] = { + issuesPath: "#{namespace_project_issues_path(@project.namespace, @project)}", + mrPath: "#{namespace_project_merge_requests_path(@project.namespace, @project)}", + name: "#{j(@project.name)}" + }; + + - if @group and @group.path + :javascript + gl.groupOptions = gl.groupOptions || {}; + gl.groupOptions["#{j(@group.path)}"] = { + name: "#{j(@group.name)}", + issuesPath: "#{issues_group_path(j(@group.path))}", + mrPath: "#{merge_requests_group_path(j(@group.path))}" + }; + + + :javascript + gl.dashboardOptions = { + issuesPath: "#{issues_dashboard_url}", + mrPath: "#{merge_requests_dashboard_url}" + }; + + - if @snippet || @snippets = hidden_field_tag :snippets, true = hidden_field_tag :repository_ref, @ref diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 2b86b289bb..33cedaaf2e 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head" - %body{class: "#{user_application_theme}", 'data-page' => body_data_page} + %body{class: "#{user_application_theme}", data: {page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}"}} = Gon::Base.render_data -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index ef31520f5c..40a2c81eeb 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,4 +1,4 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab.header-collapsed{ class: nav_header_class } +%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } %div{ class: fluid_layout ? "container-fluid" : "container-fluid" } .header-content %button.side-nav-toggle{type: 'button'} diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 54aa34bee0..66e5ec1ad1 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -1,64 +1,41 @@ -%ul.nav-links.scrolling-tabs - .fade-left - = nav_link(controller: %w(dashboard admin projects users groups builds), html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do - %span - Overview - = nav_link(controller: %w(background_jobs logs health_check)) do - = link_to admin_background_jobs_path, title: 'Monitoring' do - %span - Monitoring - = nav_link(controller: :deploy_keys) do - = link_to admin_deploy_keys_path, title: 'Deploy Keys' do - %span - Deploy Keys - = nav_link path: ['runners#index', 'runners#show'] do - = link_to admin_runners_path, title: 'Runners' do - %span - Runners - = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path, title: 'Messages' do - %span - Messages - = nav_link(controller: :hooks) do - = link_to admin_hooks_path, title: 'Hooks' do - %span - Hooks +%div{ class: nav_control_class } + = render 'layouts/nav/admin_settings' - = nav_link(controller: :appearances) do - = link_to admin_appearances_path, title: 'Appearances' do - %span - Appearance - - = nav_link(controller: :applications) do - = link_to admin_applications_path, title: 'Applications' do - %span - Applications - - = nav_link(controller: :services) do - = link_to admin_application_settings_services_path, title: 'Service Templates' do - %span - Service Templates - - = nav_link(controller: :labels) do - = link_to admin_labels_path, title: 'Labels' do - %span - Labels - - = nav_link(controller: :abuse_reports) do - = link_to admin_abuse_reports_path, title: "Abuse Reports" do - %span - Abuse Reports - %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) - - - if askimet_enabled? - = nav_link(controller: :spam_logs) do - = link_to admin_spam_logs_path, title: "Spam Logs" do + %ul.nav-links.scrolling-tabs + %li.fade-left + = icon('arrow-left') + = nav_link(controller: %w(dashboard admin projects users groups builds runners), html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do %span - Spam Logs + Overview + = nav_link(controller: %w(background_jobs logs health_check)) do + = link_to admin_background_jobs_path, title: 'Monitoring' do + %span + Monitoring + = nav_link(controller: :broadcast_messages) do + = link_to admin_broadcast_messages_path, title: 'Messages' do + %span + Messages + = nav_link(controller: :hooks) do + = link_to admin_hooks_path, title: 'Hooks' do + %span + System Hooks - = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do - = link_to admin_application_settings_path, title: 'Settings' do - %span - Settings - .fade-right + = nav_link(controller: :applications) do + = link_to admin_applications_path, title: 'Applications' do + %span + Applications + + = nav_link(controller: :abuse_reports) do + = link_to admin_abuse_reports_path, title: "Abuse Reports" do + %span + Abuse Reports + %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) + + - if askimet_enabled? + = nav_link(controller: :spam_logs) do + = link_to admin_spam_logs_path, title: "Spam Logs" do + %span + Spam Logs + %li.fade-right + = icon('arrow-right') diff --git a/app/views/layouts/nav/_admin_settings.html.haml b/app/views/layouts/nav/_admin_settings.html.haml new file mode 100644 index 0000000000..38e9b80d12 --- /dev/null +++ b/app/views/layouts/nav/_admin_settings.html.haml @@ -0,0 +1,31 @@ +.controls + .dropdown.admin-settings-dropdown + %a.dropdown-new.btn.btn-default{href: '#', 'data-toggle' => 'dropdown'} + = icon('cog') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + = nav_link(controller: :deploy_keys) do + = link_to admin_deploy_keys_path, title: 'Deploy Keys' do + %span + Deploy Keys + + = nav_link(controller: :services) do + = link_to admin_application_settings_services_path, title: 'Service Templates' do + %span + Service Templates + + = nav_link(controller: :labels) do + = link_to admin_labels_path, title: 'Labels' do + %span + Labels + + = nav_link(controller: :appearances) do + = link_to admin_appearances_path, title: 'Appearances' do + %span + Appearance + + %li.divider + = nav_link(controller: :application_settings) do + = link_to admin_application_settings_path, title: 'Settings' do + %span + Settings diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 66361a644d..f7aa9fab7c 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -2,7 +2,8 @@ = render 'layouts/nav/group_settings' %ul.nav-links.scrolling-tabs - .fade-left + %li.fade-left + = icon('arrow-left') = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do %span @@ -31,4 +32,5 @@ = link_to group_group_members_path(@group), title: 'Members' do %span Members - .fade-right + %li.fade-right + = icon('arrow-right') diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index d4b1f477f3..44ea939b7e 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,5 +1,6 @@ %ul.nav-links.scrolling-tabs - .fade-left + %li.fade-left + = icon('arrow-left') = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do %span @@ -13,6 +14,10 @@ = link_to applications_profile_path, title: 'Applications' do %span Applications + = nav_link(controller: :personal_access_tokens) do + = link_to profile_personal_access_tokens_path, title: 'Personal Access Tokens' do + %span + Personal Access Tokens = nav_link(controller: :emails) do = link_to profile_emails_path, title: 'Emails' do %span @@ -39,4 +44,5 @@ = link_to audit_log_profile_path, title: 'Audit Log' do %span Audit Log - .fade-right + %li.fade-right + = icon('arrow-right') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index a851cae4b5..7762746f84 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -5,18 +5,19 @@ = icon('cog') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right + - is_project_member = @project.users.exists?(current_user.id) - access = @project.team.max_member_access(current_user.id) - can_edit = can?(current_user, :admin_project, @project) = render 'layouts/nav/project_settings', access: access, can_edit: can_edit - - if can_edit || access + - if can_edit || is_project_member %li.divider - if can_edit %li = link_to edit_project_path(@project) do Edit Project - - if access + - if is_project_member %li = link_to polymorphic_path([:leave, @project, :members]), data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do @@ -24,7 +25,8 @@ %div{ class: nav_control_class } %ul.nav-links.scrolling-tabs - .fade-left + %li.fade-left + = icon('arrow-left') = nav_link(path: 'projects#show', html_options: {class: 'home'}) do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do %span @@ -37,9 +39,9 @@ - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do - = link_to project_files_path(@project), title: 'Code', class: 'shortcuts-tree' do + = link_to project_files_path(@project), title: 'Repository', class: 'shortcuts-tree' do %span - Code + Repository - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :environments]) do @@ -108,4 +110,5 @@ %li.hidden = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do Commits - .fade-right + %li.fade-right + = icon('arrow-right') diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml new file mode 100644 index 0000000000..b28fea35ad --- /dev/null +++ b/app/views/notify/project_was_exported_email.html.haml @@ -0,0 +1,8 @@ +%p + Project #{@project.name} was exported successfully. +%p + The project export can be downloaded from: + = link_to download_export_namespace_project_url(@project.namespace, @project) do + = @project.name_with_namespace + " export" +%p + The download link will expire in 24 hours. diff --git a/app/views/notify/project_was_exported_email.text.erb b/app/views/notify/project_was_exported_email.text.erb new file mode 100644 index 0000000000..42c4d17687 --- /dev/null +++ b/app/views/notify/project_was_exported_email.text.erb @@ -0,0 +1,6 @@ +Project <%= @project.name %> was exported successfully. + +The project export can be downloaded from: +<%= download_export_namespace_project_url(@project.namespace, @project) %> + +The download link will expire in 24 hours. diff --git a/app/views/notify/project_was_not_exported_email.html.haml b/app/views/notify/project_was_not_exported_email.html.haml new file mode 100644 index 0000000000..c9e9ade2cf --- /dev/null +++ b/app/views/notify/project_was_not_exported_email.html.haml @@ -0,0 +1,9 @@ +%p + Project #{@project.name} couldn't be exported. +%p + The errors we encountered were: + + %ul + - @errors.each do |error| + %li + error diff --git a/app/views/notify/project_was_not_exported_email.text.erb b/app/views/notify/project_was_not_exported_email.text.erb new file mode 100644 index 0000000000..a07f6edacf --- /dev/null +++ b/app/views/notify/project_was_not_exported_email.text.erb @@ -0,0 +1,6 @@ +Project <%= @project.name %> couldn't be exported. + +The errors we encountered were: + +- @errors.each do |error| +<%= error %> \ No newline at end of file diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 3d2a245ecb..8efe486e01 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -62,10 +62,14 @@ .provider-btn-image = provider_image_tag(provider) - if auth_active?(provider) - = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do - Disconnect + - if provider.to_s == 'saml' + %a.provider-btn + Active + - else + = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do + Disconnect - else - = link_to user_omniauth_authorize_path(provider), method: :post, class: "provider-btn #{'not-active' if !auth_active?(provider)}", "data-no-turbolink" => "true" do + = link_to user_omniauth_authorize_path(provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do Connect %hr - if current_user.can_change_username? diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml index f0cf82afe8..537bba21f4 100644 --- a/app/views/profiles/notifications/_group_settings.html.haml +++ b/app/views/profiles/notifications/_group_settings.html.haml @@ -9,5 +9,4 @@ = 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' + = render 'shared/notifications/button', notification_setting: setting diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml index e0fad555c0..5b2a69b889 100644 --- a/app/views/profiles/notifications/_project_settings.html.haml +++ b/app/views/profiles/notifications/_project_settings.html.haml @@ -9,5 +9,4 @@ = 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' + = render 'shared/notifications/button', notification_setting: setting diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index f2659ac14b..f77738f97f 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -24,12 +24,15 @@ .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' - = notification_level_radio_buttons - .prepend-top-default - = f.submit 'Update settings', class: "btn btn-create" + = label_tag :global_notification_level, "Global notification level", class: "label-light" + %br + .clearfix + .form-group.pull-left.global-notification-setting + = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true + + .clearfix + %hr %h5 Groups (#{@group_notifications.count}) diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml new file mode 100644 index 0000000000..1b45548bd0 --- /dev/null +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -0,0 +1,105 @@ +- page_title "Personal Access Tokens" + +.row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 + = page_title + %p + You can generate a personal access token for each application you use that needs access to the GitLab API. + .col-lg-9 + + - if flash[:personal_access_token] + .created-personal-access-token-container + %h5.prepend-top-0 + Your New Personal Access Token + .form-group + = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block" + = clipboard_button(clipboard_text: flash[:personal_access_token]) + %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again. + + %hr + + %h5.prepend-top-0 + Add a Personal Access Token + %p.profile-settings-content + Pick a name for the application, and we'll give you a unique token. + = form_for [:profile, @personal_access_token], + method: :post, html: { class: 'js-requires-input' } do |f| + + = form_errors(@personal_access_token) + + .form-group + = f.label :name, class: 'label-light' + = f.text_field :name, class: "form-control", required: true + + .form-group + = f.label :expires_at, class: 'label-light' + = f.text_field :expires_at, class: "datepicker form-control", required: false + + .prepend-top-default + = f.submit 'Create Personal Access Token', class: "btn btn-create" + + %hr + + %h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length}) + + - if @active_personal_access_tokens.present? + .table-responsive + %table.table.active-personal-access-tokens + %thead + %tr + %th Name + %th Created + %th Expires + %th + %tbody + - @active_personal_access_tokens.each do |token| + %tr + %td= token.name + %td= token.created_at.to_date.to_s(:medium) + %td + - if token.expires_at.present? + = token.expires_at.to_date.to_s(:medium) + - else + %span.personal-access-tokens-never-expires-label Never + %td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." } + + - else + .settings-message.text-center + You don't have any active tokens yet. + + %hr + + %h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length}) + + - if @inactive_personal_access_tokens.present? + .table-responsive + %table.table.inactive-personal-access-tokens + %thead + %tr + %th Name + %th Created + %tbody + - @inactive_personal_access_tokens.each do |token| + %tr + %td= token.name + %td= token.created_at.to_date.to_s(:medium) + + - else + .settings-message.text-center + There are no inactive tokens. + + +:javascript + var date = $('#personal_access_token_expires_at').val(); + + var datepicker = $(".datepicker").datepicker({ + dateFormat: "yy-mm-dd", + minDate: 0 + }); + + $("#created-personal-access-token").click(function() { + this.select(); + }); + + $("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000); diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 2b19ee93ee..86ea08dd22 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -21,7 +21,7 @@ = link_to project_path(forked_from_project) do = forked_from_project.namespace.try(:name) - .project-repo-buttons + .project-repo-buttons.project-action-buttons .count-buttons = render 'projects/buttons/star' = render 'projects/buttons/fork' @@ -29,13 +29,13 @@ .project-clone-holder = render "shared/clone_panel" - .project-repo-buttons.project-right-buttons + .project-repo-buttons.btn-group.project-right-buttons - if current_user = render 'shared/members/access_request_buttons', source: @project - .btn-group - = render "projects/buttons/download" - = render 'projects/buttons/dropdown' - = render 'projects/buttons/notifications' + + = render "projects/buttons/download" + = render 'projects/buttons/dropdown' + = render 'shared/notifications/button', notification_setting: @notification_setting :javascript new Star(); diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml index 7c2b8d0150..e0ca2a3109 100644 --- a/app/views/projects/_last_push.html.haml +++ b/app/views/projects/_last_push.html.haml @@ -1,15 +1,15 @@ - if event = last_push_event - if show_last_push_widget?(event) - .row-content-block.top-block.clear-block.hidden-xs - .event-last-push - .event-last-push-text - %span You pushed to - = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do - %strong= event.ref_name - branch - #{time_ago_with_tooltip(event.created_at)} + %div{ class: (container_class) } + .event-last-push + .event-last-push-text + %span You pushed to + = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do + %strong= event.ref_name + branch + #{time_ago_with_tooltip(event.created_at)} - .pull-right - = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do - Create Merge Request + .pull-right + = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do + Create Merge Request diff --git a/app/views/projects/badges/index.html.haml b/app/views/projects/badges/index.html.haml index ee63bc55a3..ac80951dd4 100644 --- a/app/views/projects/badges/index.html.haml +++ b/app/views/projects/badges/index.html.haml @@ -7,7 +7,7 @@ %b Builds badge · = @build_badge.to_html .pull-right - = render 'shared/ref_switcher', destination: 'badges' + = render 'shared/ref_switcher', destination: 'badges', align_right: true .panel-body .row .col-md-2.text-center diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 4071b59c00..29c7d45074 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -13,12 +13,12 @@ required: true, class: 'form-control new-file-name' .pull-right - .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} - - .gitignore-selector.hidden - = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { filenames: gitignore_names } } ) - + .license-selector.js-license-selector-wrap.hidden + = dropdown_tag("Choose a License template", options: { toggle_class: 'js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) + .gitignore-selector.js-gitignore-selector-wrap.hidden + = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) + .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden + = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) .encoding-selector = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 87c732626a..4bd8506124 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -20,15 +20,15 @@ protected .controls.hidden-xs - if create_mr_button?(@repository.root_ref, branch.name) - = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do + = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do Merge Request - if branch.name != @repository.root_ref - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do Compare - if can_remove_branch?(@project, branch.name) - = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do + = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") - if branch.name != @repository.root_ref diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index a26f8aeb31..4e2702c2e4 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -48,16 +48,16 @@ - if @build.active? .autoscroll-container %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll - #js-build-scroll.scroll-controls - = link_to '#build-trace', class: 'btn' do - %i.fa.fa-angle-up - = link_to '#down-build-trace', class: 'btn' do - %i.fa.fa-angle-down - if @build.erased? .erased.alert.alert-warning - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} - else + #js-build-scroll.scroll-controls + = link_to '#build-trace', class: 'btn' do + %i.fa.fa-angle-up + = link_to '#down-build-trace', class: 'btn' do + %i.fa.fa-angle-down %pre.build-trace#build-trace %code.bash.js-build-output = icon("refresh spin", class: "js-build-refresh") diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml deleted file mode 100644 index a7a9718109..0000000000 --- a/app/views/projects/buttons/_notifications.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- 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 - .dropdown.hidden-sm - %button.btn.btn-default.notifications-btn#notifications-button{ data: { toggle: "dropdown" }, aria: { haspopup: "true", expanded: "false" } } - = icon('bell') - = notification_title(@notification_setting.level) - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-align-right.dropdown-menu-selectable.dropdown-menu-large{ role: "menu" } - - NotificationSetting.levels.each do |level| - = notification_list_item(level.first, @notification_setting) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index a0ffa06506..e38d1ff5ff 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -24,8 +24,8 @@ %span.label.label-warning stuck %p.commit-title - - if commit_data = pipeline.commit_data - = link_to_gfm truncate(commit_data.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message" + - if commit = pipeline.commit + = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message" - else Cant find HEAD commit for this branch @@ -60,7 +60,7 @@ %li = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do = icon("download") - %span #{build.name} + %span Download '#{build.name}' artifacts - if can?(current_user, :update_pipeline, @project) - if pipeline.retryable? diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 367027182b..929496f81d 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -9,26 +9,31 @@ = cache(cache_key) do %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } - .commit-row-title - %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 ... + = commit_author_avatar(commit, size: 36) + .commit-info-block + .commit-row-title + %span.item-title + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" + %span.commit-row-message.visible-xs-inline + · + = commit.short_id + - if commit.status + = render_commit_status(commit, cssclass: 'visible-xs-inline') + - if commit.description? + %a.text-expander.hidden-xs.js-toggle-button ... - .pull-right - - if commit.status - = render_commit_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" + .commit-actions.hidden-xs + - if commit.status + = render_commit_status(commit, cssclass: 'btn btn-transparent') + = clipboard_button_with_class({ clipboard_text: commit.id }, css_class: 'btn-transparent') + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" + = link_to_browse_code(project, commit) - - if commit.description? - .commit-row-description.js-toggle-content - %pre + - if commit.description? + %pre.commit-row-description.js-toggle-content = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author)) - .commit-row-info - by - = commit_author_link(commit, avatar: true, size: 24) - .committed_ago - #{time_ago_with_tooltip(commit.committed_date)} - = link_to_browse_code(project, commit) + .commit-row-info + = commit_author_link(commit, avatar: false, size: 24) + authored + #{time_ago_with_tooltip(commit.committed_date)} diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 7283a78a64..dd12eae8f7 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -4,18 +4,11 @@ - commits, hidden = limited_commits(@commits) - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| - .row.commits-row - .col-md-2.hidden-xs.hidden-sm - %h5.commits-row-date - %i.fa.fa-calendar - %span= day.strftime('%d %b, %Y') - .light - = pluralize(commits.count, 'commit') - .col-md-10.col-sm-12 - %ul.content-list - = render commits, project: project - %hr.lists-separator + %li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}" + %li.commits-row + %ul.list-unstyled.commit-list + = render commits, project: project - if hidden > 0 - .alert.alert-warning + %li.alert.alert-warning #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index c8aa849c21..54dab4bff0 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,7 +1,8 @@ .scrolling-tabs-container .nav-links.sub-nav.scrolling-tabs %ul{ class: (container_class) } - .fade-left + %li.fade-left + = icon('arrow-left') = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do = link_to project_files_path(@project) do Files @@ -25,4 +26,5 @@ = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do Tags - .fade-right + %li.fade-right + = icon('arrow-right') diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 76ba0bea36..51ca4eb903 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -23,21 +23,18 @@ Create Merge Request .control - = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do - = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false } - + = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do + = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } - if current_user && current_user.private_token .control = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do = icon("rss") - - %ul.breadcrumb.repo-breadcrumb = commits_breadcrumbs %div{id: dom_id(@project)} - #commits-list.content_list= render "commits", project: @project - .clear + %ol#commits-list.list-unstyled.content_list + = render "commits", project: @project = spinner :javascript diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index c322942aeb..b22285c11e 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -3,7 +3,7 @@ = render "projects/commits/head" %div{ class: (container_class) } - .row-content-block.second-block.content-component-block + .sub-header-block Compare branches, tags or commit ranges. %br Fill input field with commit id like diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index cdc34f51d6..f4ec7b767f 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -1,24 +1,24 @@ +- @no_container = true - page_title "#{params[:from]}...#{params[:to]}" = render "projects/commits/head" +%div{ class: (container_class) } + .sub-header-block.no-bottom-space + = render "form" -.row-content-block - = render "form" - -- if @commits.present? - .prepend-top-default + - if @commits.present? = render "projects/commits/commit_list" = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs -- else - .light-well.prepend-top-default - .center - %h4 - There isn't anything to compare. - %p.slead - - if params[:to] == params[:from] - %span.label-branch #{params[:from]} - and - %span.label-branch #{params[:to]} - are the same. - - else - You'll need to use different branch names to get a valid comparison. + - else + .light-well + .center + %h4 + There isn't anything to compare. + %p.slead + - if params[:to] == params[:from] + %span.label-branch #{params[:from]} + and + %span.label-branch #{params[:to]} + are the same. + - else + You'll need to use different branch names to get a valid comparison. diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml index f35faa6afb..10822b6184 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/container_registry/_tag.html.haml @@ -3,9 +3,9 @@ = escape_once(tag.name) = clipboard_button(clipboard_text: "docker pull #{tag.path}") %td - - if layer = tag.layers.first - %span.has-tooltip{ title: "#{layer.revision}" } - = layer.short_revision + - if tag.revision + %span.has-tooltip{ title: "#{tag.revision}" } + = tag.short_revision - else \- %td diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 6c11afbe42..f18bc8c41b 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -11,6 +11,8 @@ = commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs') - elsif current_controller?(:merge_requests) = diff_merge_request_whitespace_link(@project, @merge_request, class: 'hidden-xs') + - elsif current_controller?(:compare) + = diff_compare_whitespace_link(@project, params[:from], params[:to], class: 'hidden-xs') .btn-group = inline_diff_btn = parallel_diff_btn diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 8449fe1e4e..27a94fe02d 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -120,6 +120,42 @@ = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), method: :post, class: "btn btn-save" %hr + .row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + Export project + %p.append-bottom-0 + %p + Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. + %p + Once the exported file is ready, you will receive a notification email with a download link. + + .col-lg-9 + + - if @project.export_project_path + = link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project), + method: :get, class: "btn btn-default" + = link_to 'Generate new export', generate_new_export_namespace_project_path(@project.namespace, @project), + method: :post, class: "btn btn-default" + - else + = link_to 'Export project', export_namespace_project_path(@project.namespace, @project), + method: :post, class: "btn btn-default" + + .bs-callout.bs-callout-info + %p.append-bottom-0 + %p + The following items will be exported: + %ul + %li Project and wiki repositories + %li Project uploads + %li Project configuration including web hooks and services + %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities + %p + The following items will NOT be exported: + %ul + %li Build traces and artifacts + %li LFS objects + %hr - if can? current_user, :archive_project, @project .row.prepend-top-default .col-lg-3 diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index ae9e77e7d8..a03f117291 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -3,16 +3,24 @@ = render "projects/pipelines/head" %div{ class: (container_class) } - - if can?(current_user, :create_environment, @project) + - if can?(current_user, :create_environment, @project) && !@environments.blank? .top-area .nav-controls = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment - if @environments.blank? - %ul.content-list.environments - %li.nothing-here-block - No environments to show + .blank-state.blank-state-no-icon + %h2.blank-state-title + You don't have any environments right now. + %p.blank-state-text + Environments are places where code gets deployed, such as staging or production. + %br + = succeed "." do + = link_to "Read more about environments", help_page_path("ci", "environments") + - if can?(current_user, :create_environment, @project) + = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do + New environment - else .table-holder %table.table.environments diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml index 54465828ba..da325efecd 100644 --- a/app/views/projects/environments/new.html.haml +++ b/app/views/projects/environments/new.html.haml @@ -4,6 +4,9 @@ .col-lg-3 %h4.prepend-top-0 New Environment - %p Environments allow you to track deployments of your application + %p + Environments allow you to track deployments of your application + = succeed "." do + = link_to "Read more about environments", help_page_path("ci", "environments") = render 'form' diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 069b77b5ad..4c15e2759d 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -13,10 +13,14 @@ = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete - if @deployments.blank? - %ul.content-list.environments - %li.nothing-here-block - No deployments for - %strong= @environment.name + .blank-state.blank-state-no-icon + %h2.blank-state-title + You don't have any deployments right now. + %p.blank-state-text + Define environments in the deploy stage(s) in + %code .gitlab-ci.yml + to track deployments here. + = link_to "Read more", help_page_path("ci", "environments"), class: "btn btn-success" - else .table-holder %table.table.environments diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 4bcf2d9d53..dbe9ddfde2 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -40,9 +40,3 @@ = render 'projects', projects: @forks - -- if @private_forks_count > 0 - .private-forks-notice - = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') - %strong= pluralize(@private_forks_count, 'private fork') - %span you have no access to. diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 8becaea246..a388d9a0a6 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,12 +1,14 @@ -- page_specific_javascripts asset_path("graphs/application.js") -%ul.nav-links - = nav_link(action: :show) do - = link_to 'Contributors', namespace_project_graph_path - = nav_link(action: :commits) do - = link_to 'Commits', commits_namespace_project_graph_path - = nav_link(action: :languages) do - = link_to 'Languages', languages_namespace_project_graph_path - - if @project.builds_enabled? - = nav_link(action: :ci) do - = link_to ci_namespace_project_graph_path do - Continuous Integration +.nav-links.sub-nav + %ul{ class: (container_class) } + + - page_specific_javascripts asset_path("graphs/application.js") + = nav_link(action: :show) do + = link_to 'Contributors', namespace_project_graph_path + = nav_link(action: :commits) do + = link_to 'Commits', commits_namespace_project_graph_path + = nav_link(action: :languages) do + = link_to 'Languages', languages_namespace_project_graph_path + - if @project.builds_enabled? + = nav_link(action: :ci) do + = link_to ci_namespace_project_graph_path do + Continuous Integration diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml index 19ccc125ea..e695d3ae36 100644 --- a/app/views/projects/graphs/ci.html.haml +++ b/app/views/projects/graphs/ci.html.haml @@ -1,15 +1,18 @@ +- @no_container = true - page_title "Continuous Integration", "Graphs" = render 'head' -.row-content-block.append-bottom-default - .oneline - A collection of graphs for Continuous Integration -#charts.ci-charts - .row - .col-md-6 - = render 'projects/graphs/ci/overall' - .col-md-6 - = render 'projects/graphs/ci/build_times' +%div{ class: (container_class) } + .sub-header-block + .oneline + A collection of graphs for Continuous Integration - %hr - = render 'projects/graphs/ci/builds' + #charts.ci-charts + .row + .col-md-6 + = render 'projects/graphs/ci/overall' + .col-md-6 + = render 'projects/graphs/ci/build_times' + + %hr + = render 'projects/graphs/ci/builds' diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index d9b2fb6c06..0daffe68f6 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -1,52 +1,54 @@ +- @no_container = true - page_title "Commits", "Graphs" = render 'head' -.row-content-block.append-bottom-default - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'graphs_commits' - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs +%div{ class: (container_class) } + .sub-header-block + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'graphs_commits' + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs -%p.lead - Commit statistics for - %strong #{@ref} - #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} + %p.lead + Commit statistics for + %strong #{@ref} + #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} -.row - .col-md-6 - %ul - %li - %p.lead - %strong #{@commits_graph.commits.size} - commits during - %strong #{@commits_graph.duration} - days - %li - %p.lead - Average - %strong #{@commits_graph.commit_per_day} - commits per day - %li - %p.lead - Contributed by - %strong #{@commits_graph.authors} - authors - .col-md-6 - %div - %p.slead - Commits per day of month - %canvas#month-chart -.row - .col-md-6 - %div - %p.slead - Commits per day hour (UTC) - %canvas#hour-chart - .col-md-6 - %div - %p.slead - Commits per weekday - %canvas#weekday-chart + .row + .col-md-6 + %ul + %li + %p.lead + %strong #{@commits_graph.commits.size} + commits during + %strong #{@commits_graph.duration} + days + %li + %p.lead + Average + %strong #{@commits_graph.commit_per_day} + commits per day + %li + %p.lead + Contributed by + %strong #{@commits_graph.authors} + authors + .col-md-6 + %div + %p.slead + Commits per day of month + %canvas#month-chart + .row + .col-md-6 + %div + %p.slead + Commits per day hour (UTC) + %canvas#hour-chart + .col-md-6 + %div + %p.slead + Commits per weekday + %canvas#weekday-chart :javascript var responsiveChart = function (selector, data) { diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml index 249c16f470..6d97f552a8 100644 --- a/app/views/projects/graphs/languages.html.haml +++ b/app/views/projects/graphs/languages.html.haml @@ -1,24 +1,26 @@ +- @no_container = true - page_title "Languages", "Graphs" = render 'head' -.row-content-block.append-bottom-default - .oneline - Programming languages used in this repository +%div{ class: (container_class) } + .sub-header-block + .oneline + Programming languages used in this repository -.row - .col-md-8 - %canvas#languages-chart{ height: 400 } - .col-md-4 - %ul.bordered-list - - @languages.each do |language| - %li - %span{ style: "color: #{language[:color]}" } - = icon('circle') - - = language[:label] - .pull-right - = language[:value] - \% + .row + .col-md-8 + %canvas#languages-chart{ height: 400 } + .col-md-4 + %ul.bordered-list + - @languages.each do |language| + %li + %span{ style: "color: #{language[:color]}" } + = icon('circle') + + = language[:label] + .pull-right + = language[:value] + \% :javascript var data = #{@languages.to_json}; diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 33970e7b90..9f7e2a361f 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,29 +1,31 @@ +- @no_container = true - page_title "Contributors", "Graphs" = render 'head' -.row-content-block.append-bottom-default - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'graphs' - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs +%div{ class: (container_class) } + .sub-header-block + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'graphs' + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs -.loading-graph - .center - %h3.page-title - %i.fa.fa-spinner.fa-spin - Building repository graph. - %p.slead Please wait a moment, this page will automatically refresh when ready. + .loading-graph + .center + %h3.page-title + %i.fa.fa-spinner.fa-spin + Building repository graph. + %p.slead Please wait a moment, this page will automatically refresh when ready. -.stat-graph.hide - .header.clearfix - %h3#date_header.page-title - %p.light - Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. - %input#brush_change{:type => "hidden"} - .graphs - #contributors-master - #contributors.clearfix - %ol.contributors-list.clearfix + .stat-graph.hide + .header.clearfix + %h3#date_header.page-title + %p.light + Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. + %input#brush_change{:type => "hidden"} + .graphs.row + #contributors-master + #contributors.clearfix + %ol.contributors-list.clearfix diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml index c0d1ce0d12..4d8ee562e6 100644 --- a/app/views/projects/imports/show.html.haml +++ b/app/views/projects/imports/show.html.haml @@ -7,7 +7,7 @@ Forking in progress. - else Import in progress. - - unless @project.forked? + - if @project.external_import? %p.monospace git clone --bare #{@project.safe_import_url} %p Please wait while we import the repository for you. Refresh at will. :javascript diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index b151393aba..c2f4457b60 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,7 +1,7 @@ - content_for :note_actions do - 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, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-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, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-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, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' #notes = render 'projects/notes/notes_with_form' diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 6e1baa46b0..aa4d69550e 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -4,9 +4,10 @@ = render "projects/issues/head" %div{ class: (container_class) } - .top-area + .top-area.adjust .nav-text - Labels can be applied to issues and merge requests. + Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. + .nav-controls - if can?(current_user, :admin_label, @project) = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do @@ -19,10 +20,9 @@ .prioritized-labels{ class: ('hide' if hide) } %h5 Prioritized Labels %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } + %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet - if @prioritized_labels.present? = render @prioritized_labels - - else - %p.empty-message No prioritized labels yet .other-labels - if can?(current_user, :admin_label, @project) %h5{ class: ('hide' if hide) } Other Labels diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 393998f15b..53dd300c35 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -1,8 +1,8 @@ - content_for :note_actions do - if can?(current_user, :update_merge_request, @merge_request) - if @merge_request.open? - = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"} + = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"} - if @merge_request.closed? - = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"} + = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"} #notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index b08524574e..de39964fca 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -21,7 +21,7 @@ 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" } + = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" } .dropdown-menu.dropdown-menu-selectable.dropdown-source-branch = dropdown_title("Select source branch") = dropdown_filter("Search branches") diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index c4df8bd504..2ec96308fd 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -17,11 +17,11 @@ = link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do Check out branch - %span.dropdown + %span.dropdown.inline.prepend-left-5 %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } Download as %span.caret - %ul.dropdown-menu + %ul.dropdown-menu.dropdown-menu-align-right %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) .normal @@ -37,7 +37,7 @@ = render "projects/merge_requests/widget/show.html.haml" - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) - .light.prepend-top-default + .light.prepend-top-default.append-bottom-default You can also accept this merge request manually using the = succeed '.' do = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index a8f09f855d..0b05785430 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -2,4 +2,5 @@ = icon("sort-amount-desc") Most recent commits displayed first -= render "projects/commits/commits", project: @merge_request.project +%ol#commits-list.list-unstyled + = render "projects/commits/commits", project: @merge_request.project diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index 0dbd159298..b3bea900d4 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -8,7 +8,7 @@ %p %strong Step 1. Fetch and check out the branch for this merge request - = clipboard_button(clipboard_target: 'pre#merge-info-1') + = clipboard_button_with_class({clipboard_target: "pre#merge-info-1"}, css_class: "btn-clipboard") %pre.dark#merge-info-1 - if @merge_request.for_fork? :preserve @@ -25,7 +25,7 @@ %p %strong Step 3. Merge the branch and fix any conflicts that come up - = clipboard_button(clipboard_target: 'pre#merge-info-3') + = clipboard_button_with_class({clipboard_target: "pre#merge-info-3"}, css_class: "btn-clipboard") %pre.dark#merge-info-3 - if @merge_request.for_fork? :preserve @@ -38,7 +38,7 @@ %p %strong Step 4. Push the result of the merge to GitLab - = clipboard_button(clipboard_target: 'pre#merge-info-4') + = clipboard_button_with_class({clipboard_target: "pre#merge-info-4"}, css_class: "btn-clipboard") %pre.dark#merge-info-4 :preserve git push origin #{h @merge_request.target_branch} diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index f5e2b927da..cbf1ba0417 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -19,6 +19,7 @@ = f.label :due_date, "Due Date", class: "control-label" .col-sm-10 = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" + %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date .form-actions - if @milestone.new_record? @@ -27,10 +28,3 @@ -else = f.submit 'Save changes', class: "btn-save btn" = link_to "Cancel", namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-cancel" - - -:javascript - $(".datepicker").datepicker({ - dateFormat: "yy-mm-dd", - onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } - }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index bf9baaea88..e4ab064eda 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,4 +1,5 @@ - page_title "Network", @ref +- page_specific_javascripts asset_path("network/application.js") = render "projects/commits/head" = render "head" %div{ class: (container_class) } @@ -14,14 +15,5 @@ = check_box_tag :filter_ref, 1, @options[:filter_ref] %span Begin with the selected commit - .network-graph + .network-graph{ data: { url: '#{escape_javascript(@url)}', commit_url: '#{escape_javascript(@commit_url)}', ref: '#{escape_javascript(@ref)}', commit_id: '#{escape_javascript(@commit.id)}' } } = spinner nil, true - -:javascript - network_graph = new Network({ - url: "#{escape_javascript(@url)}", - commit_url: "#{escape_javascript(@commit_url)}", - ref: "#{escape_javascript(@ref)}", - commit_id: '#{@commit.id}' - }) - new ShortcutsNetwork(network_graph.branch_graph) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index f9ac16b32f..3c1c606050 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -11,26 +11,22 @@ .project-edit-content = form_for @project, html: { class: 'new_project form-horizontal js-requires-input' } do |f| - .form-group.project-name-holder + .form-group = f.label :path, class: 'control-label' do - Project path + Project owner .col-sm-10 - .input-group - - if current_user.can_select_namespace? - .input-group-addon - = root_url - = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1} - .input-group-addon - \/ - - else - .input-group-addon - #{root_url}#{current_user.username}/ - = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true - + = f.select :namespace_id, namespaces_options(:current_user), {}, {class: 'select2 js-select-namespace', tabindex: 1} + - if current_user.can_create_group? .help-block Want to house several dependent projects under the same namespace? = link_to "Create a group", new_group_path + + .form-group + = f.label :path, class: 'control-label' do + Project name + .col-sm-10 + = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true - if import_sources_enabled? .project-import.js-toggle-container @@ -88,7 +84,12 @@ - if git_import_enabled? = link_to "#", class: 'btn js-toggle-button import_git' do %i.fa.fa-git - %span Any repo by URL + %span Repo by URL + + - if gitlab_project_import_enabled? + = link_to new_import_gitlab_project_path, class: 'btn import_gitlab_project project-submit' do + %i.fa.fa-gitlab + %span GitLab export .js-toggle-content.hide = render "shared/import_form", f: f @@ -119,6 +120,33 @@ e.preventDefault(); var import_modal = $(this).next(".modal").show(); }); + $('.modal-header .close').bind('click', function() { $(".modal").hide(); }); + + $('.import_gitlab_project').bind('click', function() { + var _href = $("a.import_gitlab_project").attr("href"); + $(".import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val()); + }); + + $('.import_gitlab_project').attr('disabled',true) + $('.import_gitlab_project').attr('title', 'Project path required.'); + + $('.import_gitlab_project').click(function( event ) { + if($('.import_gitlab_project').attr('disabled')) { + event.preventDefault(); + new Flash("Please enter a path for the project to be imported to."); + } + }); + + $('#project_path').keyup(function(){ + if($(this).val().length !=0) { + $('.import_gitlab_project').attr('disabled', false); + $('.import_gitlab_project').attr('title',''); + $(".flash-container").html("") + } else { + $('.import_gitlab_project').attr('disabled',true); + $('.import_gitlab_project').attr('title', 'Project path required.'); + } + }) diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index c87a3fadf7..8620f49228 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -6,6 +6,6 @@ = render 'projects/notes/hints' .note-form-actions.clearfix - = f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button' + = f.submit 'Save Comment', class: 'btn btn-nr btn-save js-comment-button' %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' } Cancel diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 67ed38a7b2..03b3f6935d 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -14,7 +14,7 @@ .error-alert .note-form-actions.clearfix - = f.submit 'Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button" + = f.submit 'Comment', class: "btn btn-nr btn-create append-right-10 comment-btn js-comment-button" = yield(:note_actions) %a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}} Discard draft diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index bcdbff0801..c04d291412 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -18,9 +18,9 @@ = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') .note-actions - access = note.project.team.human_max_access(note.author.id) - - if access + - if access and not note.system %span.note-role.hidden-xs= access - - if current_user + - if current_user and not note.system = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do = icon('spinner spin') = icon('smile-o') diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 2fb3a41d54..45f8ef8906 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,2 +1,2 @@ :plain - $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member))}'); + $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}'); diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index d62f5c8f13..c45a9d4f81 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -12,6 +12,12 @@ .checkbox = f.check_box :run_untagged %span.light Indicates whether this runner can pick jobs without tags + .form-group + = label :locked, 'Lock to current projects', class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :locked + %span.light When a runner is locked, it cannot be assigned to other projects .form-group = label_tag :token, class: 'control-label' do Token diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 96e2aac451..8522585775 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -2,8 +2,10 @@ %h4 = runner_status_icon(runner) %span.monospace - - if @runners.include?(runner) + - if @project_runners.include?(runner) = link_to runner.short_sha, runner_path(runner) + - if runner.locked? + = icon('lock', class: 'has-tooltip', title: 'Locked to current projects') %small = link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do %i.fa.fa-edit.btn @@ -11,7 +13,7 @@ = runner.short_sha .pull-right - - if @runners.include?(runner) + - if @project_runners.include?(runner) - if runner.belongs_to_one_project? = link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - else diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 8ae9f0d95f..d469dda5b8 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -17,13 +17,13 @@ Start runner! -- if @runners.any? +- if @project_runners.any? %h4.underlined-title Runners activated for this project %ul.bordered-list.activated-specific-runners - = render partial: 'runner', collection: @runners, as: :runner + = render partial: 'runner', collection: @project_runners, as: :runner -- if @specific_runners.any? +- if @assignable_runners.any? %h4.underlined-title Available specific runners %ul.bordered-list.available-specific-runners - = render partial: 'runner', collection: @specific_runners, as: :runner - = paginate @specific_runners + = render partial: 'runner', collection: @assignable_runners, as: :runner + = paginate @assignable_runners diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml index f24e1b9144..61b99f35d7 100644 --- a/app/views/projects/runners/show.html.haml +++ b/app/views/projects/runners/show.html.haml @@ -22,6 +22,9 @@ %tr %td Can run untagged jobs %td= @runner.run_untagged? ? 'Yes' : 'No' + %tr + %td Locked to this project + %td= @runner.locked? ? 'Yes' : 'No' %tr %td Tags %td diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 4afa902b4e..15f0d85194 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -23,10 +23,10 @@ #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)}) %li = link_to namespace_project_branches_path(@project.namespace, @project) do - #{'Branch'.pluralize(@repository.branch_names.count)} (#{number_with_delimiter(@repository.branch_names.count)}) + #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) %li = link_to namespace_project_tags_path(@project.namespace, @project) do - #{'Tag'.pluralize(@repository.tag_names.count)} (#{number_with_delimiter(@repository.tag_names.count)}) + #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) - if default_project_view != 'readme' && @repository.readme %li @@ -57,6 +57,10 @@ %li.missing = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do Add Contribution guide + - unless @repository.gitlab_ci_yml + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do + Set Up CI - if @repository.commit .content-block.second-block.white diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 844e105581..2c11c0e5b2 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -15,7 +15,7 @@ = render 'projects/tags/download', ref: tag.name, project: @project - if can?(current_user, :push_code, @project) - = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes" do + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do = icon("pencil") - if can?(current_user, :admin_project, @project) diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 2779084fe3..4ca1f58ac5 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -11,12 +11,23 @@ .nav-controls = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do New tag + .dropdown.inline + %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } + %span.light= @sort.humanize + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + %li + = link_to namespace_project_tags_path(sort: nil) do + Name + = link_to namespace_project_tags_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to namespace_project_tags_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated .tags - unless @tags.empty? %ul.content-list - - @tags.each do |tag| - = render 'tag', tag: @repository.find_tag(tag) + = render partial: 'tag', collection: @tags = paginate @tags, theme: 'gitlab' diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml index 2ddc5d504f..a3a4dba3fa 100644 --- a/app/views/projects/tree/_blob_item.html.haml +++ b/app/views/projects/tree/_blob_item.html.haml @@ -1,8 +1,9 @@ %tr{ class: "tree-item #{tree_hex_class(blob_item)}" } %td.tree-item-file-name = tree_icon(type, blob_item.mode, blob_item.name) - %span.str-truncated - = link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)) + - file_name = blob_item.name + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)), title: file_name do + %span.str-truncated= file_name %td.tree_time_ago.cgray = render 'projects/tree/spinner' %td.hidden-xs.tree_commit diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index cf65057e70..9577696fc0 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -1,9 +1,9 @@ %tr{ class: "tree-item #{tree_hex_class(tree_item)}" } %td.tree-item-file-name = tree_icon(type, tree_item.mode, tree_item.name) - %span.str-truncated - - path = flatten_tree(tree_item) - = link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)) + - path = flatten_tree(tree_item) + = link_to namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)), title: path do + %span.str-truncated= path %td.tree_time_ago.cgray = render 'projects/tree/spinner' %td.hidden-xs.tree_commit diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 4faa547769..4ea75dbbf0 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,4 +1,7 @@ - if (@page && @page.persisted?) + - if can?(current_user, :create_wiki, @project) + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + New Page = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do Page History - if can?(current_user, :create_wiki, @project) diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index 988fe024e2..f8ea479e0b 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -1,5 +1,5 @@ -.top-area - %ul.nav-links +.nav-links.sub-nav + %ul{ class: (container_class) } = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) @@ -10,9 +10,4 @@ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do Git Access - .nav-controls - - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - New Page - -= render 'projects/wikis/new' + = render 'projects/wikis/new' diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index 919daf0a7b..4f8abcdc8e 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -1,14 +1,17 @@ -%div#modal-new-wiki.modal - .modal-dialog - .modal-content - .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × - %h3.page-title New Wiki Page - .modal-body - %form.new-wiki-page - .form-group - = label_tag :new_wiki_path do - %span Page slug - = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true - .form-actions - = button_tag 'Create Page', class: 'build-new-wiki btn btn-create' +- @no_container = true + +%div{ class: (container_class) } + %div#modal-new-wiki.modal + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3.page-title New Wiki Page + .modal-body + %form.new-wiki-page + .form-group + = label_tag :new_wiki_path do + %span Page slug + = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true + .form-actions + = button_tag 'Create Page', class: 'build-new-wiki btn btn-create' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index cbd69ee1a7..bf5d09d50c 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,19 +1,24 @@ +- @no_container = true - page_title "Edit", @page.title.capitalize, "Wiki" = render 'nav' -.top-area - .nav-text.wiki-page - %strong - - if @page.persisted? - = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) - - else - = @page.title.capitalize - %span.light - · - Edit Page +%div{ class: (container_class) } + .top-area + .nav-text + %strong + - if @page.persisted? + = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) + - else + = @page.title.capitalize + %span.light + · + Edit Page - .nav-controls - = render 'main_links' + .nav-controls + - if can?(current_user, :create_wiki, @project) + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + New Page + = render 'main_links' -= render 'form' + = render 'form' diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index ccceab6155..6caf7230f3 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,32 +1,34 @@ +- @no_container = true - page_title "Git Access", "Wiki" = render 'nav' -.row-content-block - %span.oneline - Git access for - %strong= @project_wiki.path_with_namespace +%div{ class: (container_class) } + .sub-header-block + %span.oneline + Git access for + %strong= @project_wiki.path_with_namespace - .pull-right - = render "shared/clone_panel", project: @project_wiki + .pull-right + = render "shared/clone_panel", project: @project_wiki -.git-empty.prepend-top-default - %fieldset - %legend Install Gollum: - %pre.dark - :preserve - gem install gollum + .prepend-top-default + %fieldset + %legend Install Gollum: + %pre.dark + :preserve + gem install gollum - %legend Clone Your Wiki: - %pre.dark - :preserve - git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')} - cd #{h @project_wiki.path} + %legend Clone Your Wiki: + %pre.dark + :preserve + git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')} + cd #{h @project_wiki.path} - %legend Start Gollum And Edit Locally: - %pre.dark - :preserve - gollum - == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin - >> Thin web server (v1.5.0 codename Knife) - >> Maximum connections set to 1024 - >> Listening on 0.0.0.0:4567, CTRL+C to stop + %legend Start Gollum And Edit Locally: + %pre.dark + :preserve + gollum + == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin + >> Thin web server (v1.5.0 codename Knife) + >> Maximum connections set to 1024 + >> Listening on 0.0.0.0:4567, CTRL+C to stop diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 45460ed9f4..630ee35b70 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,37 +1,37 @@ - page_title "History", @page.title.capitalize, "Wiki" = render 'nav' +%div{ class: (container_class) } + .top-area + .nav-text + %strong + = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) + %span.light + · + History -.top-area - .nav-text - %strong - = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) - %span.light - · - History - -.table-holder - %table.table - %thead - %tr - %th Page version - %th Author - %th Commit Message - %th Last updated - %th Format - %tbody - - @page.versions.each_with_index do |version, index| - - commit = version + .table-holder + %table.table + %thead %tr - %td - = link_to project_wiki_path_with_version(@project, @page, - commit.id, index == 0) do - = truncate_sha(commit.id) - %td - = commit.author.name - %td - = commit.message - %td - #{time_ago_with_tooltip(version.authored_date)} - %td - %strong - = @page.page.wiki.page(@page.page.name, commit.id).try(:format) + %th Page version + %th Author + %th Commit Message + %th Last updated + %th Format + %tbody + - @page.versions.each_with_index do |version, index| + - commit = version + %tr + %td + = link_to project_wiki_path_with_version(@project, @page, + commit.id, index == 0) do + = truncate_sha(commit.id) + %td + = commit.author.name + %td + = commit.message + %td + #{time_ago_with_tooltip(version.authored_date)} + %td + %strong + = @page.page.wiki.page(@page.page.name, commit.id).try(:format) diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 2f6162fa3c..81d9f391c1 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,12 +1,14 @@ +- @no_container = true - page_title "Pages", "Wiki" = render 'nav' -%ul.content-list - - @wiki_pages.each do |wiki_page| - %li - = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) - %small (#{wiki_page.format}) - .pull-right - %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} -= paginate @wiki_pages, theme: 'gitlab' +%div{ class: (container_class) } + %ul.content-list + - @wiki_pages.each do |wiki_page| + %li + = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) + %small (#{wiki_page.format}) + .pull-right + %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} + = paginate @wiki_pages, theme: 'gitlab' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 9166c0edb3..76f9b1ecd7 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,24 +1,26 @@ +- @no_container = true - page_title @page.title.capitalize, "Wiki" = render 'nav' -.top-area - .nav-text - %strong= @page.title.capitalize +%div{ class: (container_class) } + .top-area + .nav-text + %strong= @page.title.capitalize - %span.wiki-last-edit-by - · - last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + %span.wiki-last-edit-by + · + last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} - .nav-controls - = render 'main_links' + .nav-controls + = render 'main_links' -- if @page.historical? - .warning_message - This is an old version of this page. - You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}. + - if @page.historical? + .warning_message + This is an old version of this page. + You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}. -.wiki-holder.prepend-top-default.append-bottom-default - .wiki - = preserve do - = render_wiki_content(@page) + .wiki-holder.prepend-top-default.append-bottom-default + .wiki + = preserve do + = render_wiki_content(@page) diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index 3005500221..aa18e6f236 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,7 +1,9 @@ %ul.nav-links.event-filter.scrolling-tabs - .fade-left + %li.fade-left + = icon('arrow-left') = event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.team, 'Team' - .fade-right + %li.fade-right + = icon('arrow-right') diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 478c04318c..77676454b5 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,5 +1,7 @@ %span.label-row - if can?(current_user, :admin_label, @project) + .draggable-handler + = icon('bars') .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label), dom_id: dom_id(label) } } %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' } diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index eb2e1919e1..ea7162d4d6 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,7 +1,14 @@ +- dropdown_toggle_text = @ref || @project.default_branch = form_tag switch_namespace_project_refs_path(@project.namespace, @project), method: :get, class: "project-refs-form" do - = select_tag "ref", grouped_options_refs, class: "project-refs-select select2 select2-sm" = hidden_field_tag :destination, destination - if defined?(path) = hidden_field_tag :path, path - @options && @options.each do |key, value| = hidden_field_tag key, value, id: nil + .dropdown + = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project) }, { toggle_class: "js-project-refs-dropdown" } + .dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } + = dropdown_title "Switch branch/tag" + = dropdown_filter "Search branches and tags" + = dropdown_content + = dropdown_loading diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 380ab465bf..094d6636c6 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -12,7 +12,7 @@ - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", - placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author], field_name: "author_id", default_label: "Author" } }) + placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - if params[:assignee_id].present? diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 539c4f3630..adfab1af53 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,4 +1,4 @@ -- todo = has_todo(issuable) +- todo = issuable_todo(issuable) %aside.right-sidebar{ class: sidebar_gutter_collapsed_class } .issuable-sidebar - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) @@ -9,12 +9,12 @@ %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } } = sidebar_gutter_toggle_icon - if current_user - %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", id: (todo.id unless todo.nil?), issuable: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project) } } + %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } } %span.js-issuable-todo-text - - if todo.nil? - Add Todo - - else + - if todo Mark Done + - else + Add Todo = icon('spin spinner', class: 'hidden js-issuable-todo-loading') = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| @@ -29,20 +29,21 @@ = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.bold.hide-collapsed + .value.hide-collapsed - if issuable.assignee - = link_to_member(@project, issuable.assignee, size: 32) do + = link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee) %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' } = icon('exclamation-triangle') %span.username = issuable.assignee.to_reference - else - %span.assign-yourself + %span.assign-yourself.no-value No assignee - if can_edit_issuable + \- %a.js-assign-yourself{ href: '#' } - \- assign yourself + assign yourself .selectbox.hide-collapsed = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id' @@ -62,13 +63,11 @@ = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.bold.hide-collapsed + .value.hide-collapsed - if issuable.milestone - = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do - %span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1}} - = issuable.milestone.title + = link_to issuable.milestone.title, namespace_project_milestone_path(@project.namespace, @project, issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 } - else - .light None + %span.no-value None .selectbox.hide-collapsed = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil @@ -85,14 +84,14 @@ = 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 + .value.hide-collapsed %span.value-content - if issuable.due_date - = issuable.due_date.to_s(:medium) + %span.bold= issuable.due_date.to_s(:medium) - else - None + %span.no-value No due date - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - %span.light.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) } + %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) } \- %a.js-remove-due-date{ href: "#", role: "button" } remove due date @@ -124,7 +123,7 @@ - issuable.labels_array.each do |label| = link_to_label(label, type: issuable.to_ability_name) - else - .light None + %span.no-value None .selectbox.hide-collapsed - issuable.labels_array.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml index ed0a6ebcf8..480e8ba6c8 100644 --- a/app/views/shared/members/_access_request_buttons.html.haml +++ b/app/views/shared/members/_access_request_buttons.html.haml @@ -1,12 +1,14 @@ - member = source.members.find_by(user_id: current_user.id) +- group_member = source.group.members.find_by(user_id: current_user.id) if source.respond_to?(:group) && source.group -- if member - - if member.request? - = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), - method: :delete, - data: { confirm: remove_member_message(member) }, +- unless group_member + - if member + - if member.request? + = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), + method: :delete, + data: { confirm: remove_member_message(member) }, + class: 'btn access-request-button hidden-xs' + - else + = link_to 'Request Access', polymorphic_path([:request_access, source, :members]), + method: :post, class: 'btn access-request-button hidden-xs' -- else - = link_to 'Request Access', polymorphic_path([:request_access, source, :members]), - method: :post, - class: 'btn access-request-button hidden-xs' diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index c69d4cbfbe..a884e78e6e 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -1,4 +1,4 @@ -- show_roles = local_assigns.fetch(:show_roles, true) +- show_roles = local_assigns.fetch(:show_roles, default_show_roles(member)) - show_controls = local_assigns.fetch(:show_controls, true) - user = member.user @@ -36,7 +36,7 @@ method: :post, class: 'btn-xs btn' - - if show_roles && can_see_member_roles?(source: member.source, user: current_user) + - if show_roles %span.pull-right %strong= member.human_access - if show_controls diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml index 385c659660..975c74f4ea 100644 --- a/app/views/shared/milestones/_summary.html.haml +++ b/app/views/shared/milestones/_summary.html.haml @@ -10,6 +10,13 @@ open and %strong= milestone.issues_visible_to_user(current_user).closed.size closed + %strong= milestone.merge_requests.size + merge requests: + %span.milestone-stat + %strong= milestone.merge_requests.opened.size + open and + %strong= milestone.merge_requests.merged.size + merged %span.milestone-stat %strong== #{milestone.percent_complete(current_user)}% complete diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml new file mode 100644 index 0000000000..ff1cf966a9 --- /dev/null +++ b/app/views/shared/notifications/_button.html.haml @@ -0,0 +1,25 @@ +- left_align = local_assigns[:left_align] +- if notification_setting + .dropdown.notification-dropdown.pull-right + = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f| + = hidden_setting_source_input(notification_setting) + = f.hidden_field :level, class: "notification_setting_level" + .js-notification-toggle-btns + %div{ class: ("btn-group" if notification_setting.custom?) } + - if notification_setting.custom? + %button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } } + = icon("bell", class: "js-notification-loading") + = notification_title(notification_setting.level) + %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } + %span.caret + .sr-only Toggle dropdown + - else + %button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } + = icon("bell", class: "js-notification-loading") + = notification_title(notification_setting.level) + = icon("caret-down") + + = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align + + = content_for :scripts_body do + = render "shared/notifications/custom_notifications", notification_setting: notification_setting diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml new file mode 100644 index 0000000000..b704981e3d --- /dev/null +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -0,0 +1,31 @@ +.modal.fade{ tabindex: "-1", role: "dialog", id: notifications_menu_identifier("modal", notification_setting), aria: { labelledby: "custom-notifications-title" } } + .modal-dialog + .modal-content + .modal-header + %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } } + %span{ aria: { hidden: "true" } } × + %h4#custom-notifications-title.modal-title + Custom notification events + + .modal-body + .container-fluid + = form_for notification_setting, html: { class: "custom-notifications-form" } do |f| + = hidden_setting_source_input(notification_setting) + .row + .col-lg-4 + %h4.prepend-top-0 + Notification events + %p + Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out + = succeed "." do + %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank"} notification emails + .col-lg-8 + - NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index| + - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]" + .form-group + .checkbox{ class: ("prepend-top-0" if index == 0) } + %label{ for: field_id } + = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event]) + %strong + = event.to_s.humanize + = icon("spinner spin", class: "custom-notification-event-loading") diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml new file mode 100644 index 0000000000..d3258ee64c --- /dev/null +++ b/app/views/shared/notifications/_notification_dropdown.html.haml @@ -0,0 +1,13 @@ +- left_align = local_assigns[:left_align] +%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] } + - NotificationSetting.levels.each_key do |level| + - next if level == "custom" + - next if level == "global" && notification_setting.source.nil? + + = notification_list_item(level, notification_setting) + + %li.divider + %li + %a.update-notification{ href: "#", role: "button", class: ("is-active" if notification_setting.custom?), data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), notification_level: "custom", notification_title: "Custom" } } + %strong.dropdown-menu-inner-title Custom + %span.dropdown-menu-inner-content= notification_description("custom") diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 2e08bb2ac0..3a9dd37dc7 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -16,6 +16,12 @@ = render "shared/projects/project", project: project, skip_namespace: skip_namespace, avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar, forks: forks, show_last_commit_as_description: show_last_commit_as_description + + - if @private_forks_count && @private_forks_count > 0 + %li.project-row.private-forks-notice + = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') + %strong= pluralize(@private_forks_count, 'private fork') + %span you have no access to. = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages - else .nothing-here-block No projects found diff --git a/app/workers/gitlab_remove_project_export_worker.rb b/app/workers/gitlab_remove_project_export_worker.rb new file mode 100644 index 0000000000..1d91897d52 --- /dev/null +++ b/app/workers/gitlab_remove_project_export_worker.rb @@ -0,0 +1,9 @@ +class GitlabRemoveProjectExportWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform + Project.remove_gitlab_exports! + end +end diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb new file mode 100644 index 0000000000..39f6037e07 --- /dev/null +++ b/app/workers/project_export_worker.rb @@ -0,0 +1,12 @@ +class ProjectExportWorker + include Sidekiq::Worker + + sidekiq_options queue: :gitlab_shell, retry: true + + def perform(current_user_id, project_id) + current_user = User.find(current_user_id) + project = Project.find(project_id) + + ::Projects::ImportExport::ExportService.new(project, current_user).execute + end +end diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb index f2d12ba5a7..98ddf5d068 100644 --- a/app/workers/repository_check/single_repository_worker.rb +++ b/app/workers/repository_check/single_repository_worker.rb @@ -15,7 +15,7 @@ module RepositoryCheck private def check(project) - if !git_fsck(project.repository) + if has_pushes?(project) && !git_fsck(project.repository) false elsif project.wiki_enabled? # Historically some projects never had their wiki repos initialized; @@ -44,5 +44,9 @@ module RepositoryCheck false end end + + def has_pushes?(project) + Project.with_push.exists?(project.id) + end end end diff --git a/config/application.rb b/config/application.rb index 49d4d3ba55..05fec995ed 100644 --- a/config/application.rb +++ b/config/application.rb @@ -83,6 +83,7 @@ module Gitlab config.assets.precompile << "mailers/*.css" config.assets.precompile << "graphs/application.js" config.assets.precompile << "users/application.js" + config.assets.precompile << "network/application.js" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 916fd33e76..c6dc1e4ab3 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -215,7 +215,7 @@ Settings.gitlab.default_projects_features['container_registry'] = true if Settin Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) 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['import_sources'] ||= %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project] Settings.gitlab['trusted_proxies'] ||= [] @@ -291,6 +291,9 @@ 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' +Settings.cron_jobs['gitlab_remove_project_export_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['gitlab_remove_project_export_worker']['cron'] ||= '0 * * * *' +Settings.cron_jobs['gitlab_remove_project_export_worker']['job_class'] = 'GitlabRemoveProjectExportWorker' # # GitLab Shell diff --git a/config/initializers/default_url_options.rb b/config/initializers/default_url_options.rb index 8fd27b1d88..de2cdc6eca 100644 --- a/config/initializers/default_url_options.rb +++ b/config/initializers/default_url_options.rb @@ -9,3 +9,4 @@ unless Gitlab.config.gitlab_on_standard_port? end Rails.application.routes.default_url_options = default_url_options +ActionMailer::Base.asset_host = Settings.gitlab['base_url'] diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index f1eec67488..7a2b9a7f6c 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -23,6 +23,10 @@ Sidekiq.configure_server do |config| config['pool'] = Sidekiq.options[:concurrency] + 2 ActiveRecord::Base.establish_connection(config) Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}") + + # Avoid autoload issue such as 'Mail::Parsers::AddressStruct' + # https://github.com/mikel/mail/issues/912#issuecomment-214850355 + Mail.eager_autoload! end Sidekiq.configure_client do |config| diff --git a/config/routes.rb b/config/routes.rb index d52cbb2242..e45293cdf7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -123,9 +123,17 @@ Rails.application.routes.draw do end end + # # Spam reports + # resources :abuse_reports, only: [:new, :create] + # + # Notification settings + # + resources :notification_settings, only: [:create, :update] + + # # Import # @@ -171,6 +179,10 @@ Rails.application.routes.draw do get :new_user_map, path: :user_map post :create_user_map, path: :user_map end + + resource :gitlab_project, only: [:create, :new] do + post :create + end end # @@ -283,7 +295,7 @@ Rails.application.routes.draw do post :repository_check end - resources :runner_projects + resources :runner_projects, only: [:create, :destroy] end end @@ -348,6 +360,13 @@ Rails.application.routes.draw do resources :keys resources :emails, only: [:index, :create, :destroy] resource :avatar, only: [:destroy] + + resources :personal_access_tokens, only: [:index, :create] do + member do + put :revoke + end + end + resource :two_factor_auth, only: [:show, :create, :destroy] do member do post :create_u2f @@ -421,7 +440,6 @@ 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 @@ -455,8 +473,13 @@ Rails.application.routes.draw do post :housekeeping post :toggle_star post :markdown_preview + post :export + post :remove_export + post :generate_new_export + get :download_export get :autocomplete_sources get :activity + get :refs end scope module: :projects do @@ -653,7 +676,6 @@ 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 @@ -797,7 +819,7 @@ Rails.application.routes.draw do end end - resources :todos, only: [:create, :update], constraints: { id: /\d+/ } + resources :todos, only: [:create] resources :uploads, only: [:create] do collection do diff --git a/db/migrate/20160415062917_create_personal_access_tokens.rb b/db/migrate/20160415062917_create_personal_access_tokens.rb new file mode 100644 index 0000000000..ce0b33f32b --- /dev/null +++ b/db/migrate/20160415062917_create_personal_access_tokens.rb @@ -0,0 +1,13 @@ +class CreatePersonalAccessTokens < ActiveRecord::Migration + def change + create_table :personal_access_tokens do |t| + t.references :user, index: true, foreign_key: true, null: false + t.string :token, index: { unique: true }, null: false + t.string :name, null: false + t.boolean :revoked, default: false + t.datetime :expires_at + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160509091049_add_locked_to_ci_runner.rb b/db/migrate/20160509091049_add_locked_to_ci_runner.rb new file mode 100644 index 0000000000..3fbaef3b7f --- /dev/null +++ b/db/migrate/20160509091049_add_locked_to_ci_runner.rb @@ -0,0 +1,13 @@ +class AddLockedToCiRunner < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + def up + add_column_with_default(:ci_runners, :locked, :boolean, + default: false, allow_null: false) + end + + def down + remove_column(:ci_runners, :locked) + end +end diff --git a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb new file mode 100644 index 0000000000..bd0463886b --- /dev/null +++ b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb @@ -0,0 +1,9 @@ +class SetMissingStageOnCiBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + def up + update_column_in_batches(:ci_builds, :stage, :test) do |table, query| + query.where(table[:stage].eq(nil)) + end + end +end diff --git a/db/migrate/20160616084004_change_project_of_environment.rb b/db/migrate/20160616084004_change_project_of_environment.rb new file mode 100644 index 0000000000..cc1daf9b62 --- /dev/null +++ b/db/migrate/20160616084004_change_project_of_environment.rb @@ -0,0 +1,21 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ChangeProjectOfEnvironment < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + change_column_null :environments, :project_id, true + end +end diff --git a/db/migrate/20160616102642_remove_duplicated_keys.rb b/db/migrate/20160616102642_remove_duplicated_keys.rb new file mode 100644 index 0000000000..00a45d7fe7 --- /dev/null +++ b/db/migrate/20160616102642_remove_duplicated_keys.rb @@ -0,0 +1,19 @@ +# rubocop:disable all +class RemoveDuplicatedKeys < ActiveRecord::Migration + def up + select_all("SELECT fingerprint FROM #{quote_table_name(:keys)} GROUP BY fingerprint HAVING COUNT(*) > 1").each do |row| + fingerprint = connection.quote(row['fingerprint']) + execute(%Q{ + DELETE FROM keys + WHERE fingerprint = #{fingerprint} + AND id != ( + SELECT id FROM ( + SELECT max(id) AS id + FROM keys + WHERE fingerprint = #{fingerprint} + ) max_ids + ) + }) + end + end +end diff --git a/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb b/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb new file mode 100644 index 0000000000..4bb4204ceb --- /dev/null +++ b/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb @@ -0,0 +1,21 @@ +class RemoveKeysFingerprintIndexIfExists < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + # https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/250 + # That MR was added on gitlab-ee so we need to check if the index + # already exists because we want to do is create an unique index instead. + + def up + if index_exists?(:keys, :fingerprint) + remove_index :keys, :fingerprint + end + end + + def down + unless index_exists?(:keys, :fingerprint) + add_concurrent_index :keys, :fingerprint + end + end +end diff --git a/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb b/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb new file mode 100644 index 0000000000..e35af38aac --- /dev/null +++ b/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb @@ -0,0 +1,13 @@ +class AddUniqueIndexToKeysFingerprint < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def up + add_concurrent_index :keys, :fingerprint, unique: true + end + + def down + remove_index :keys, :fingerprint + end +end diff --git a/db/migrate/20160617301627_add_events_to_notification_settings.rb b/db/migrate/20160617301627_add_events_to_notification_settings.rb new file mode 100644 index 0000000000..609596f45e --- /dev/null +++ b/db/migrate/20160617301627_add_events_to_notification_settings.rb @@ -0,0 +1,7 @@ +class AddEventsToNotificationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + def change + add_column :notification_settings, :events, :text + end +end diff --git a/db/migrate/20160620115026_add_index_on_runners_locked.rb b/db/migrate/20160620115026_add_index_on_runners_locked.rb new file mode 100644 index 0000000000..dfa5110dea --- /dev/null +++ b/db/migrate/20160620115026_add_index_on_runners_locked.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexOnRunnersLocked < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def change + add_concurrent_index :ci_runners, :locked + end +end diff --git a/db/schema.rb b/db/schema.rb index 6a3be7297e..7a8377f687 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: 20160615142710) do +ActiveRecord::Schema.define(version: 20160620115026) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -287,9 +287,11 @@ ActiveRecord::Schema.define(version: 20160615142710) do t.string "platform" t.string "architecture" t.boolean "run_untagged", default: true, null: false + t.boolean "locked", default: false, null: false end add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} + add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"} @@ -507,6 +509,7 @@ ActiveRecord::Schema.define(version: 20160615142710) do end add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree + add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree create_table "label_links", force: :cascade do |t| @@ -707,6 +710,7 @@ ActiveRecord::Schema.define(version: 20160615142710) do t.integer "level", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.text "events" end add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree @@ -756,6 +760,19 @@ ActiveRecord::Schema.define(version: 20160615142710) do add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree + create_table "personal_access_tokens", force: :cascade do |t| + t.integer "user_id", null: false + t.string "token", null: false + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "revoked", default: false + t.datetime "expires_at" + end + + add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree + add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree + create_table "project_group_links", force: :cascade do |t| t.integer "project_id", null: false t.integer "group_id", null: false @@ -1095,5 +1112,6 @@ ActiveRecord::Schema.define(version: 20160615142710) do add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree + add_foreign_key "personal_access_tokens", "users" add_foreign_key "u2f_registrations", "users" end diff --git a/doc/api/README.md b/doc/api/README.md index e3fc5a09f2..288f7f9ee6 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -8,6 +8,7 @@ under [`/lib/api`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api). Documentation for various API resources can be found separately in the following locations: +- [Award Emoji](award_emoji.md) - [Branches](branches.md) - [Builds](builds.md) - [Build triggers](build_triggers.md) @@ -31,6 +32,7 @@ following locations: - [Services](services.md) - [Session](session.md) - [Settings](settings.md) +- [Sidekiq metrics](sidekiq_metrics.md) - [System Hooks](system_hooks.md) - [Tags](tags.md) - [Users](users.md) @@ -44,13 +46,11 @@ The following documentation is for the [internal CI API](ci/README.md): ## Authentication -All API requests require authentication. You need to pass a `private_token` -parameter via query string or header. If passed as a header, the header name -must be `PRIVATE-TOKEN` (uppercase and with a dash instead of an underscore). -You can find or reset your private token in your account page (`/profile/account`). +All API requests require authentication via a token. There are three types of tokens +available: private tokens, OAuth 2 tokens, and personal access tokens. -If `private_token` is invalid or omitted, then an error message will be -returned with status code `401`: +If a token is invalid or omitted, an error message will be returned with +status code `401`: ```json { @@ -58,6 +58,39 @@ returned with status code `401`: } ``` +### Private Tokens + +You need to pass a `private_token` parameter via query string or header. If passed as a +header, the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of +an underscore). You can find or reset your private token in your account page +(`/profile/account`). + +### OAuth 2 Tokens + +You can use an OAuth 2 token to authenticate with the API by passing it either in the +`access_token` parameter or in the `Authorization` header. + +Example of using the OAuth2 token in the header: + +```shell +curl -H "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects +``` + +Read more about [GitLab as an OAuth2 client](oauth2.md). + +### Personal Access Tokens + +> **Note:** This feature was [introduced][ce-3749] in GitLab 8.8 + +You can create as many personal access tokens as you like from your GitLab +profile (`/profile/personal_access_tokens`); perhaps one for each application +that needs access to the GitLab API. + +Once you have your token, pass it to the API using either the `private_token` +parameter or the `PRIVATE-TOKEN` header. + +## Basic Usage + API requests should be prefixed with `api` and the API version. The API version is defined in [`lib/api.rb`][lib-api-url]. @@ -76,25 +109,6 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/ The API uses JSON to serialize data. You don't need to specify `.json` at the end of an API URL. -## Authentication with OAuth2 token - -Instead of the `private_token` you can transmit the OAuth2 access token as a -header or as a parameter. - -Example of OAuth2 token as a parameter: - -```shell -curl https://gitlab.example.com/api/v3/user?access_token=OAUTH-TOKEN -``` - -Example of OAuth2 token as a header: - -```shell -curl -H "Authorization: Bearer OAUTH-TOKEN" https://example.com/api/v3/user -``` - -Read more about [GitLab as an OAuth2 client](oauth2.md). - ## Status codes The API is designed to return different status codes according to context and @@ -330,3 +344,4 @@ programming languages. Visit the [GitLab website] for a complete list. [GitLab website]: https://about.gitlab.com/applications/#api-clients "Clients using the GitLab API" [lib-api-url]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api/api.rb +[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749 diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md new file mode 100644 index 0000000000..b44f8cfd62 --- /dev/null +++ b/doc/api/award_emoji.md @@ -0,0 +1,367 @@ +# Award Emoji + + >**Note:** This feature was introduced in GitLab 8.9 + +An awarded emoji tells a thousand words, and can be awarded on issues, merge +requests and notes/comments. Issues, merge requests and notes are further called +`awardables`. + +## Issues and merge requests + +### List an awardable's award emoji + +Gets a list of all award emoji + +``` +GET /projects/:id/issues/:issue_id/award_emoji +GET /projects/:id/merge_requests/:merge_request_id/award_emoji +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `awardable_id` | integer | yes | The ID of an awardable | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji +``` + +Example Response: + +```json +[ + { + "id": 4, + "name": "1234", + "user": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/u/root" + }, + "created_at": "2016-06-15T10:09:34.206Z", + "updated_at": "2016-06-15T10:09:34.206Z", + "awardable_id": 80, + "awardable_type": "Issue" + }, + { + "id": 1, + "name": "microphone", + "user": { + "name": "User 4", + "username": "user4", + "id": 26, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", + "web_url": "http://gitlab.example.com/u/user4" + }, + "created_at": "2016-06-15T10:09:34.177Z", + "updated_at": "2016-06-15T10:09:34.177Z", + "awardable_id": 80, + "awardable_type": "Issue" + } +] +``` + +### Get single issue note + +Gets a single award emoji + +``` +GET /projects/:id/issues/:issue_id/award_emoji/:award_id +GET /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `awardable_id` | integer | yes | The ID of an awardable | +| `award_id` | integer | yes | The ID of the award emoji | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1 +``` + +Example Response: + +```json +{ + "id": 1, + "name": "microphone", + "user": { + "name": "User 4", + "username": "user4", + "id": 26, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", + "web_url": "http://gitlab.example.com/u/user4" + }, + "created_at": "2016-06-15T10:09:34.177Z", + "updated_at": "2016-06-15T10:09:34.177Z", + "awardable_id": 80, + "awardable_type": "Issue" +} +``` + +### Award a new emoji + +This end point creates an award emoji on the specified resource + +``` +POST /projects/:id/issues/:issue_id/award_emoji +POST /projects/:id/merge_requests/:merge_request_id/award_emoji +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `awardable_id` | integer | yes | The ID of an awardable | +| `name` | string | yes | The name of the emoji, without colons | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish +``` + +Example Response: + +```json +{ + "id": 344, + "name": "blowfish", + "user": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/u/root" + }, + "created_at": "2016-06-17T17:47:29.266Z", + "updated_at": "2016-06-17T17:47:29.266Z", + "awardable_id": 80, + "awardable_type": "Issue" +} +``` + +### Delete an award emoji + +Sometimes its just not meant to be, and you'll have to remove your award. Only available to +admins or the author of the award. Status code 200 on success, 401 if unauthorized. + +``` +DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id +DELETE /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of an issue | +| `award_id` | integer | yes | The ID of a award_emoji | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344 +``` + +Example Response: + +```json +{ + "id": 344, + "name": "blowfish", + "user": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/u/root" + }, + "created_at": "2016-06-17T17:47:29.266Z", + "updated_at": "2016-06-17T17:47:29.266Z", + "awardable_id": 80, + "awardable_type": "Issue" +} +``` + +## Award Emoji on Notes + +The endpoints documented above are available for Notes as well. Notes +are a sub-resource of Issues and Merge Requests. The examples below +describe working with Award Emoji on notes for an Issue, but can be +easily adapted for notes on a Merge Request. + +### List a note's award emoji + +``` +GET /projects/:id/issues/:issue_id/notes/:note_id/award_emoji +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of an issue | +| `note_id` | integer | yes | The ID of an note | + + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji +``` + +Example Response: + +```json +[ + { + "id": 2, + "name": "mood_bubble_lightning", + "user": { + "name": "User 4", + "username": "user4", + "id": 26, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", + "web_url": "http://gitlab.example.com/u/user4" + }, + "created_at": "2016-06-15T10:09:34.197Z", + "updated_at": "2016-06-15T10:09:34.197Z", + "awardable_id": 1, + "awardable_type": "Note" + } +] +``` + +### Get single note's award emoji + +``` +GET /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of an issue | +| `note_id` | integer | yes | The ID of a note | +| `award_id` | integer | yes | The ID of the award emoji | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2 +``` + +Example Response: + +```json +{ + "id": 2, + "name": "mood_bubble_lightning", + "user": { + "name": "User 4", + "username": "user4", + "id": 26, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", + "web_url": "http://gitlab.example.com/u/user4" + }, + "created_at": "2016-06-15T10:09:34.197Z", + "updated_at": "2016-06-15T10:09:34.197Z", + "awardable_id": 1, + "awardable_type": "Note" +} +``` + +### Award a new emoji on a note + +``` +POST /projects/:id/issues/:issue_id/notes/:note_id/award_emoji +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of an issue | +| `note_id` | integer | yes | The ID of a note | +| `name` | string | yes | The name of the emoji, without colons | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket +``` + +Example Response: + +```json +{ + "id": 345, + "name": "rocket", + "user": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/u/root" + }, + "created_at": "2016-06-17T19:59:55.888Z", + "updated_at": "2016-06-17T19:59:55.888Z", + "awardable_id": 1, + "awardable_type": "Note" +} +``` + +### Delete an award emoji + +Sometimes its just not meant to be, and you'll have to remove your award. Only available to +admins or the author of the award. Status code 200 on success, 401 if unauthorized. + +``` +DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of an issue | +| `note_id` | integer | yes | The ID of a note | +| `award_id` | integer | yes | The ID of a award_emoji | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345 +``` + +Example Response: + +```json +{ + "id": 345, + "name": "rocket", + "user": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/u/root" + }, + "created_at": "2016-06-17T19:59:55.888Z", + "updated_at": "2016-06-17T19:59:55.888Z", + "awardable_id": 1, + "awardable_type": "Note" +} +``` diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md new file mode 100644 index 0000000000..ebd131c94c --- /dev/null +++ b/doc/api/sidekiq_metrics.md @@ -0,0 +1,152 @@ +# Sidekiq Metrics + +>**Note:** This endpoint is only available on GitLab 8.9 and above. + +This API endpoint allows you to retrieve some information about the current state +of Sidekiq, its jobs, queues, and processes. + +## Get the current Queue Metrics + +List information about all the registered queues, their backlog and their +latency. + +``` +GET /sidekiq/queue_metrics +``` + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics +``` + +Example response: + +```json +{ + "queues": { + "default": { + "backlog": 0, + "latency": 0 + } + } +} +``` + +## Get the current Process Metrics + +List information about all the Sidekiq workers registered to process your queues. + +``` +GET /sidekiq/process_metrics +``` + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics +``` + +Example response: + +```json +{ + "processes": [ + { + "hostname": "gitlab.example.com", + "pid": 5649, + "tag": "gitlab", + "started_at": "2016-06-14T10:45:07.159-05:00", + "queues": [ + "post_receive", + "mailers", + "archive_repo", + "system_hook", + "project_web_hook", + "gitlab_shell", + "incoming_email", + "runner", + "common", + "default" + ], + "labels": [], + "concurrency": 25, + "busy": 0 + } + ] +} +``` + +## Get the current Job Statistics + +List information about the jobs that Sidekiq has performed. + +``` +GET /sidekiq/job_stats +``` + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats +``` + +Example response: + +```json +{ + "jobs": { + "processed": 2, + "failed": 0, + "enqueued": 0 + } +} +``` + +## Get a compound response of all the previously mentioned metrics + +List all the currently available information about Sidekiq. + +``` +GET /sidekiq/compound_metrics +``` + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics +``` + +Example response: + +```json +{ + "queues": { + "default": { + "backlog": 0, + "latency": 0 + } + }, + "processes": [ + { + "hostname": "gitlab.example.com", + "pid": 5649, + "tag": "gitlab", + "started_at": "2016-06-14T10:45:07.159-05:00", + "queues": [ + "post_receive", + "mailers", + "archive_repo", + "system_hook", + "project_web_hook", + "gitlab_shell", + "incoming_email", + "runner", + "common", + "default" + ], + "labels": [], + "concurrency": 25, + "busy": 0 + } + ], + "jobs": { + "processed": 2, + "failed": 0, + "enqueued": 0 + } +} +``` + diff --git a/doc/ci/README.md b/doc/ci/README.md index ef72df97ce..5a1cb5319c 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -5,6 +5,7 @@ - [Get started with GitLab CI](quick_start/README.md) - [CI examples for various languages](examples/README.md) - [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md) +- [Environments and deployments](environments.md) - [Learn how `.gitlab-ci.yml` works](yaml/README.md) - [Configure a Runner, the application that runs your builds](runners/README.md) - [Use Docker images with GitLab Runner](docker/using_docker_images.md) diff --git a/doc/ci/environments.md b/doc/ci/environments.md new file mode 100644 index 0000000000..040379bb38 --- /dev/null +++ b/doc/ci/environments.md @@ -0,0 +1,58 @@ +# Introduction to environments and deployments + +>**Note:** +Introduced in GitLab 8.9. + +## Environments + +Environments are places where code gets deployed, such as staging or production. +CI/CD [Pipelines] usually have one or more [jobs] that deploy to an environment. +Defining environments in a project's `.gitlab-ci.yml` lets developers track +[deployments] to these environments. + +## Deployments + +Deployments are created when [jobs] deploy versions of code to [environments]. + +## Defining environments + +You can create and delete environments manually in the web interface, but we +recommend that you define your environments in `.gitlab-ci.yml` first, which +will automatically create environments for you after the first deploy. + +The `environment` is just a hint for GitLab that this job actually deploys to +this environment. Each time the job succeeds, a deployment is recorded, +remembering the git SHA and environment. + +Add something like this to your `.gitlab-ci.yml`: +``` +production: + stage: deploy + script: dpl... + environment: production +``` + +See full [documentation](yaml/README.md#environment). + +## Seeing environment status + +You can find the environment list under **Pipelines > Environments** for your +project. You'll see the git SHA and date of the last deployment to each +environment defined. + +>**Note:** +Only deploys that happen after your `.gitlab-ci.yml` is properly configured will +show up in the environments and deployments lists. + +## Seeing deployment history + +Clicking on an environment will show the history of deployments. + +>**Note:** +Only deploys that happen after your `.gitlab-ci.yml` is properly configured will +show up in the environments and deployments lists. + +[Pipelines]: quick_start/README.md +[jobs]: yaml/README.md#jobs +[environments]: #environments +[deployments]: #deployments diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 61294be599..27bc21c292 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -4,7 +4,8 @@ - [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md) - [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md) - [Test a Clojure application](test-clojure-application.md) -- [Using `dpl` as deployment tool](../deployment/README.md) +- [Test a Scala application](test-scala-application.md) +- [Using `dpl` as deployment tool](deployment/README.md) - Help your favorite programming language and GitLab by sending a merge request with a guide for that language. diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md new file mode 100644 index 0000000000..7412fdbbc7 --- /dev/null +++ b/doc/ci/examples/test-scala-application.md @@ -0,0 +1,47 @@ +## Test a Scala application + +This example demonstrates the integration of Gitlab CI with Scala +applications using SBT. Checkout the example +[project](https://gitlab.com/gitlab-examples/scala-sbt) and +[build status](https://gitlab.com/gitlab-examples/scala-sbt/builds). + +### Add `.gitlab-ci.yml` file to project + +The following `.gitlab-ci.yml` should be added in the root of your +repository to trigger CI: + +``` yaml +image: java:8 + +before_script: + - apt-get update -y + - apt-get install apt-transport-https -y + # Install SBT + - echo "deb http://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list + - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 642AC823 + - apt-get update -y + - apt-get install sbt -y + - sbt sbt-version + +test: + script: + - sbt clean coverage test coverageReport +``` + +The `before_script` installs [SBT](http://www.scala-sbt.org/) and +displays the version that is being used. The `test` stage executes SBT +to compile and test the project. +[scoverage](https://github.com/scoverage/sbt-scoverage) is used as an SBT +plugin to measure test coverage. + +You can use other versions of Scala and SBT by defining them in +`build.sbt`. + +### Display test coverage in build + +Add the `Coverage was \[\d+.\d+\%\]` regular expression in the +**Settings > Edit Project > Test coverage parsing** project setting to +retrieve the test coverage rate from the build trace and have it +displayed with your builds. + +**Builds** must be enabled for this option to appear. diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index 400784da61..ddebd98765 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -96,6 +96,12 @@ To register the runner, run the command below and follow instructions: sudo gitlab-ci-multi-runner register ``` +### Lock a specific runner from being enabled for other projects + +You can configure a runner to assign it exclusively to a project. When a +runner is locked this way, it can no longer be enabled for other projects. +This setting is available on each runner in *Project Settings* > *Runners*. + ### Making an existing Shared Runner Specific If you are an admin on your GitLab instance, @@ -128,7 +134,7 @@ the appropriate dependencies to run Rails test suites. ### Prevent runner with tags from picking jobs without tags You can configure a runner to prevent it from picking jobs with tags when -the runnner does not have tags assigned. This setting is available on each +the runner does not have tags assigned. This setting is available on each runner in *Project Settings* > *Runners*. ### Be careful with sensitive information diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 9c98f9c98c..d0fbcbe998 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -167,7 +167,7 @@ There are also two edge cases worth mentioning: 1. If no `stages` is defined in `.gitlab-ci.yml`, then by default the `build`, `test` and `deploy` are allowed to be used as job's stage by default. -2. If a job doesn't specify `stage`, the job is assigned the `test` stage. +2. If a job doesn't specify a `stage`, the job is assigned the `test` stage. ### types @@ -178,9 +178,9 @@ Alias for [stages](#stages). >**Note:** Introduced in GitLab Runner v0.5.0. -GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build -environment. The variables are stored in the git repository and are meant to -store non-sensitive project configuration, for example: +GitLab CI allows you to add variables to `.gitlab-ci.yml` that are set in the +build environment. The variables are stored in the git repository and are meant +to store non-sensitive project configuration, for example: ```yaml variables: @@ -253,8 +253,8 @@ rspec: - binaries/ ``` -The cache is provided on best effort basis, so don't expect that cache will be -always present. For implementation details please check GitLab Runner. +The cache is provided on a best-effort basis, so don't expect that the cache +will be always present. For implementation details, please check GitLab Runner. #### cache:key @@ -479,10 +479,10 @@ failure. `when` can be set to one of the following values: 1. `on_success` - execute build only when all builds from prior stages - succeeded. This is the default. + succeed. This is the default. 1. `on_failure` - execute build only when at least one build from prior stages - failed. -1. `always` - execute build despite the status of builds from prior stages. + fails. +1. `always` - execute build regardless of the status of builds from prior stages. For example: @@ -530,14 +530,18 @@ The above script will: ### environment >**Note:** -Introduced in GitLab v8.9.0. +Introduced in GitLab 8.9. -`environment` is used to define that job does deployment to specific environment. -This allows to easily track all deployments to your environments straight from GitLab. +`environment` is used to define that a job deploys to a specific environment. +This allows easy tracking of all deployments to your environments straight from +GitLab. -If `environment` is specified and no environment under that name does exist a new one will be created automatically. +If `environment` is specified and no environment under that name exists, a new +one will be created automatically. -The `environment` name must contain only letters, digits, '-' and '_'. +The `environment` name must contain only letters, digits, '-' and '_'. Common +names are `qa`, `staging`, and `production`, but you can use whatever name works +with your workflow. --- @@ -550,7 +554,8 @@ deploy to production: environment: production ``` -The `deploy to production` job will be marked as doing deployment to `production` environment. +The `deploy to production` job will be marked as doing deployment to +`production` environment. ### artifacts @@ -559,10 +564,10 @@ The `deploy to production` job will be marked as doing deployment to `production > - Introduced in GitLab Runner v0.7.0 for non-Windows platforms. > - Windows support was added in GitLab Runner v.1.0.0. > - Currently not all executors are supported. -> - Build artifacts are only collected for successful builds. +> - Build artifacts are only collected for successful builds by default. -`artifacts` is used to specify list of files and directories which should be -attached to build after success. To pass artifacts between different builds, +`artifacts` is used to specify a list of files and directories which should be +attached to the build after success. To pass artifacts between different builds, see [dependencies](#dependencies). Below are some examples. @@ -690,9 +695,9 @@ failure. `artifacts:when` can be set to one of the following values: -1. `on_success` - upload artifacts only when build succeeds. This is the default -1. `on_failure` - upload artifacts only when build fails -1. `always` - upload artifacts despite the build status +1. `on_success` - upload artifacts only when the build succeeds. This is the default. +1. `on_failure` - upload artifacts only when the build fails. +1. `always` - upload artifacts regardless of the build status. --- @@ -711,16 +716,18 @@ job: >**Note:** Introduced in GitLab 8.9 and GitLab Runner v1.3.0. -`artifacts:expire_in` is used to remove uploaded artifacts after specified time. -By default artifacts are stored on GitLab forver. -`expire_in` allows to specify after what time the artifacts should be removed. -The artifacts will expire counting from the moment when they are uploaded and stored on GitLab. +`artifacts:expire_in` is used to delete uploaded artifacts after the specified +time. By default, artifacts are stored on GitLab forever. `expire_in` allows you +to specify how long artifacts should live before they expire, counting from the +time they are uploaded and stored on GitLab. -After artifacts uploading you can use the **Keep** button on build page to keep the artifacts forever. +You can use the **Keep** button on the build page to override expiration and +keep artifacts forever. -Artifacts are removed every hour, but they are not accessible after expire date. +By default, artifacts are deleted hourly (via a cron job), but they are not +accessible after expiry. -The value of `expire_in` is a elapsed time. The example of parsable values: +The value of `expire_in` is an elapsed time. Examples of parseable values: - '3 mins 4 sec' - '2 hrs 20 min' - '2h20min' @@ -732,7 +739,7 @@ The value of `expire_in` is a elapsed time. The example of parsable values: **Example configurations** -To expire artifacts after 1 week from the moment that they are uploaded: +To expire artifacts 1 week after being uploaded: ```yaml job: diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md index 6cd9b274d1..c2272ab0a2 100644 --- a/doc/development/instrumentation.md +++ b/doc/development/instrumentation.md @@ -94,23 +94,8 @@ Visibility: public Number of lines: 21 def #{name}(#{args_signature}) - trans = Gitlab::Metrics::Instrumentation.transaction - - if trans - start = Time.now - cpu_start = Gitlab::Metrics::System.cpu_time - retval = super - duration = (Time.now - start) * 1000.0 - - if duration >= Gitlab::Metrics.method_call_threshold - cpu_duration = Gitlab::Metrics::System.cpu_time - cpu_start - - trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, - { duration: duration, cpu_duration: cpu_duration }, - method: #{label.inspect}) - end - - retval + if trans = Gitlab::Metrics::Instrumentation.transaction + trans.measure_method(#{label.inspect}) { super } else super end diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 02e024ca15..8a7547e532 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -34,6 +34,15 @@ First, you need to provide information on whether the migration can be applied: 3. online with errors on new instances while migrating 4. offline (needs to happen without app servers to prevent db corruption) +For example: + +``` +# rubocop:disable all +# Migration type: online without errors (works on previous version and new one) +class MyMigration < ActiveRecord::Migration +... +``` + It is always preferable to have a migration run online. If you expect the migration to take particularly long (for instance, if it loops through all notes), this is valuable information to add. @@ -48,7 +57,6 @@ be possible to downgrade in case of a vulnerability or bugs. In your migration, add a comment describing how the reversibility of the migration was tested. - ## Removing indices If you need to remove index, please add a condition like in following example: @@ -70,6 +78,7 @@ so: ``` class MyMigration < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers disable_ddl_transaction! def change @@ -90,8 +99,11 @@ value of `10` you'd write the following: ``` class MyMigration < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + def up - add_column_with_default(:projects, :foo, :integer, 10) + add_column_with_default(:projects, :foo, :integer, default: 10) end def down diff --git a/doc/project_services/emails_on_push.md b/doc/project_services/emails_on_push.md new file mode 100644 index 0000000000..2f9f36f962 --- /dev/null +++ b/doc/project_services/emails_on_push.md @@ -0,0 +1,17 @@ +## Enabling emails on push + +To receive email notifications for every change that is pushed to the project, visit +your project's **Settings > Services > Emails on push** and activate the service. + +In the _Recipients_ area, provide a list of emails separated by commas. + +You can configure any of the following settings depending on your preference. + ++ **Push events** - Email will be triggered when a push event is recieved ++ **Tag push events** - Email will be triggered when a tag is created and pushed ++ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`). ++ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body. + +--- + +![Email on push service settings](img/emails_on_push_service.png) diff --git a/doc/project_services/img/emails_on_push_service.png b/doc/project_services/img/emails_on_push_service.png new file mode 100644 index 0000000000..cd6f79ad1e Binary files /dev/null and b/doc/project_services/img/emails_on_push_service.png differ diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index a5af620d9b..f81a035f70 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -33,7 +33,7 @@ further configuration instructions and details. Contributions are welcome. | Campfire | Simple web-based real-time group chat | | Custom Issue Tracker | Custom issue tracker | | Drone CI | Continuous Integration platform built on Docker, written in Go | -| Emails on push | Email the commits and diff of each push to a list of recipients | +| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients | | External Wiki | Replaces the link to the internal wiki with a link to an external wiki | | Flowdock | Flowdock is a collaboration web app for technical teams | | Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md index fffa0aba57..4b55113025 100644 --- a/doc/workflow/add-user/add-user.md +++ b/doc/workflow/add-user/add-user.md @@ -8,7 +8,7 @@ You should have `master` or `owner` permissions to add or import a new user to your project. The first step to add or import a user, go to your project and click on -**Members** on the left side of your screen. +**Members** in the drop-down menu on the right side of your screen. ![Members](img/add_user_members_menu.png) @@ -87,3 +87,25 @@ invitation, change their access level or even delete them. Once the user accepts the invitation, they will be prompted to create a new GitLab account using the same e-mail address the invitation was sent to. + +## Request access to a project + +As a user, you can request to be a member of a project. Go to the project you'd +like to be a member of, and click the **Request Access** button on the right +side of your screen. + +![Request access button](img/request_access_button.png) + +--- + +Project owners & masters will be notified of your request and will be able to approve or +decline it on the members page. + +![Manage access requests](img/access_requests_management.png) + +--- + +If you change your mind before your request is approved, just click the +**Withdraw Access Request** button. + +![Withdraw access request button](img/withdraw_access_request_button.png) diff --git a/doc/workflow/add-user/img/access_requests_management.png b/doc/workflow/add-user/img/access_requests_management.png new file mode 100644 index 0000000000..e9641cb4f8 Binary files /dev/null and b/doc/workflow/add-user/img/access_requests_management.png differ diff --git a/doc/workflow/add-user/img/add_user_email_accept.png b/doc/workflow/add-user/img/add_user_email_accept.png index 910affc965..18aabf93d5 100644 Binary files a/doc/workflow/add-user/img/add_user_email_accept.png and b/doc/workflow/add-user/img/add_user_email_accept.png differ diff --git a/doc/workflow/add-user/img/add_user_email_ready.png b/doc/workflow/add-user/img/add_user_email_ready.png index 5f02ce89b3..385d64330c 100644 Binary files a/doc/workflow/add-user/img/add_user_email_ready.png and b/doc/workflow/add-user/img/add_user_email_ready.png differ diff --git a/doc/workflow/add-user/img/add_user_email_search.png b/doc/workflow/add-user/img/add_user_email_search.png index 140979fbe1..84741edbca 100644 Binary files a/doc/workflow/add-user/img/add_user_email_search.png and b/doc/workflow/add-user/img/add_user_email_search.png differ diff --git a/doc/workflow/add-user/img/add_user_give_permissions.png b/doc/workflow/add-user/img/add_user_give_permissions.png index 8ef9156c8d..7e580384e5 100644 Binary files a/doc/workflow/add-user/img/add_user_give_permissions.png and b/doc/workflow/add-user/img/add_user_give_permissions.png differ diff --git a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png index 5770d5cf0c..8dbd73a5bc 100644 Binary files a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png and b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png differ diff --git a/doc/workflow/add-user/img/add_user_imported_members.png b/doc/workflow/add-user/img/add_user_imported_members.png index dea4b3f40a..abac1f59c0 100644 Binary files a/doc/workflow/add-user/img/add_user_imported_members.png and b/doc/workflow/add-user/img/add_user_imported_members.png differ diff --git a/doc/workflow/add-user/img/add_user_list_members.png b/doc/workflow/add-user/img/add_user_list_members.png index 7daa6ca7d9..e17d88c6f5 100644 Binary files a/doc/workflow/add-user/img/add_user_list_members.png and b/doc/workflow/add-user/img/add_user_list_members.png differ diff --git a/doc/workflow/add-user/img/add_user_members_menu.png b/doc/workflow/add-user/img/add_user_members_menu.png index f1797b95f6..ec5d39f402 100644 Binary files a/doc/workflow/add-user/img/add_user_members_menu.png and b/doc/workflow/add-user/img/add_user_members_menu.png differ diff --git a/doc/workflow/add-user/img/add_user_search_people.png b/doc/workflow/add-user/img/add_user_search_people.png index 5ac10ce80d..eaa062376f 100644 Binary files a/doc/workflow/add-user/img/add_user_search_people.png and b/doc/workflow/add-user/img/add_user_search_people.png differ diff --git a/doc/workflow/add-user/img/request_access_button.png b/doc/workflow/add-user/img/request_access_button.png new file mode 100644 index 0000000000..984d640b0f Binary files /dev/null and b/doc/workflow/add-user/img/request_access_button.png differ diff --git a/doc/workflow/add-user/img/withdraw_access_request_button.png b/doc/workflow/add-user/img/withdraw_access_request_button.png new file mode 100644 index 0000000000..ff54a0e438 Binary files /dev/null and b/doc/workflow/add-user/img/withdraw_access_request_button.png differ diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md index 34ada1774d..1a316e8097 100644 --- a/doc/workflow/groups.md +++ b/doc/workflow/groups.md @@ -51,6 +51,28 @@ If necessary, you can increase the access level of an individual user for a spec ![Barry effectively has 'Master' access to GitLab CI now](groups/override_access_level.png) +## Request access to a group + +As a user, you can request to be a member of a group. Go to the group you'd +like to be a member of, and click the **Request Access** button on the right +side of your screen. + +![Request access button](groups/request_access_button.png) + +--- + +Group owners & masters will be notified of your request and will be able to approve or +decline it on the members page. + +![Manage access requests](groups/access_requests_management.png) + +--- + +If you change your mind before your request is approved, just click the +**Withdraw Access Request** button. + +![Withdraw access request button](groups/withdraw_access_request_button.png) + ## Managing group memberships via LDAP In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups. diff --git a/doc/workflow/groups/access_requests_management.png b/doc/workflow/groups/access_requests_management.png new file mode 100644 index 0000000000..ffede8e9bd Binary files /dev/null and b/doc/workflow/groups/access_requests_management.png differ diff --git a/doc/workflow/groups/request_access_button.png b/doc/workflow/groups/request_access_button.png new file mode 100644 index 0000000000..ff0ac8747a Binary files /dev/null and b/doc/workflow/groups/request_access_button.png differ diff --git a/doc/workflow/groups/withdraw_access_request_button.png b/doc/workflow/groups/withdraw_access_request_button.png new file mode 100644 index 0000000000..99d7a326ed Binary files /dev/null and b/doc/workflow/groups/withdraw_access_request_button.png differ diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index cbca94c0b5..fe4485e148 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -4,7 +4,7 @@ GitLab has a notification system in place to notify a user of events that are im ## Notification settings -Under user profile page you can find the notification settings. +You can find notification settings under the user profile. ![notification settings](notifications/settings.png) @@ -20,6 +20,7 @@ Each of these settings have levels of notification: * Participating - receive notifications from related resources * Watch - receive notifications from projects or groups user is a member of * Global - notifications as set at the global settings +* Custom - user will receive notifications when mentioned, is participant and custom selected events. #### Global Settings @@ -55,7 +56,7 @@ Below is the table of events users can be notified of: | User added to project | User | Sent when user is added to project | | Project access level changed | User | Sent when user project access level is changed | | User added to group | User | Sent when user is added to group | -| Group access level changed | User | Sent when user group access level is changed | +| Group access level changed | User | Sent when user group access level is changed | | Project moved | Project members [1] | [1] not disabled | ### Issue / Merge Request events @@ -71,6 +72,7 @@ In all of the below cases, the notification will be sent to: - Watchers: users with notification level "Watch" - Subscribers: anyone who manually subscribed to the issue/merge request +- Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below | Event | Sent to | |------------------------|---------| diff --git a/doc/workflow/notifications/settings.png b/doc/workflow/notifications/settings.png index e5b50ee249..7c6857aad1 100644 Binary files a/doc/workflow/notifications/settings.png and b/doc/workflow/notifications/settings.png differ diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png index beb6c53ec7..16be0413b6 100644 Binary files a/doc/workflow/shortcuts.png and b/doc/workflow/shortcuts.png differ diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature index 7639206835..56b4a639c0 100644 --- a/features/dashboard/new_project.feature +++ b/features/dashboard/new_project.feature @@ -14,7 +14,7 @@ Background: @javascript Scenario: I should see instructions on how to import from Git URL Given I see "New Project" page - When I click on "Any repo by URL" + When I click on "Repo by URL" Then I see instructions on how to import from Git URL @javascript diff --git a/features/dashboard/todos.feature b/features/dashboard/todos.feature index 8677b45081..42f5d6d2af 100644 --- a/features/dashboard/todos.feature +++ b/features/dashboard/todos.feature @@ -14,7 +14,12 @@ Feature: Dashboard Todos Scenario: I mark todos as done Then I should see todos assigned to me And I mark the todo as done - And I click on the "Done" tab + Then I should see the todo marked as done + + @javascript + Scenario: I mark all todos as done + Then I should see todos assigned to me + And I mark all todos as done Then I should see all todos marked as done @javascript diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index c4f987a792..57dda9c223 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -10,9 +10,9 @@ Feature: Project Active Tab Then the active main tab should be Home And no other main tabs should be active - Scenario: On Project Code + Scenario: On Project Repository Given I visit my project's files page - Then the active main tab should be Code + Then the active main tab should be Repository And no other main tabs should be active Scenario: On Project Issues @@ -59,46 +59,46 @@ Feature: Project Active Tab And no other sub navs should be active And the active main tab should be Settings - # Sub Tabs: Code + # Sub Tabs: Repository - Scenario: On Project Code/Files + Scenario: On Project Repository/Files Given I visit my project's files page Then the active sub tab should be Files And no other sub tabs should be active - And the active main tab should be Code + And the active main tab should be Repository - Scenario: On Project Code/Commits + Scenario: On Project Repository/Commits Given I visit my project's commits page Then the active sub tab should be Commits And no other sub tabs should be active - And the active main tab should be Code + And the active main tab should be Repository - Scenario: On Project Code/Network + Scenario: On Project Repository/Network Given I visit my project's network page Then the active sub tab should be Network And no other sub tabs should be active - And the active main tab should be Code + And the active main tab should be Repository - Scenario: On Project Code/Compare + Scenario: On Project Repository/Compare Given I visit my project's commits page And I click the "Compare" tab Then the active sub tab should be Compare And no other sub tabs should be active - And the active main tab should be Code + And the active main tab should be Repository - Scenario: On Project Code/Branches + Scenario: On Project Repository/Branches Given I visit my project's commits page And I click the "Branches" tab Then the active sub tab should be Branches And no other sub tabs should be active - And the active main tab should be Code + And the active main tab should be Repository - Scenario: On Project Code/Tags + Scenario: On Project Repository/Tags Given I visit my project's commits page And I click the "Tags" tab Then the active sub tab should be Tags And no other sub tabs should be active - And the active main tab should be Code + And the active main tab should be Repository Scenario: On Project Issues/Browse Given I visit my project's issues page diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature index c73d0b3233..f71f69ef06 100644 --- a/features/project/shortcuts.feature +++ b/features/project/shortcuts.feature @@ -8,21 +8,21 @@ Feature: Project Shortcuts @javascript Scenario: Navigate to files tab Given I press "g" and "f" - Then the active main tab should be Code + Then the active main tab should be Repository Then the active sub tab should be Files @javascript Scenario: Navigate to commits tab Given I visit my project's files page Given I press "g" and "c" - Then the active main tab should be Code + Then the active main tab should be Repository Then the active sub tab should be Commits @javascript Scenario: Navigate to network tab Given I press "g" and "n" Then the active sub tab should be Network - And the active main tab should be Code + And the active main tab should be Repository @javascript Scenario: Navigate to graphs tab diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index a0aad66184..29e6b9f1a0 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -10,7 +10,8 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps end step 'I see "New Project" page' do - expect(page).to have_content('Project path') + expect(page).to have_content('Project owner') + expect(page).to have_content('Project name') end step 'I see all possible import optios' do @@ -19,7 +20,8 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps expect(page).to have_link('GitLab.com') expect(page).to have_link('Gitorious.org') expect(page).to have_link('Google Code') - expect(page).to have_link('Any repo by URL') + expect(page).to have_link('Repo by URL') + expect(page).to have_link('GitLab export') end step 'I click on "Import project from GitHub"' do @@ -36,7 +38,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps end end - step 'I click on "Any repo by URL"' do + step 'I click on "Repo by URL"' do first('.import_git').click end diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index 19fedfbfcd..60152d3da5 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -26,14 +26,15 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps end step 'I should see todos assigned to me' do + page.within('.todos-pending-count') { expect(page).to have_content '4' } expect(page).to have_content 'To do 4' 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.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) + should_see_todo(2, "John Doe mentioned you on issue #{issue.to_reference}", "#{current_user.to_reference} Wdyt?") + should_see_todo(3, "John Doe assigned you issue #{issue.to_reference}", issue.title) + should_see_todo(4, "Mary Jane mentioned you on issue #{issue.to_reference}", issue.title) end step 'I mark the todo as done' do @@ -41,18 +42,40 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps click_link 'Done' end + page.within('.todos-pending-count') { expect(page).to have_content '3' } expect(page).to have_content 'To do 3' expect(page).to have_content 'Done 1' should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" end - step 'I click on the "Done" tab' do + step 'I mark all todos as done' do + click_link 'Mark all as done' + + page.within('.todos-pending-count') { expect(page).to have_content '0' } + expect(page).to have_content 'To do 0' + expect(page).to have_content 'Done 4' + expect(page).not_to have_link project.name_with_namespace + should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" + should_not_see_todo "John Doe mentioned you on issue #{issue.to_reference}" + should_not_see_todo "John Doe assigned you issue #{issue.to_reference}" + should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference}" + end + + step 'I should see the todo marked as done' do click_link 'Done 1' + + expect(page).to have_link project.name_with_namespace + should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false) end step 'I should see all todos marked as done' do + click_link 'Done 4' + expect(page).to have_link project.name_with_namespace should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false) + should_see_todo(2, "John Doe mentioned you on issue #{issue.to_reference}", "#{current_user.to_reference} Wdyt?", false) + should_see_todo(3, "John Doe assigned you issue #{issue.to_reference}", issue.title, false) + should_see_todo(4, "Mary Jane mentioned you on issue #{issue.to_reference}", issue.title, false) end step 'I filter by "Enterprise"' do @@ -76,7 +99,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps end step 'I should not see todos related to "Mary Jane" in the list' do - should_not_see_todo "Mary Jane mentioned you on issue ##{issue.iid}" + should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference}" end step 'I should not see todos related to "Merge Requests" in the list' do @@ -85,7 +108,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps 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.to_reference}" - should_not_see_todo "John Doe assigned you issue ##{issue.iid}" + should_not_see_todo "John Doe assigned you issue #{issue.to_reference}" end step 'I click on the todo' do diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb index a96f35ada5..7e339443b7 100644 --- a/features/steps/profile/notifications.rb +++ b/features/steps/profile/notifications.rb @@ -11,12 +11,10 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps end step 'I select Mention setting from dropdown' do - select 'mention', from: 'notification_setting_level' + first(:link, "On mention").trigger('click') end step 'I should see Notification saved message' do - page.within '.flash-container' do - expect(page).to have_content 'Notification settings saved' - end + expect(page).to have_content 'On mention' end end diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb index 9b59b68267..019b3124a8 100644 --- a/features/steps/project/network_graph.rb +++ b/features/steps/project/network_graph.rb @@ -20,11 +20,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end step 'page should select "master" in select box' do - expect(page).to have_selector '.select2-chosen', text: "master" + expect(page).to have_selector '.dropdown-menu-toggle', text: "master" end step 'page should select "v1.0.0" in select box' do - expect(page).to have_selector '.select2-chosen', text: "v1.0.0" + expect(page).to have_selector '.dropdown-menu-toggle', text: "v1.0.0" end step 'page should have "master" on graph' do @@ -40,11 +40,19 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end When 'I switch ref to "feature"' do - select 'feature', from: 'ref' + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link 'feature' + end end When 'I switch ref to "v1.0.0"' do - select 'v1.0.0', from: 'ref' + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link 'v1.0.0' + end end When 'click "Show only selected branch" checkbox' do @@ -68,11 +76,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end step 'page should select "feature" in select box' do - expect(page).to have_selector '.select2-chosen', text: "feature" + expect(page).to have_selector '.dropdown-menu-toggle', text: "feature" end step 'page should select "v1.0.0" in select box' do - expect(page).to have_selector '.select2-chosen', text: "v1.0.0" + expect(page).to have_selector '.dropdown-menu-toggle', text: "v1.0.0" end step 'page should have "feature" on graph' do diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 2a1a8e776f..76fefee925 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -126,7 +126,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I click notifications drop down button' do - find('#notifications-button').click + first('.notifications-btn').click end step 'I choose Mention setting' do @@ -134,8 +134,8 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see Notification saved message' do - page.within '.flash-container' do - expect(page).to have_content 'Notification settings saved' + page.within '#notifications-button' do + expect(page).to have_content 'On mention' end end diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb index 47de4b91df..9077184790 100644 --- a/features/steps/project/project_find_file.rb +++ b/features/steps/project/project_find_file.rb @@ -13,12 +13,12 @@ class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps end step 'I should see "find file" page' do - ensure_active_main_tab('Code') + ensure_active_main_tab('Repository') expect(page).to have_selector('.file-finder-holder', count: 1) end step 'I fill in Find by path with "git"' do - ensure_active_main_tab('Code') + ensure_active_main_tab('Repository') expect(page).to have_selector('.file-finder-holder', count: 1) end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 2c0498de3b..0fe046dcbf 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -202,8 +202,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I see Browse dir link' do - expect(page).to have_link 'Browse Directory »' - expect(page).not_to have_link 'Browse Code »' + expect(page).to have_link 'Browse Directory' + expect(page).not_to have_link 'Browse Code' end step 'I click on readme file' do @@ -219,7 +219,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps step 'I see Browse code link' do expect(page).to have_link 'Browse Files' - expect(page).not_to have_link 'Browse Directory »' + expect(page).not_to have_link 'Browse Directory' end step 'I click on Permalink' do @@ -290,15 +290,23 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step "I switch ref to 'test'" do - select "'test'", from: 'ref' + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link 'test' + end end step "I switch ref to fix" do - select "fix", from: 'ref' + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link 'fix' + end end step "I see the ref 'test' has been selected" do - expect(page).to have_selector '.select2-chosen', text: "'test'" + expect(page).to have_selector '.dropdown-toggle-text', text: "'test'" end step "I visit the 'test' tree" do diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index bfee879330..d602421260 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -8,8 +8,8 @@ module SharedProjectTab ensure_active_main_tab('Project') end - step 'the active main tab should be Code' do - ensure_active_main_tab('Code') + step 'the active main tab should be Repository' do + ensure_active_main_tab('Repository') end step 'the active main tab should be Graphs' do diff --git a/lib/api/api.rb b/lib/api/api.rb index 6cd909f611..f8f680a631 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -26,38 +26,40 @@ module API # Ensure the namespace is right, otherwise we might load Grape::API::Helpers helpers ::API::Helpers - mount ::API::Groups + mount ::API::AwardEmoji + mount ::API::Branches + mount ::API::Builds + mount ::API::CommitStatuses + mount ::API::Commits + mount ::API::DeployKeys + mount ::API::Files mount ::API::GroupMembers - mount ::API::Users + mount ::API::Groups + mount ::API::Internal + mount ::API::Issues + mount ::API::Keys + mount ::API::Labels + mount ::API::Licenses + mount ::API::MergeRequests + mount ::API::Milestones + mount ::API::Namespaces + mount ::API::Notes + mount ::API::ProjectHooks + mount ::API::ProjectMembers + mount ::API::ProjectSnippets mount ::API::Projects mount ::API::Repositories - mount ::API::Issues - mount ::API::Milestones - mount ::API::Session - mount ::API::MergeRequests - mount ::API::Notes - mount ::API::Internal - mount ::API::SystemHooks - mount ::API::ProjectSnippets - mount ::API::ProjectMembers - mount ::API::DeployKeys - mount ::API::ProjectHooks - mount ::API::Services - mount ::API::Files - mount ::API::Commits - mount ::API::CommitStatuses - mount ::API::Namespaces - mount ::API::Branches - mount ::API::Labels - mount ::API::Settings - mount ::API::Keys - mount ::API::Tags - mount ::API::Triggers - mount ::API::Builds - mount ::API::Variables mount ::API::Runners - mount ::API::Licenses + mount ::API::Services + mount ::API::Session + mount ::API::Settings + mount ::API::SidekiqMetrics mount ::API::Subscriptions - mount ::API::Gitignores + mount ::API::SystemHooks + mount ::API::Tags + mount ::API::Templates + mount ::API::Triggers + mount ::API::Users + mount ::API::Variables end end diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb new file mode 100644 index 0000000000..985590312e --- /dev/null +++ b/lib/api/award_emoji.rb @@ -0,0 +1,116 @@ +module API + class AwardEmoji < Grape::API + before { authenticate! } + AWARDABLES = [Issue, MergeRequest] + + resource :projects do + AWARDABLES.each do |awardable_type| + awardable_string = awardable_type.to_s.underscore.pluralize + awardable_id_string = "#{awardable_type.to_s.underscore}_id" + + [ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", + ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji" + ].each do |endpoint| + + # Get a list of project +awardable+ award emoji + # + # Parameters: + # id (required) - The ID of a project + # awardable_id (required) - The ID of an issue or MR + # Example Request: + # GET /projects/:id/issues/:awardable_id/award_emoji + get endpoint do + if can_read_awardable? + awards = paginate(awardable.award_emoji) + present awards, with: Entities::AwardEmoji + else + not_found!("Award Emoji") + end + end + + # Get a specific award emoji + # + # Parameters: + # id (required) - The ID of a project + # awardable_id (required) - The ID of an issue or MR + # award_id (required) - The ID of the award + # Example Request: + # GET /projects/:id/issues/:awardable_id/award_emoji/:award_id + get "#{endpoint}/:award_id" do + if can_read_awardable? + present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji + else + not_found!("Award Emoji") + end + end + + # Award a new Emoji + # + # Parameters: + # id (required) - The ID of a project + # awardable_id (required) - The ID of an issue or mr + # name (required) - The name of a award_emoji (without colons) + # Example Request: + # POST /projects/:id/issues/:awardable_id/award_emoji + post endpoint do + required_attributes! [:name] + + not_found!('Award Emoji') unless can_read_awardable? + + award = awardable.award_emoji.new(name: params[:name], user: current_user) + + if award.save + present award, with: Entities::AwardEmoji + else + not_found!("Award Emoji #{award.errors.messages}") + end + end + + # Delete a +awardables+ award emoji + # + # Parameters: + # id (required) - The ID of a project + # awardable_id (required) - The ID of an issue or MR + # award_emoji_id (required) - The ID of an award emoji + # Example Request: + # DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id + delete "#{endpoint}/:award_id" do + award = awardable.award_emoji.find(params[:award_id]) + + unauthorized! unless award.user == current_user || current_user.admin? + + award.destroy + present award, with: Entities::AwardEmoji + end + end + end + end + + helpers do + def can_read_awardable? + ability = "read_#{awardable.class.to_s.underscore}".to_sym + + can?(current_user, ability, awardable) + end + + def awardable + @awardable ||= + begin + if params.include?(:note_id) + noteable.notes.find(params[:note_id]) + else + noteable + end + end + end + + def noteable + if params.include?(:issue_id) + user_project.issues.find(params[:issue_id]) + else + user_project.merge_requests.find(params[:merge_request_id]) + end + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index cc29c7ef42..5a23a18fe9 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -225,6 +225,14 @@ module API expose(:downvote?) { |note| false } end + class AwardEmoji < Grape::Entity + expose :id + expose :name + expose :user, using: Entities::UserBasic + expose :created_at, :updated_at + expose :awardable_id, :awardable_type + end + class MRNote < Grape::Entity expose :note expose :author, using: Entities::UserBasic @@ -415,6 +423,7 @@ module API class RunnerDetails < Runner expose :tag_list expose :run_untagged + expose :locked expose :version, :revision, :platform, :architecture expose :contacted_at expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? } @@ -436,11 +445,7 @@ module API expose :created_at, :started_at, :finished_at expose :user, with: User expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? } - expose :commit, with: RepoCommit do |repo_obj, _options| - if repo_obj.respond_to?(:commit) - repo_obj.commit.commit_data - end - end + expose :commit, with: RepoCommit expose :runner, with: Runner end @@ -464,11 +469,11 @@ module API expose :content end - class GitignoresList < Grape::Entity + class TemplatesList < Grape::Entity expose :name end - class Gitignore < Grape::Entity + class Template < Grape::Entity expose :name, :content end end diff --git a/lib/api/gitignores.rb b/lib/api/gitignores.rb deleted file mode 100644 index 270c9501dd..0000000000 --- a/lib/api/gitignores.rb +++ /dev/null @@ -1,29 +0,0 @@ -module API - class Gitignores < Grape::API - - # Get the list of the available gitignore templates - # - # Example Request: - # GET /gitignores - get 'gitignores' do - present Gitlab::Gitignore.all, with: Entities::GitignoresList - end - - # Get the text for a specific gitignore - # - # Parameters: - # name (required) - The name of a license - # - # Example Request: - # GET /gitignores/Elixir - # - get 'gitignores/:name' do - required_attributes! [:name] - - gitignore = Gitlab::Gitignore.find(params[:name]) - not_found!('.gitignore') unless gitignore - - present gitignore, with: Entities::Gitignore - end - end -end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index de5959e3aa..77e407b54c 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -9,9 +9,13 @@ module API [ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(value) end + def find_user_by_private_token + token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s + User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string) + end + def current_user - private_token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s - @current_user ||= (User.find_by(authentication_token: private_token) || doorkeeper_guard) + @current_user ||= (find_user_by_private_token || doorkeeper_guard) unless @current_user && Gitlab::UserAccess.allowed?(@current_user) return nil @@ -33,7 +37,7 @@ module API identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] # Regex for integers - if !!(identifier =~ /^[0-9]+$/) + if !!(identifier =~ /\A[0-9]+\z/) identifier.to_i else identifier diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 3ac7b50c4c..1d361569d5 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -23,8 +23,6 @@ module API end post "/allowed" do - Gitlab::Metrics.action = 'Grape#/internal/allowed' - status 200 actor = diff --git a/lib/api/notes.rb b/lib/api/notes.rb index d4fcfd3d4d..8bfa998dc5 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -144,7 +144,7 @@ module API helpers do def noteable_read_ability_name(noteable) - "read_#{noteable.class.to_s.underscore.downcase}".to_sym + "read_#{noteable.class.to_s.underscore}".to_sym end end end diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 4faba9dc87..ecc8f2fc5a 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -49,7 +49,7 @@ module API runner = get_runner(params[:id]) authenticate_update_runner!(runner) - attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged] + attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged, :locked] if runner.update(attrs) present runner, with: Entities::RunnerDetails, current_user: current_user else @@ -96,9 +96,14 @@ module API runner = get_runner(params[:runner_id]) authenticate_enable_runner!(runner) - Ci::RunnerProject.create(runner: runner, project: user_project) - present runner, with: Entities::Runner + runner_project = runner.assign_to(user_project) + + if runner_project.persisted? + present runner, with: Entities::Runner + else + conflict!("Runner was already enabled for this project") + end end # Disable project's runner @@ -163,6 +168,7 @@ module API def authenticate_enable_runner!(runner) forbidden!("Runner is shared") if runner.is_shared? + forbidden!("Runner is locked") if runner.locked? return if current_user.is_admin? forbidden!("No access granted") unless user_can_access_runner?(runner) end diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb new file mode 100644 index 0000000000..d3d6827dc5 --- /dev/null +++ b/lib/api/sidekiq_metrics.rb @@ -0,0 +1,90 @@ +require 'sidekiq/api' + +module API + class SidekiqMetrics < Grape::API + before { authenticated_as_admin! } + + helpers do + def queue_metrics + Sidekiq::Queue.all.each_with_object({}) do |queue, hash| + hash[queue.name] = { + backlog: queue.size, + latency: queue.latency.to_i + } + end + end + + def process_metrics + Sidekiq::ProcessSet.new.map do |process| + { + hostname: process['hostname'], + pid: process['pid'], + tag: process['tag'], + started_at: Time.at(process['started_at']), + queues: process['queues'], + labels: process['labels'], + concurrency: process['concurrency'], + busy: process['busy'] + } + end + end + + def job_stats + stats = Sidekiq::Stats.new + { + processed: stats.processed, + failed: stats.failed, + enqueued: stats.enqueued + } + end + end + + # Get Sidekiq Queue metrics + # + # Parameters: + # None + # + # Example: + # GET /sidekiq/queue_metrics + # + get 'sidekiq/queue_metrics' do + { queues: queue_metrics } + end + + # Get Sidekiq Process metrics + # + # Parameters: + # None + # + # Example: + # GET /sidekiq/process_metrics + # + get 'sidekiq/process_metrics' do + { processes: process_metrics } + end + + # Get Sidekiq Job statistics + # + # Parameters: + # None + # + # Example: + # GET /sidekiq/job_stats + # + get 'sidekiq/job_stats' do + { jobs: job_stats } + end + + # Get Sidekiq Compound metrics. Includes all previous metrics + # + # Parameters: + # None + # + # Example: + # GET /sidekiq/compound_metrics + # + get 'sidekiq/compound_metrics' do + { queues: queue_metrics, processes: process_metrics, jobs: job_stats } + end + end +end diff --git a/lib/api/templates.rb b/lib/api/templates.rb new file mode 100644 index 0000000000..1840879775 --- /dev/null +++ b/lib/api/templates.rb @@ -0,0 +1,36 @@ +module API + class Templates < Grape::API + TEMPLATE_TYPES = { + gitignores: Gitlab::Template::Gitignore, + gitlab_ci_ymls: Gitlab::Template::GitlabCiYml + }.freeze + + TEMPLATE_TYPES.each do |template, klass| + # Get the list of the available template + # + # Example Request: + # GET /gitignores + # GET /gitlab_ci_ymls + get template.to_s do + present klass.all, with: Entities::TemplatesList + end + + # Get the text for a specific template + # + # Parameters: + # name (required) - The name of a template + # + # Example Request: + # GET /gitignores/Elixir + # GET /gitlab_ci_ymls/Ruby + get "#{template}/:name" do + required_attributes! [:name] + + new_template = klass.find(params[:name]) + not_found!(template.to_s.singularize) unless new_template + + present new_template, with: Entities::Template + end + end + end +end diff --git a/lib/banzai.rb b/lib/banzai.rb index b467413a7d..093382261a 100644 --- a/lib/banzai.rb +++ b/lib/banzai.rb @@ -7,10 +7,6 @@ module Banzai Renderer.render_result(text, context) end - def self.pre_process(text, context) - Renderer.pre_process(text, context) - end - def self.post_process(html, context) Renderer.post_process(html, context) end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index db95d7c908..81d6627113 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -103,7 +103,7 @@ module Banzai ref_pattern = object_class.reference_pattern link_pattern = object_class.link_reference_pattern - each_node do |node| + nodes.each do |node| if text_node?(node) && ref_pattern replace_text_when_pattern_matches(node, ref_pattern) do |content| object_link_filter(content, ref_pattern) @@ -206,6 +206,56 @@ module Banzai text end + # Returns a Hash containing all object references (e.g. issue IDs) per the + # project they belong to. + def references_per_project + @references_per_project ||= begin + refs = Hash.new { |hash, key| hash[key] = Set.new } + + regex = Regexp.union(object_class.reference_pattern, + object_class.link_reference_pattern) + + nodes.each do |node| + node.to_html.scan(regex) do + project = $~[:project] || current_project_path + symbol = $~[object_sym] + + refs[project] << symbol if object_class.reference_valid?(symbol) + end + end + + refs + end + end + + # Returns a Hash containing referenced projects grouped per their full + # path. + def projects_per_reference + @projects_per_reference ||= begin + hash = {} + refs = Set.new + + references_per_project.each do |project_ref, _| + refs << project_ref + end + + find_projects_for_paths(refs.to_a).each do |project| + hash[project.path_with_namespace] = project + end + + hash + end + end + + # Returns the projects for the given paths. + def find_projects_for_paths(paths) + Project.where_paths_in(paths).includes(:namespace) + end + + def current_project_path + @current_project_path ||= project.path_with_namespace + end + private def project_refs_cache diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index f73ecfc941..0a29c547a4 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -3,17 +3,8 @@ module Banzai # HTML Filter to modify the attributes of external links class ExternalLinkFilter < HTML::Pipeline::Filter def call - doc.search('a').each do |node| - link = node.attr('href') - - next unless link - - # Skip non-HTTP(S) links - next unless link.start_with?('http') - - # Skip internal links - next if link.start_with?(internal_url) - + # Skip non-HTTP(S) links and internal links + doc.xpath("descendant-or-self::a[starts-with(@href, 'http') and not(starts-with(@href, '#{internal_url}'))]").each do |node| node.set_attribute('rel', 'nofollow noreferrer') node.set_attribute('target', '_blank') end diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index 2496e70400..2614261f9e 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -11,13 +11,40 @@ module Banzai Issue end - def find_object(project, id) - project.get_issue(id) + def find_object(project, iid) + issues_per_project[project][iid] end def url_for_object(issue, project) IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path]) end + + def project_from_ref(ref) + projects_per_reference[ref || current_project_path] + end + + # Returns a Hash containing the issues per Project instance. + def issues_per_project + @issues_per_project ||= begin + hash = Hash.new { |h, k| h[k] = {} } + + projects_per_reference.each do |path, project| + issue_ids = references_per_project[path] + + next unless project.default_issues_tracker? + + project.issues.where(iid: issue_ids.to_a).each do |issue| + hash[project][issue.iid] = issue + end + end + + hash + end + end + + def find_projects_for_paths(paths) + super(paths).includes(:gitlab_issue_tracker_service) + end end end end diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index ea21c7b041..c78da40460 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -14,6 +14,8 @@ module Banzai def call return doc unless linkable_files? + @uri_types = {} + doc.search('a:not(.gfm)').each do |el| process_link_attr el.attribute('href') end @@ -48,7 +50,7 @@ module Banzai uri.path = [ relative_url_root, context[:project].path_with_namespace, - path_type(file_path), + uri_type(file_path), ref || context[:project].default_branch, # if no ref exists, point to the default branch file_path ].compact.join('/').squeeze('/').chomp('/') @@ -87,7 +89,7 @@ module Banzai return path unless request_path parts = request_path.split('/') - parts.pop if path_type(request_path) != 'tree' + parts.pop if uri_type(request_path) != :tree while path.start_with?('../') parts.pop @@ -98,45 +100,20 @@ module Banzai end def file_exists?(path) - return false if path.nil? - repository.blob_at(current_sha, path).present? || - repository.tree(current_sha, path).entries.any? + path.present? && !!uri_type(path) end - # Get the type of the given path - # - # path - String path to check - # - # Examples: - # - # path_type('doc/README.md') # => 'blob' - # path_type('doc/logo.png') # => 'raw' - # path_type('doc/api') # => 'tree' - # - # Returns a String - def path_type(path) - unescaped_path = Addressable::URI.unescape(path) + def uri_type(path) + @uri_types[path] ||= begin + unescaped_path = Addressable::URI.unescape(path) - if tree?(unescaped_path) - 'tree' - elsif image?(unescaped_path) - 'raw' - else - 'blob' + current_commit.uri_type(unescaped_path) end end - def tree?(path) - repository.tree(current_sha, path).entries.any? - end - - def image?(path) - repository.blob_at(current_sha, path).try(:image?) - end - - def current_sha - context[:commit].try(:id) || - ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha + def current_commit + @current_commit ||= context[:commit] || + ref ? repository.commit(ref) : repository.head_commit end def relative_url_root @@ -148,7 +125,7 @@ module Banzai end def repository - context[:project].try(:repository) + @repository ||= context[:project].try(:repository) end end end diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb index c0f503c9af..45bb66dc99 100644 --- a/lib/banzai/filter/upload_link_filter.rb +++ b/lib/banzai/filter/upload_link_filter.rb @@ -10,11 +10,11 @@ module Banzai def call return doc unless project - doc.search('a').each do |el| + doc.xpath('descendant-or-self::a[starts-with(@href, "/uploads/")]').each do |el| process_link_attr el.attribute('href') end - doc.search('img').each do |el| + doc.xpath('descendant-or-self::img[starts-with(@src, "/uploads/")]').each do |el| process_link_attr el.attribute('src') end @@ -24,12 +24,7 @@ module Banzai protected def process_link_attr(html_attr) - return if html_attr.blank? - - uri = html_attr.value - if uri.starts_with?("/uploads/") - html_attr.value = build_url(uri).to_s - end + html_attr.value = build_url(html_attr.value).to_s end def build_url(uri) diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index c14a9c4c72..6718acdef7 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -30,15 +30,11 @@ module Banzai end def self.render_result(text, context = {}) + text = Pipeline[:pre_process].to_html(text, context) if text + Pipeline[context[:pipeline]].call(text, context) end - def self.pre_process(text, context) - pipeline = Pipeline[:pre_process] - - pipeline.to_html(text, context) - end - # Perform post-processing on an HTML String # # This method is used to perform state-dependent changes to a String of diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index 0c41f22c7c..bcc82969eb 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -28,12 +28,9 @@ module Ci post "register" do required_attributes! [:token] - attributes = { description: params[:description], - tag_list: params[:tag_list] } - - unless params[:run_untagged].nil? - attributes[:run_untagged] = params[:run_untagged] - end + attributes = attributes_for_keys( + [:description, :tag_list, :run_untagged, :locked] + ) runner = if runner_registration_token_valid? diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 68246497e9..ed86de819e 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -30,7 +30,10 @@ module Ci end def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil) - builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag, trigger_request)} + builds.select do |build| + build[:stage] == stage && + process?(build[:only], build[:except], ref, tag, trigger_request) + end end def builds @@ -51,7 +54,7 @@ module Ci job = @jobs[name.to_sym] return [] unless job - job.fetch(:variables, []) + job[:variables] || [] end private @@ -201,12 +204,12 @@ module Ci raise ValidationError, "#{name} job: tags parameter should be an array of strings" end - if job[:only] && !validate_array_of_strings(job[:only]) - raise ValidationError, "#{name} job: only parameter should be an array of strings" + if job[:only] && !validate_array_of_strings_or_regexps(job[:only]) + raise ValidationError, "#{name} job: only parameter should be an array of strings or regexps" end - if job[:except] && !validate_array_of_strings(job[:except]) - raise ValidationError, "#{name} job: except parameter should be an array of strings" + if job[:except] && !validate_array_of_strings_or_regexps(job[:except]) + raise ValidationError, "#{name} job: except parameter should be an array of strings or regexps" end if job[:allow_failure] && !validate_boolean(job[:allow_failure]) diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index e0b3f14d38..42232b7129 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -15,11 +15,11 @@ module ContainerRegistry end def repository_tags(name) - @faraday.get("/v2/#{name}/tags/list").body + response_body @faraday.get("/v2/#{name}/tags/list") end def repository_manifest(name, reference) - @faraday.get("/v2/#{name}/manifests/#{reference}").body + response_body @faraday.get("/v2/#{name}/manifests/#{reference}") end def repository_tag_digest(name, reference) @@ -34,7 +34,7 @@ module ContainerRegistry def blob(name, digest, type = nil) headers = {} headers['Accept'] = type if type - @faraday.get("/v2/#{name}/blobs/#{digest}", nil, headers).body + response_body @faraday.get("/v2/#{name}/blobs/#{digest}", nil, headers) end def delete_blob(name, digest) @@ -47,6 +47,7 @@ module ContainerRegistry conn.request :json conn.headers['Accept'] = MANIFEST_VERSION + conn.response :json, content_type: 'application/json' conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+prettyjws' conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+json' conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v2+json' @@ -59,5 +60,9 @@ module ContainerRegistry conn.adapter :net_http end + + def response_body(response) + response.body if response.success? + end end end diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb index 7a0929d774..708d01b95a 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -3,6 +3,7 @@ module ContainerRegistry attr_reader :repository, :name delegate :registry, :client, to: :repository + delegate :revision, :short_revision, to: :config_blob, allow_nil: true def initialize(repository, name) @repository, @name = repository, name diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 7e3f5abba6..ab7b811c5d 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -31,7 +31,7 @@ module Grack auth! - lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call + lfs_response = Gitlab::Lfs::Router.new(project, @user, @ci, @request).try_call return lfs_response unless lfs_response.nil? if @user.nil? && !@ci diff --git a/lib/gitlab/ci/config/node/validation_helpers.rb b/lib/gitlab/ci/config/node/validation_helpers.rb index 3900fc8939..72f648975d 100644 --- a/lib/gitlab/ci/config/node/validation_helpers.rb +++ b/lib/gitlab/ci/config/node/validation_helpers.rb @@ -15,6 +15,10 @@ module Gitlab values.is_a?(Array) && values.all? { |value| validate_string(value) } end + def validate_array_of_strings_or_regexps(values) + values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) } + end + def validate_variables(variables) variables.is_a?(Hash) && variables.all? { |key, value| validate_string(key) && validate_string(value) } @@ -24,6 +28,19 @@ module Gitlab value.is_a?(String) || value.is_a?(Symbol) end + def validate_string_or_regexp(value) + return true if value.is_a?(Symbol) + return false unless value.is_a?(String) + + if value.first == '/' && value.last == '/' + Regexp.new(value[1...-1]) + else + true + end + rescue RegexpError + false + end + def validate_environment(value) value.is_a?(String) && value =~ Gitlab::Regex.environment_name_regex end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 5e7532f57a..28c34429c1 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -36,7 +36,7 @@ module Gitlab default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], - import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], + import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index d76ecb5401..078609c86f 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -1,5 +1,10 @@ module Gitlab module Database + # The max value of INTEGER type is the same between MySQL and PostgreSQL: + # https://www.postgresql.org/docs/9.2/static/datatype-numeric.html + # http://dev.mysql.com/doc/refman/5.7/en/integer-types.html + MAX_INT_VALUE = 2147483647 + def self.adapter_name connection.adapter_name end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index dd3ff0ab18..dec20d8659 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -28,65 +28,79 @@ module Gitlab # Updates the value of a column in batches. # # This method updates the table in batches of 5% of the total row count. - # Any data inserted while running this method (or after it has finished - # running) is _not_ updated automatically. + # This method will continue updating rows until no rows remain. + # + # When given a block this method will yield two values to the block: + # + # 1. An instance of `Arel::Table` for the table that is being updated. + # 2. The query to run as an Arel object. + # + # By supplying a block one can add extra conditions to the queries being + # executed. Note that the same block is used for _all_ queries. + # + # Example: + # + # update_column_in_batches(:projects, :foo, 10) do |table, query| + # query.where(table[:some_column].eq('hello')) + # end + # + # This would result in this method updating only rows where + # `projects.some_column` equals "hello". # # table - The name of the table. # column - The name of the column to update. # value - The value for the column. + # + # Rubocop's Metrics/AbcSize metric is disabled for this method as Rubocop + # determines this method to be too complex while there's no way to make it + # less "complex" without introducing extra methods (which actually will + # make things _more_ complex). + # + # rubocop: disable Metrics/AbcSize def update_column_in_batches(table, column, value) - quoted_table = quote_table_name(table) - quoted_column = quote_column_name(column) + table = Arel::Table.new(table) - ## - # Workaround for #17711 - # - # It looks like for MySQL `ActiveRecord::Base.conntection.quote(true)` - # returns correct value (1), but `ActiveRecord::Migration.new.quote` - # returns incorrect value ('true'), which causes migrations to fail. - # - quoted_value = connection.quote(value) - processed = 0 + count_arel = table.project(Arel.star.count.as('count')) + count_arel = yield table, count_arel if block_given? - total = exec_query("SELECT COUNT(*) AS count FROM #{quoted_table}"). - to_hash. - first['count']. - to_i + total = exec_query(count_arel.to_sql).to_hash.first['count'].to_i + + return if total == 0 # Update in batches of 5% until we run out of any rows to update. batch_size = ((total / 100.0) * 5.0).ceil + start_arel = table.project(table[:id]).order(table[:id].asc).take(1) + start_arel = yield table, start_arel if block_given? + start_id = exec_query(start_arel.to_sql).to_hash.first['id'].to_i + loop do - start_row = exec_query(%Q{ - SELECT id - FROM #{quoted_table} - ORDER BY id ASC - LIMIT 1 OFFSET #{processed} - }).to_hash.first + stop_arel = table.project(table[:id]). + where(table[:id].gteq(start_id)). + order(table[:id].asc). + take(1). + skip(batch_size) - # There are no more rows to process - break unless start_row + stop_arel = yield table, stop_arel if block_given? + stop_row = exec_query(stop_arel.to_sql).to_hash.first - stop_row = exec_query(%Q{ - SELECT id - FROM #{quoted_table} - ORDER BY id ASC - LIMIT 1 OFFSET #{processed + batch_size} - }).to_hash.first - - query = %Q{ - UPDATE #{quoted_table} - SET #{quoted_column} = #{quoted_value} - WHERE id >= #{start_row['id']} - } + update_arel = Arel::UpdateManager.new(ActiveRecord::Base). + table(table). + set([[table[column], value]]). + where(table[:id].gteq(start_id)) if stop_row - query += " AND id < #{stop_row['id']}" + stop_id = stop_row['id'].to_i + start_id = stop_id + update_arel = update_arel.where(table[:id].lt(stop_id)) end - execute(query) + update_arel = yield table, update_arel if block_given? - processed += batch_size + execute(update_arel.to_sql) + + # There are no more rows left to update. + break unless stop_row end end @@ -95,9 +109,9 @@ module Gitlab # This method runs the following steps: # # 1. Add the column with a default value of NULL. - # 2. Update all existing rows in batches. - # 3. Change the default value of the column to the specified value. - # 4. Update any remaining rows. + # 2. Change the default value of the column to the specified value. + # 3. Update all existing rows in batches. + # 4. Set a `NOT NULL` constraint on the column if desired (the default). # # These steps ensure a column can be added to a large and commonly used # table without locking the entire table for the duration of the table @@ -109,7 +123,10 @@ module Gitlab # default - The default value for the column. # allow_null - When set to `true` the column will allow NULL values, the # default is to not allow NULL values. - def add_column_with_default(table, column, type, default:, allow_null: false) + # + # This method can also take a block which is passed directly to the + # `update_column_in_batches` method. + def add_column_with_default(table, column, type, default:, allow_null: false, &block) if transaction_open? raise 'add_column_with_default can not be run inside a transaction, ' \ 'you can disable transactions by calling disable_ddl_transaction! ' \ @@ -125,11 +142,9 @@ module Gitlab end begin - transaction do - update_column_in_batches(table, column, default) + update_column_in_batches(table, column, default, &block) - change_column_null(table, column, false) unless allow_null - end + change_column_null(table, column, false) unless allow_null # We want to rescue _all_ exceptions here, even those that don't inherit # from StandardError. rescue Exception => error # rubocop: disable all diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index e2fee6b9f3..047c77c6fc 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -37,7 +37,7 @@ module Gitlab end def diffs - @diffs ||= (safe_diff_files(compare.diffs, diff_refs) if compare) + @diffs ||= (safe_diff_files(compare.diffs(max_files: 30), diff_refs) if compare) end def diffs_count diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index e5cf66a037..2286ac8829 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -66,8 +66,7 @@ module Gitlab end def import_pull_requests - hooks = client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?) - disable_webhooks(hooks) + disable_webhooks pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?) @@ -90,14 +89,14 @@ module Gitlab raise Projects::ImportService::Error, e.message ensure clean_up_restored_branches(branches_removed) - clean_up_disabled_webhooks(hooks) + clean_up_disabled_webhooks end - def disable_webhooks(hooks) + def disable_webhooks update_webhooks(hooks, active: false) end - def clean_up_disabled_webhooks(hooks) + def clean_up_disabled_webhooks update_webhooks(hooks, active: true) end @@ -107,6 +106,20 @@ module Gitlab end end + def hooks + @hooks ||= + begin + client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?) + + # The GitHub Repository Webhooks API returns 404 for users + # without admin access to the repository when listing hooks. + # In this case we just want to return gracefully instead of + # spitting out an error and stop the import process. + rescue Octokit::NotFound + [] + end + end + def restore_branches(branches) branches.each do |name, sha| client.create_ref(repo, "refs/heads/#{name}", sha) diff --git a/lib/gitlab/gitignore.rb b/lib/gitlab/gitignore.rb deleted file mode 100644 index f46b43b61a..0000000000 --- a/lib/gitlab/gitignore.rb +++ /dev/null @@ -1,56 +0,0 @@ -module Gitlab - class Gitignore - FILTER_REGEX = /\.gitignore\z/.freeze - - def initialize(path) - @path = path - end - - def name - File.basename(@path, '.gitignore') - end - - def content - File.read(@path) - end - - class << self - def all - languages_frameworks + global - end - - def find(key) - file_name = "#{key}.gitignore" - - directory = select_directory(file_name) - directory ? new(File.join(directory, file_name)) : nil - end - - def global - files_for_folder(global_dir).map { |file| new(File.join(global_dir, file)) } - end - - def languages_frameworks - files_for_folder(gitignore_dir).map { |file| new(File.join(gitignore_dir, file)) } - end - - private - - def select_directory(file_name) - [gitignore_dir, global_dir].find { |dir| File.exist?(File.join(dir, file_name)) } - end - - def global_dir - File.join(gitignore_dir, 'Global') - end - - def gitignore_dir - Rails.root.join('vendor/gitignore') - end - - def files_for_folder(dir) - Dir.glob("#{dir.to_s}/*.gitignore").map { |file| file.gsub(FILTER_REGEX, '') } - end - end - end -end diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb index 77c33db4b5..3d0418261b 100644 --- a/lib/gitlab/gitlab_import/project_creator.rb +++ b/lib/gitlab/gitlab_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["path"], @@ -22,8 +22,6 @@ module Gitlab import_source: repo["path_with_namespace"], import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{@session_data[:gitlab_access_token]}@") ).execute - - project end end end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb new file mode 100644 index 0000000000..99cf85d9a3 --- /dev/null +++ b/lib/gitlab/import_export.rb @@ -0,0 +1,39 @@ +module Gitlab + module ImportExport + extend self + + VERSION = '0.1.0' + + def export_path(relative_path:) + File.join(storage_path, relative_path) + end + + def storage_path + File.join(Settings.shared['path'], 'tmp/project_exports') + end + + def project_filename + "project.json" + end + + def project_bundle_filename + "project.bundle" + end + + def config_file + Rails.root.join('lib/gitlab/import_export/import_export.yml') + end + + def version_filename + 'VERSION' + end + + def version + VERSION + end + + def reset_tokens? + true + end + end +end diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb new file mode 100644 index 0000000000..d230de781d --- /dev/null +++ b/lib/gitlab/import_export/attributes_finder.rb @@ -0,0 +1,47 @@ +module Gitlab + module ImportExport + class AttributesFinder + + def initialize(included_attributes:, excluded_attributes:, methods:) + @included_attributes = included_attributes || {} + @excluded_attributes = excluded_attributes || {} + @methods = methods || {} + end + + def find(model_object) + parsed_hash = find_attributes_only(model_object) + parsed_hash.empty? ? model_object : { model_object => parsed_hash } + end + + def parse(model_object) + parsed_hash = find_attributes_only(model_object) + yield parsed_hash unless parsed_hash.empty? + end + + def find_included(value) + key = key_from_hash(value) + @included_attributes[key].nil? ? {} : { only: @included_attributes[key] } + end + + def find_excluded(value) + key = key_from_hash(value) + @excluded_attributes[key].nil? ? {} : { except: @excluded_attributes[key] } + end + + def find_method(value) + key = key_from_hash(value) + @methods[key].nil? ? {} : { methods: @methods[key] } + end + + private + + def find_attributes_only(value) + find_included(value).merge(find_excluded(value)).merge(find_method(value)) + end + + def key_from_hash(value) + value.is_a?(Hash) ? value.keys.first : value + end + end + end +end diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb new file mode 100644 index 0000000000..78664f076e --- /dev/null +++ b/lib/gitlab/import_export/command_line_util.rb @@ -0,0 +1,40 @@ +module Gitlab + module ImportExport + module CommandLineUtil + def tar_czf(archive:, dir:) + tar_with_options(archive: archive, dir: dir, options: 'czf') + end + + def untar_zxf(archive:, dir:) + untar_with_options(archive: archive, dir: dir, options: 'zxf') + end + + def git_bundle(repo_path:, bundle_path:) + execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all)) + end + + def git_unbundle(repo_path:, bundle_path:) + execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path})) + end + + private + + def tar_with_options(archive:, dir:, options:) + execute(%W(tar -#{options} #{archive} -C #{dir} .)) + end + + def untar_with_options(archive:, dir:, options:) + execute(%W(tar -#{options} #{archive} -C #{dir})) + end + + def execute(cmd) + _output, status = Gitlab::Popen.popen(cmd) + status.zero? + end + + def git_bin_path + Gitlab.config.git.bin_path + end + end + end +end diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb new file mode 100644 index 0000000000..e341c4d9cf --- /dev/null +++ b/lib/gitlab/import_export/error.rb @@ -0,0 +1,5 @@ +module Gitlab + module ImportExport + class Error < StandardError; end + end +end diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb new file mode 100644 index 0000000000..0e70d9282d --- /dev/null +++ b/lib/gitlab/import_export/file_importer.rb @@ -0,0 +1,30 @@ +module Gitlab + module ImportExport + class FileImporter + include Gitlab::ImportExport::CommandLineUtil + + def self.import(*args) + new(*args).import + end + + def initialize(archive_file:, shared:) + @archive_file = archive_file + @shared = shared + end + + def import + FileUtils.mkdir_p(@shared.export_path) + decompress_archive + rescue => e + @shared.error(e) + false + end + + private + + def decompress_archive + untar_zxf(archive: @archive_file, dir: @shared.export_path) + end + end + end +end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml new file mode 100644 index 0000000000..164ab6238c --- /dev/null +++ b/lib/gitlab/import_export/import_export.yml @@ -0,0 +1,54 @@ +# Model relationships to be included in the project import/export +project_tree: + - issues: + - notes: + :author + - :labels + - :milestones + - snippets: + - notes: + :author + - :releases + - :events + - project_members: + - :user + - merge_requests: + - notes: + :author + - :merge_request_diff + - pipelines: + - notes: + :author + - :statuses + - :variables + - :triggers + - :deploy_keys + - :services + - :hooks + - :protected_branches + +# Only include the following attributes for the models specified. +included_attributes: + project: + - :description + - :issues_enabled + - :merge_requests_enabled + - :wiki_enabled + - :snippets_enabled + - :visibility_level + - :archived + user: + - :id + - :email + - :username + author: + - :name + +# Do not include the following attributes for the models specified. +excluded_attributes: + snippets: + - :expired_at + +methods: + statuses: + - :type \ 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 0000000000..d209e04f7b --- /dev/null +++ b/lib/gitlab/import_export/importer.rb @@ -0,0 +1,64 @@ +module Gitlab + module ImportExport + class Importer + + def initialize(project) + @archive_file = project.import_source + @current_user = project.creator + @project = project + @shared = Gitlab::ImportExport::Shared.new(relative_path: path_with_namespace) + end + + def execute + Gitlab::ImportExport::FileImporter.import(archive_file: @archive_file, + shared: @shared) + if check_version! && [project_tree, repo_restorer, wiki_restorer, uploads_restorer].all?(&:restore) + project_tree.restored_project + else + raise Projects::ImportService::Error.new(@shared.errors.join(', ')) + end + end + + private + + def check_version! + Gitlab::ImportExport::VersionChecker.check!(shared: @shared) + end + + def project_tree + @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(user: @current_user, + shared: @shared, + project: @project) + end + + def repo_restorer + Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path, + shared: @shared, + project: project_tree.restored_project) + end + + def wiki_restorer + Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path, + shared: @shared, + project: ProjectWiki.new(project_tree.restored_project), + wiki: true) + end + + def uploads_restorer + Gitlab::ImportExport::UploadsRestorer.new(project: project_tree.restored_project, shared: @shared) + end + + def path_with_namespace + File.join(@project.namespace.path, @project.path) + end + + def repo_path + File.join(@shared.export_path, 'project.bundle') + end + + def wiki_repo_path + File.join(@shared.export_path, 'project.wiki.bundle') + 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 0000000000..c569a35a48 --- /dev/null +++ b/lib/gitlab/import_export/members_mapper.rb @@ -0,0 +1,68 @@ +module Gitlab + module ImportExport + class MembersMapper + + attr_reader :missing_author_ids + + def initialize(exported_members:, user:, project:) + @exported_members = exported_members + @user = user + @project = project + @missing_author_ids = [] + + # This needs to run first, as second call would be from #map + # which means project members already exist. + ensure_default_member! + end + + def map + @map ||= + begin + @exported_members.inject(missing_keys_tracking_hash) do |hash, member| + existing_user = User.where(find_project_user_query(member)).first + old_user_id = member['user']['id'] + if existing_user && add_user_as_team_member(existing_user, member) + hash[old_user_id] = existing_user.id + end + hash + end + end + end + + def default_user_id + @user.id + end + + private + + def missing_keys_tracking_hash + Hash.new do |_, key| + @missing_author_ids << key + default_user_id + end + end + + def ensure_default_member! + ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true) + end + + def add_user_as_team_member(existing_user, member) + member['user'] = existing_user + + ProjectMember.create(member_hash(member)).persisted? + end + + def member_hash(member) + member.except('id').merge(source_id: @project.id, importing: true) + 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_creator.rb b/lib/gitlab/import_export/project_creator.rb new file mode 100644 index 0000000000..89388d1984 --- /dev/null +++ b/lib/gitlab/import_export/project_creator.rb @@ -0,0 +1,24 @@ +module Gitlab + module ImportExport + class ProjectCreator + + def initialize(namespace_id, current_user, file, project_path) + @namespace_id = namespace_id + @current_user = current_user + @file = file + @project_path = project_path + end + + def execute + ::Projects::CreateService.new( + @current_user, + name: @project_path, + path: @project_path, + namespace_id: @namespace_id, + import_type: "gitlab_project", + import_source: @file + ).execute + 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 0000000000..dd71b92c52 --- /dev/null +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -0,0 +1,105 @@ +module Gitlab + module ImportExport + class ProjectTreeRestorer + + def initialize(user:, shared:, project:) + @path = File.join(shared.export_path, 'project.json') + @user = user + @shared = shared + @project = project + end + + def restore + json = IO.read(@path) + @tree_hash = ActiveSupport::JSON.decode(json) + @project_members = @tree_hash.delete('project_members') + create_relations + rescue => e + @shared.error(e) + false + end + + def restored_project + @restored_project ||= restore_project + end + + private + + def members_mapper + @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members, + user: @user, + project: restored_project) + end + + # Loops through the tree of models defined in import_export.yml and + # finds them in the imported JSON so they can be instantiated and saved + # in the DB. The structure and relationships between models are guessed from + # the configuration yaml file too. + # Finally, it updates each attribute in the newly imported project. + def create_relations + saved = [] + default_relation_list.each do |relation| + next unless relation.is_a?(Hash) || @tree_hash[relation.to_s].present? + + create_sub_relations(relation, @tree_hash) if relation.is_a?(Hash) + + relation_key = relation.is_a?(Hash) ? relation.keys.first : relation + relation_hash = create_relation(relation_key, @tree_hash[relation_key.to_s]) + saved << restored_project.update_attribute(relation_key, relation_hash) + end + saved.all? + end + + def default_relation_list + Gitlab::ImportExport::Reader.new(shared: @shared).tree.reject do |model| + model.is_a?(Hash) && model[:project_members] + end + end + + def restore_project + return @project unless @tree_hash + + project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } + @project.update(project_params) + @project + end + + # Given a relation hash containing one or more models and its relationships, + # loops through each model and each object from a model type and + # and assigns its correspondent attributes hash from +tree_hash+ + # Example: + # +relation_key+ issues, loops through the list of *issues* and for each individual + # issue, finds any subrelations such as notes, creates them and assign them back to the hash + def create_sub_relations(relation, tree_hash) + relation_key = relation.keys.first.to_s + tree_hash[relation_key].each do |relation_item| + relation.values.flatten.each do |sub_relation| + relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation) + relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank? + end + end + end + + def assign_relation_hash(relation_item, sub_relation) + if sub_relation.is_a?(Hash) + relation_hash = relation_item[sub_relation.keys.first.to_s] + sub_relation = sub_relation.keys.first + else + relation_hash = relation_item[sub_relation.to_s] + end + [relation_hash, sub_relation] + end + + def create_relation(relation, relation_hash_list) + relation_array = [relation_hash_list].flatten.map do |relation_hash| + Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, + relation_hash: relation_hash.merge('project_id' => restored_project.id), + members_mapper: members_mapper, + user: @user) + end + + relation_hash_list.is_a?(Array) ? relation_array : relation_array.first + end + end + end +end diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb new file mode 100644 index 0000000000..9153088e96 --- /dev/null +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -0,0 +1,29 @@ +module Gitlab + module ImportExport + class ProjectTreeSaver + attr_reader :full_path + + def initialize(project:, shared:) + @project = project + @shared = shared + @full_path = File.join(@shared.export_path, ImportExport.project_filename) + end + + def save + FileUtils.mkdir_p(@shared.export_path) + + File.write(full_path, project_json_tree) + true + rescue => e + @shared.error(e) + false + end + + private + + def project_json_tree + @project.to_json(Gitlab::ImportExport::Reader.new(shared: @shared).project_tree) + end + end + end +end diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb new file mode 100644 index 0000000000..19defd8f03 --- /dev/null +++ b/lib/gitlab/import_export/reader.rb @@ -0,0 +1,117 @@ +module Gitlab + module ImportExport + class Reader + + attr_reader :tree + + def initialize(shared:) + @shared = shared + config_hash = YAML.load_file(Gitlab::ImportExport.config_file).deep_symbolize_keys + @tree = config_hash[:project_tree] + @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes], + excluded_attributes: config_hash[:excluded_attributes], + methods: config_hash[:methods]) + end + + # Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html + # for outputting a project in JSON format, including its relations and sub relations. + def project_tree + @attributes_finder.find_included(:project).merge(include: build_hash(@tree)) + rescue => e + @shared.error(e) + false + end + + private + + # Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html + # + # +model_list+ - List of models as a relation tree to be included in the generated JSON, from the _import_export.yml_ file + def build_hash(model_list) + model_list.map do |model_objects| + if model_objects.is_a?(Hash) + build_json_config_hash(model_objects) + else + @attributes_finder.find(model_objects) + end + end + end + + # Called when the model is actually a hash containing other relations (more models) + # Returns the config in the right format for calling +to_json+ + # +model_object_hash+ - A model relationship such as: + # {:merge_requests=>[:merge_request_diff, :notes]} + def build_json_config_hash(model_object_hash) + @json_config_hash = {} + + model_object_hash.values.flatten.each do |model_object| + current_key = model_object_hash.keys.first + + @attributes_finder.parse(current_key) { |hash| @json_config_hash[current_key] ||= hash } + + handle_model_object(current_key, model_object) + process_sub_model(current_key, model_object) if model_object.is_a?(Hash) + end + @json_config_hash + end + + + # If the model is a hash, process the sub_models, which could also be hashes + # If there is a list, add to an existing array, otherwise use hash syntax + # +current_key+ main model that will be a key in the hash + # +model_object+ model or list of models to include in the hash + def process_sub_model(current_key, model_object) + sub_model_json = build_json_config_hash(model_object).dup + @json_config_hash.slice!(current_key) + + if @json_config_hash[current_key] && @json_config_hash[current_key][:include] + @json_config_hash[current_key][:include] << sub_model_json + else + @json_config_hash[current_key] = { include: sub_model_json } + end + end + + # Creates or adds to an existing hash an individual model or list + # +current_key+ main model that will be a key in the hash + # +model_object+ model or list of models to include in the hash + def handle_model_object(current_key, model_object) + if @json_config_hash[current_key] + add_model_value(current_key, model_object) + else + create_model_value(current_key, model_object) + end + end + + # Constructs a new hash that will hold the configuration for that particular object + # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ + # +current_key+ main model that will be a key in the hash + # +value+ existing model to be included in the hash + def create_model_value(current_key, value) + parsed_hash = { include: value } + + @attributes_finder.parse(value) do |hash| + parsed_hash = { include: hash_or_merge(value, hash) } + end + @json_config_hash[current_key] = parsed_hash + end + + # Adds new model configuration to an existing hash with key +current_key+ + # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ + # +current_key+ main model that will be a key in the hash + # +value+ existing model to be included in the hash + def add_model_value(current_key, value) + @attributes_finder.parse(value) { |hash| value = { value => hash } } + old_values = @json_config_hash[current_key][:include] + @json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten + end + + # Construct a new hash or merge with an existing one a model configuration + # This is to fulfil +to_json+ requirements. + # +value+ existing model to be included in the hash + # +hash+ hash containing configuration generated mainly from +@attributes_finder+ + def hash_or_merge(value, hash) + value.is_a?(Hash) ? value.merge(hash) : { value => hash } + end + end + end +end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb new file mode 100644 index 0000000000..b872780f20 --- /dev/null +++ b/lib/gitlab/import_export/relation_factory.rb @@ -0,0 +1,128 @@ +module Gitlab + module ImportExport + class RelationFactory + + OVERRIDES = { snippets: :project_snippets, + pipelines: 'Ci::Pipeline', + statuses: 'commit_status', + variables: 'Ci::Variable', + triggers: 'Ci::Trigger', + builds: 'Ci::Build', + hooks: 'ProjectHook' }.freeze + + USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze + + def self.create(*args) + new(*args).create + end + + def initialize(relation_sym:, relation_hash:, members_mapper:, user:) + @relation_name = OVERRIDES[relation_sym] || relation_sym + @relation_hash = relation_hash.except('id', 'noteable_id') + @members_mapper = members_mapper + @user = user + end + + # Creates an object from an actual model with name "relation_sym" with params from + # the relation_hash, updating references with new object IDs, mapping users using + # the "members_mapper" object, also updating notes if required. + def create + set_note_author if @relation_name == :notes + update_user_references + update_project_references + reset_ci_tokens if @relation_name == 'Ci::Trigger' + + generate_imported_object + end + + private + + def update_user_references + USER_REFERENCES.each do |reference| + if @relation_hash[reference] + @relation_hash[reference] = @members_mapper.map[@relation_hash[reference]] + end + end + end + + # Sets the author for a note. If the user importing the project + # has admin access, an actual mapping with new project members + # will be used. Otherwise, a note stating the original author name + # is left. + def set_note_author + old_author_id = @relation_hash['author_id'] + + # Users with admin access can map users + @relation_hash['author_id'] = admin_user? ? @members_mapper.map[old_author_id] : @members_mapper.default_user_id + + author = @relation_hash.delete('author') + + update_note_for_missing_author(author['name']) if missing_author?(old_author_id) + end + + def missing_author?(old_author_id) + !admin_user? || @members_mapper.missing_author_ids.include?(old_author_id) + end + + def missing_author_note(updated_at, author_name) + timestamp = updated_at.split('.').first + "\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*" + end + + def generate_imported_object + if @relation_sym == 'commit_status' # call #trace= method after assigning the other attributes + trace = @relation_hash.delete('trace') + imported_object do |object| + object.trace = trace + object.commit_id = nil + end + else + imported_object + end + end + + def update_project_references + 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 relation_class.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'] + + # If source and target are the same, populate them with the new project ID. + if @relation_hash['source_project_id'] && @relation_hash['target_project_id'] && + @relation_hash['target_project_id'] == @relation_hash['source_project_id'] + @relation_hash['source_project_id'] = project_id + end + end + + def reset_ci_tokens + return unless Gitlab::ImportExport.reset_tokens? + + # If we import/export a project to the same instance, tokens will have to be reset. + @relation_hash['token'] = nil + end + + def relation_class + @relation_class ||= @relation_name.to_s.classify.constantize + end + + def imported_object + imported_object = relation_class.new(@relation_hash) + yield(imported_object) if block_given? + imported_object.importing = true if imported_object.respond_to?(:importing) + imported_object + end + + def update_note_for_missing_author(author_name) + @relation_hash['note'] = '*Blank note*' if @relation_hash['note'].blank? + @relation_hash['note'] += missing_author_note(@relation_hash['updated_at'], author_name) + end + + def admin_user? + @user.is_admin? + end + end + end +end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb new file mode 100644 index 0000000000..546dae4d12 --- /dev/null +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -0,0 +1,39 @@ +module Gitlab + module ImportExport + class RepoRestorer + include Gitlab::ImportExport::CommandLineUtil + + def initialize(project:, shared:, path_to_bundle:, wiki: false) + @project = project + @path_to_bundle = path_to_bundle + @shared = shared + @wiki = wiki + end + + def restore + return wiki? unless File.exist?(@path_to_bundle) + + FileUtils.mkdir_p(path_to_repo) + + git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) + rescue => e + @shared.error(e) + false + end + + private + + def repos_path + Gitlab.config.gitlab_shell.repos_path + end + + def path_to_repo + @project.repository.path_to_repo + end + + def wiki? + @wiki + end + end + end +end diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb new file mode 100644 index 0000000000..cce43fe994 --- /dev/null +++ b/lib/gitlab/import_export/repo_saver.rb @@ -0,0 +1,35 @@ +module Gitlab + module ImportExport + class RepoSaver + include Gitlab::ImportExport::CommandLineUtil + + attr_reader :full_path + + def initialize(project:, shared:) + @project = project + @shared = shared + end + + def save + return false if @project.empty_repo? + + @full_path = File.join(@shared.export_path, ImportExport.project_bundle_filename) + bundle_to_disk + end + + private + + def bundle_to_disk + FileUtils.mkdir_p(@shared.export_path) + git_bundle(repo_path: path_to_repo, bundle_path: @full_path) + rescue => e + @shared.error(e) + false + 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 new file mode 100644 index 0000000000..f38229c6c5 --- /dev/null +++ b/lib/gitlab/import_export/saver.rb @@ -0,0 +1,42 @@ +module Gitlab + module ImportExport + class Saver + include Gitlab::ImportExport::CommandLineUtil + + def self.save(*args) + new(*args).save + end + + def initialize(shared:) + @shared = shared + end + + def save + if compress_and_save + remove_export_path + Rails.logger.info("Saved project export #{archive_file}") + archive_file + else + false + end + rescue => e + @shared.error(e) + false + end + + private + + def compress_and_save + tar_czf(archive: archive_file, dir: @shared.export_path) + end + + def remove_export_path + FileUtils.rm_rf(@shared.export_path) + end + + def archive_file + @archive_file ||= File.join(@shared.export_path, '..', "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_project_export.tar.gz") + end + end + end +end diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb new file mode 100644 index 0000000000..6aff05b886 --- /dev/null +++ b/lib/gitlab/import_export/shared.rb @@ -0,0 +1,30 @@ +module Gitlab + module ImportExport + class Shared + + attr_reader :errors, :opts + + def initialize(opts) + @opts = opts + @errors = [] + end + + def export_path + @export_path ||= Gitlab::ImportExport.export_path(relative_path: opts[:relative_path]) + end + + def error(error) + error_out(error.message, caller[0].dup) + @errors << error.message + # Debug: + Rails.logger.error(error.backtrace) + end + + private + + def error_out(message, caller) + Rails.logger.error("Import/Export error raised on #{caller}: #{message}") + end + end + end +end diff --git a/lib/gitlab/import_export/uploads_restorer.rb b/lib/gitlab/import_export/uploads_restorer.rb new file mode 100644 index 0000000000..df19354b76 --- /dev/null +++ b/lib/gitlab/import_export/uploads_restorer.rb @@ -0,0 +1,14 @@ +module Gitlab + module ImportExport + class UploadsRestorer < UploadsSaver + def restore + return true unless File.directory?(uploads_export_path) + + copy_files(uploads_export_path, uploads_path) + rescue => e + @shared.error(e) + false + end + end + end +end diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb new file mode 100644 index 0000000000..7292e9d971 --- /dev/null +++ b/lib/gitlab/import_export/uploads_saver.rb @@ -0,0 +1,36 @@ +module Gitlab + module ImportExport + class UploadsSaver + + def initialize(project:, shared:) + @project = project + @shared = shared + end + + def save + return true unless File.directory?(uploads_path) + + copy_files(uploads_path, uploads_export_path) + rescue => e + @shared.error(e) + false + end + + private + + def copy_files(source, destination) + FileUtils.mkdir_p(destination) + FileUtils.copy_entry(source, destination) + true + end + + def uploads_export_path + File.join(@shared.export_path, 'uploads') + end + + def uploads_path + File.join(Rails.root.join('public/uploads'), @project.path_with_namespace) + end + end + end +end diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb new file mode 100644 index 0000000000..cf5c62c5e3 --- /dev/null +++ b/lib/gitlab/import_export/version_checker.rb @@ -0,0 +1,36 @@ +module Gitlab + module ImportExport + class VersionChecker + + def self.check!(*args) + new(*args).check! + end + + def initialize(shared:) + @shared = shared + end + + def check! + version = File.open(version_file, &:readline) + verify_version!(version) + rescue => e + @shared.error(e) + false + end + + private + + def version_file + File.join(@shared.export_path, Gitlab::ImportExport.version_filename) + end + + def verify_version!(version) + if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.version) + raise Gitlab::ImportExport::Error("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}") + else + true + end + end + end + end +end diff --git a/lib/gitlab/import_export/version_saver.rb b/lib/gitlab/import_export/version_saver.rb new file mode 100644 index 0000000000..f7f73dc934 --- /dev/null +++ b/lib/gitlab/import_export/version_saver.rb @@ -0,0 +1,25 @@ +module Gitlab + module ImportExport + class VersionSaver + + def initialize(shared:) + @shared = shared + end + + def save + FileUtils.mkdir_p(@shared.export_path) + + File.write(version_file, Gitlab::ImportExport.version, mode: 'w') + rescue => e + @shared.error(e) + false + end + + private + + def version_file + File.join(@shared.export_path, Gitlab::ImportExport.version_filename) + end + end + end +end diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb new file mode 100644 index 0000000000..1eedae39f8 --- /dev/null +++ b/lib/gitlab/import_export/wiki_repo_saver.rb @@ -0,0 +1,33 @@ +module Gitlab + module ImportExport + class WikiRepoSaver < RepoSaver + def save + @wiki = ProjectWiki.new(@project) + return true unless wiki_repository_exists? # it's okay to have no Wiki + bundle_to_disk(File.join(@shared.export_path, project_filename)) + end + + def bundle_to_disk(full_path) + FileUtils.mkdir_p(@shared.export_path) + git_bundle(repo_path: path_to_repo, bundle_path: full_path) + rescue => e + @shared.error(e) + false + end + + private + + def project_filename + "project.wiki.bundle" + end + + def path_to_repo + @wiki.repository.path_to_repo + end + + def wiki_repository_exists? + File.exist?(@wiki.repository.path_to_repo) && !@wiki.repository.empty? + end + end + end +end diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb index ccfdfbe73e..948d43582c 100644 --- a/lib/gitlab/import_sources.rb +++ b/lib/gitlab/import_sources.rb @@ -20,7 +20,8 @@ module Gitlab 'Gitorious.org' => 'gitorious', 'Google Code' => 'google_code', 'FogBugz' => 'fogbugz', - 'Any repo by URL' => 'git', + 'Repo by URL' => 'git', + 'GitLab export' => 'gitlab_project' } end diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb index 9d9617761b..e3ed2f6791 100644 --- a/lib/gitlab/lfs/response.rb +++ b/lib/gitlab/lfs/response.rb @@ -2,10 +2,11 @@ module Gitlab module Lfs class Response - def initialize(project, user, request) + def initialize(project, user, ci, request) @origin_project = project @project = storage_project(project) @user = user + @ci = ci @env = request.env @request = request end @@ -189,7 +190,7 @@ module Gitlab return render_not_enabled unless Gitlab.config.lfs.enabled unless @project.public? - return render_unauthorized unless @user + return render_unauthorized unless @user || @ci return render_forbidden unless user_can_fetch? end @@ -210,7 +211,7 @@ module Gitlab def user_can_fetch? # Check user access against the project they used to initiate the pull - @user.can?(:download_code, @origin_project) + @ci || @user.can?(:download_code, @origin_project) end def user_can_push? diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb index 78d0289110..69bd5e6230 100644 --- a/lib/gitlab/lfs/router.rb +++ b/lib/gitlab/lfs/router.rb @@ -1,9 +1,12 @@ module Gitlab module Lfs class Router - def initialize(project, user, request) + attr_reader :project, :user, :ci, :request + + def initialize(project, user, ci, request) @project = project @user = user + @ci = ci @env = request.env @request = request end @@ -80,7 +83,7 @@ module Gitlab def lfs return unless @project - Gitlab::Lfs::Response.new(@project, @user, @request) + Gitlab::Lfs::Response.new(@project, @user, @ci, @request) end def sanitize_tmp_filename(name) diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index d81d26754f..dcec7543c1 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -148,23 +148,8 @@ module Gitlab proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1 def #{name}(#{args_signature}) - trans = Gitlab::Metrics::Instrumentation.transaction - - if trans - start = Time.now - cpu_start = Gitlab::Metrics::System.cpu_time - retval = super - duration = (Time.now - start) * 1000.0 - - if duration >= Gitlab::Metrics.method_call_threshold - cpu_duration = Gitlab::Metrics::System.cpu_time - cpu_start - - trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, - { duration: duration, cpu_duration: cpu_duration }, - method: #{label.inspect}) - end - - retval + if trans = Gitlab::Metrics::Instrumentation.transaction + trans.measure_method(#{label.inspect}) { super } else super end diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb new file mode 100644 index 0000000000..faf0d9b631 --- /dev/null +++ b/lib/gitlab/metrics/method_call.rb @@ -0,0 +1,52 @@ +module Gitlab + module Metrics + # Class for tracking timing information about method calls + class MethodCall + attr_reader :real_time, :cpu_time, :call_count + + # name - The full name of the method (including namespace) such as + # `User#sign_in`. + # + # series - The series to use for storing the data. + def initialize(name, series) + @name = name + @series = series + @real_time = 0.0 + @cpu_time = 0.0 + @call_count = 0 + end + + # Measures the real and CPU execution time of the supplied block. + def measure + start_real = Time.now + start_cpu = System.cpu_time + retval = yield + + @real_time += (Time.now - start_real) * 1000.0 + @cpu_time += System.cpu_time.to_f - start_cpu + @call_count += 1 + + retval + end + + # Returns a Metric instance of the current method call. + def to_metric + Metric.new( + @series, + { + duration: real_time, + cpu_duration: cpu_time, + call_count: call_count + }, + method: @name + ) + end + + # Returns true if the total runtime of this method exceeds the method call + # threshold. + def above_threshold? + real_time >= Metrics.method_call_threshold + end + end + end +end diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb index 3fe27779d0..e61670f491 100644 --- a/lib/gitlab/metrics/rack_middleware.rb +++ b/lib/gitlab/metrics/rack_middleware.rb @@ -35,7 +35,7 @@ module Gitlab def transaction_from_env(env) trans = Transaction.new - trans.set(:request_uri, env['REQUEST_URI']) + trans.set(:request_uri, filtered_path(env)) trans.set(:request_method, env['REQUEST_METHOD']) trans @@ -54,6 +54,10 @@ module Gitlab private + def filtered_path(env) + ActionDispatch::Request.new(env).filtered_path.presence || env['REQUEST_URI'] + end + def endpoint_paths_cache @endpoint_paths_cache ||= Hash.new do |hash, http_method| hash[http_method] = Hash.new do |inner_hash, raw_path| diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index 2578ddc49f..4bc5081aa0 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -4,7 +4,7 @@ module Gitlab class Transaction THREAD_KEY = :_gitlab_metrics_transaction - attr_reader :tags, :values + attr_reader :tags, :values, :methods attr_accessor :action @@ -16,6 +16,7 @@ module Gitlab # plus method name. def initialize(action = nil) @metrics = [] + @methods = {} @started_at = nil @finished_at = nil @@ -51,9 +52,23 @@ module Gitlab end def add_metric(series, values, tags = {}) - prefix = sidekiq? ? 'sidekiq_' : 'rails_' + @metrics << Metric.new("#{series_prefix}#{series}", values, tags) + end - @metrics << Metric.new("#{prefix}#{series}", values, tags) + # Measures the time it takes to execute a method. + # + # Multiple calls to the same method add up to the total runtime of the + # method. + # + # name - The full name of the method to measure (e.g. `User#sign_in`). + def measure_method(name, &block) + unless @methods[name] + series = "#{series_prefix}#{Instrumentation::SERIES}" + + @methods[name] = MethodCall.new(name, series) + end + + @methods[name].measure(&block) end def increment(name, value) @@ -84,7 +99,13 @@ module Gitlab end def submit - metrics = @metrics.map do |metric| + submit = @metrics.dup + + @methods.each do |name, method| + submit << method.to_metric if method.above_threshold? + end + + submit_hashes = submit.map do |metric| hash = metric.to_hash hash[:tags][:action] ||= @action if @action @@ -92,12 +113,16 @@ module Gitlab hash end - Metrics.submit_metrics(metrics) + Metrics.submit_metrics(submit_hashes) end def sidekiq? Sidekiq.server? end + + def series_prefix + sidekiq? ? 'sidekiq_' : 'rails_' + end end end end diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb new file mode 100644 index 0000000000..760ff3e614 --- /dev/null +++ b/lib/gitlab/template/base_template.rb @@ -0,0 +1,67 @@ +module Gitlab + module Template + class BaseTemplate + def initialize(path) + @path = path + end + + def name + File.basename(@path, self.class.extension) + end + + def content + File.read(@path) + end + + class << self + def all + self.categories.keys.flat_map { |cat| by_category(cat) } + end + + def find(key) + file_name = "#{key}#{self.extension}" + + directory = select_directory(file_name) + directory ? new(File.join(category_directory(directory), file_name)) : nil + end + + def categories + raise NotImplementedError + end + + def extension + raise NotImplementedError + end + + def base_dir + raise NotImplementedError + end + + def by_category(category) + templates_for_directory(category_directory(category)) + end + + def category_directory(category) + File.join(base_dir, categories[category]) + end + + private + + def select_directory(file_name) + categories.keys.find do |category| + File.exist?(File.join(category_directory(category), file_name)) + end + end + + def templates_for_directory(dir) + dir << '/' unless dir.end_with?('/') + Dir.glob(File.join(dir, "*#{self.extension}")).select { |f| f =~ filter_regex }.map { |f| new(f) } + end + + def filter_regex + @filter_reges ||= /#{Regexp.escape(extension)}\z/ + end + end + end + end +end diff --git a/lib/gitlab/template/gitignore.rb b/lib/gitlab/template/gitignore.rb new file mode 100644 index 0000000000..964fbfd4de --- /dev/null +++ b/lib/gitlab/template/gitignore.rb @@ -0,0 +1,22 @@ +module Gitlab + module Template + class Gitignore < BaseTemplate + class << self + def extension + '.gitignore' + end + + def categories + { + "Languages" => '', + "Global" => 'Global' + } + end + + def base_dir + Rails.root.join('vendor/gitignore') + end + end + end + end +end diff --git a/lib/gitlab/template/gitlab_ci_yml.rb b/lib/gitlab/template/gitlab_ci_yml.rb new file mode 100644 index 0000000000..7f480fe33c --- /dev/null +++ b/lib/gitlab/template/gitlab_ci_yml.rb @@ -0,0 +1,27 @@ +module Gitlab + module Template + class GitlabCiYml < BaseTemplate + def content + explanation = "# This file is a template, and might need editing before it works on your project." + [explanation, super].join("\n") + end + + class << self + def extension + '.gitlab-ci.yml' + end + + def categories + { + "General" => '', + "Pages" => 'Pages' + } + end + + def base_dir + Rails.root.join('vendor/gitlab-ci-yml') + end + end + end + end +end diff --git a/lib/tasks/gitlab/update_gitignore.rake b/lib/tasks/gitlab/update_gitignore.rake deleted file mode 100644 index 4fd48cccb1..0000000000 --- a/lib/tasks/gitlab/update_gitignore.rake +++ /dev/null @@ -1,46 +0,0 @@ -namespace :gitlab do - desc "GitLab | Update gitignore" - task :update_gitignore do - unless clone_gitignores - puts "Cloning the gitignores failed".color(:red) - return - end - - remove_unneeded_files(gitignore_directory) - remove_unneeded_files(global_directory) - - puts "Done".color(:green) - end - - def clone_gitignores - FileUtils.rm_rf(gitignore_directory) if Dir.exist?(gitignore_directory) - FileUtils.cd vendor_directory - - system('git clone --depth=1 --branch=master https://github.com/github/gitignore.git') - end - - # Retain only certain files: - # - The LICENSE, because we have to - # - The sub dir global - # - The gitignores themself - # - Dir.entires returns also the entries '.' and '..' - def remove_unneeded_files(path) - Dir.foreach(path) do |file| - FileUtils.rm_rf(File.join(path, file)) unless file =~ /(\.{1,2}|LICENSE|Global|\.gitignore)\z/ - end - end - - private - - def vendor_directory - Rails.root.join('vendor') - end - - def gitignore_directory - File.join(vendor_directory, 'gitignore') - end - - def global_directory - File.join(gitignore_directory, 'Global') - end -end diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake new file mode 100644 index 0000000000..4f76dad728 --- /dev/null +++ b/lib/tasks/gitlab/update_templates.rake @@ -0,0 +1,54 @@ +namespace :gitlab do + desc "GitLab | Update templates" + task :update_templates do + TEMPLATE_DATA.each { |template| update(template) } + end + + def update(template) + sub_dir = template.repo_url.match(/([a-z-]+)\.git\z/)[1] + dir = File.join(vendor_directory, sub_dir) + + unless clone_repository(template.repo_url, dir) + puts "Cloning the #{sub_dir} templates failed".red + return + end + + remove_unneeded_files(dir, template.cleanup_regex) + puts "Done".green + end + + def clone_repository(url, directory) + FileUtils.rm_rf(directory) if Dir.exist?(directory) + + system("git clone #{url} --depth=1 --branch=master #{directory}") + end + + # Retain only certain files: + # - The LICENSE, because we have to + # - The sub dirs so we can organise the file by category + # - The templates themself + # - Dir.entries returns also the entries '.' and '..' + def remove_unneeded_files(directory, regex) + Dir.foreach(directory) do |file| + FileUtils.rm_rf(File.join(directory, file)) unless file =~ regex + end + end + + private + + Template = Struct.new(:repo_url, :cleanup_regex) + TEMPLATE_DATA = [ + Template.new( + "https://github.com/github/gitignore.git", + /(\.{1,2}|LICENSE|Global|\.gitignore)\z/ + ), + Template.new( + "https://gitlab.com/gitlab-org/gitlab-ci-yml.git", + /(\.{1,2}|LICENSE|Pages|\.gitlab-ci.yml)\z/ + ) + ] + + def vendor_directory + Rails.root.join('vendor') + end +end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 186239d309..ff5b391627 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -30,4 +30,75 @@ describe ApplicationController do controller.send(:check_password_expiration) end end + + describe "#authenticate_user_from_token!" do + describe "authenticating a user from a private token" do + controller(ApplicationController) do + def index + render text: "authenticated" + end + end + + let(:user) { create(:user) } + + context "when the 'private_token' param is populated with the private token" do + it "logs the user in" do + get :index, private_token: user.private_token + expect(response.status).to eq(200) + expect(response.body).to eq("authenticated") + end + end + + + context "when the 'PRIVATE-TOKEN' header is populated with the private token" do + it "logs the user in" do + @request.headers['PRIVATE-TOKEN'] = user.private_token + get :index + expect(response.status).to eq(200) + expect(response.body).to eq("authenticated") + end + end + + it "doesn't log the user in otherwise" do + @request.headers['PRIVATE-TOKEN'] = "token" + get :index, private_token: "token", authenticity_token: "token" + expect(response.status).not_to eq(200) + expect(response.body).not_to eq("authenticated") + end + end + + describe "authenticating a user from a personal access token" do + controller(ApplicationController) do + def index + render text: 'authenticated' + end + end + + let(:user) { create(:user) } + let(:personal_access_token) { create(:personal_access_token, user: user) } + + context "when the 'personal_access_token' param is populated with the personal access token" do + it "logs the user in" do + get :index, private_token: personal_access_token.token + expect(response.status).to eq(200) + expect(response.body).to eq('authenticated') + end + end + + context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do + it "logs the user in" do + @request.headers["PRIVATE-TOKEN"] = personal_access_token.token + get :index + expect(response.status).to eq(200) + expect(response.body).to eq('authenticated') + end + end + + it "doesn't log the user in otherwise" do + get :index, private_token: "token" + expect(response.status).not_to eq(200) + expect(response.body).not_to eq('authenticated') + end + end + end end diff --git a/spec/controllers/groups/notification_settings_controller_spec.rb b/spec/controllers/groups/notification_settings_controller_spec.rb deleted file mode 100644 index 0786e45515..0000000000 --- a/spec/controllers/groups/notification_settings_controller_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -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/notification_settings_controller_spec.rb b/spec/controllers/notification_settings_controller_spec.rb new file mode 100644 index 0000000000..15d155833b --- /dev/null +++ b/spec/controllers/notification_settings_controller_spec.rb @@ -0,0 +1,125 @@ +require 'spec_helper' + +describe NotificationSettingsController do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + project.team << [user, :developer] + end + + describe '#create' do + context 'when not authorized' do + it 'redirects to sign in page' do + post :create, + project: { id: project.id }, + 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 + post :create, + project: { id: project.id }, + notification_setting: { level: :participating } + + expect(response.status).to eq 200 + end + + context 'and setting custom notification setting' do + let(:custom_events) do + events = {} + + NotificationSetting::EMAIL_EVENTS.each do |event| + events[event] = "true" + end + end + + it 'returns success' do + post :create, + project: { id: project.id }, + notification_setting: { level: :participating, events: custom_events } + + expect(response.status).to eq 200 + end + end + end + + context 'not authorized' do + let(:private_project) { create(:project, :private) } + before { sign_in(user) } + + it 'returns 404' do + post :create, + project: { id: private_project.id }, + notification_setting: { level: :participating } + + expect(response.status).to eq(404) + end + end + end + + describe '#update' do + let(:notification_setting) { user.global_notification_setting } + + context 'when not authorized' do + it 'redirects to sign in page' do + put :update, + id: notification_setting, + notification_setting: { level: :participating } + + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when authorized' do + before{ sign_in(user) } + + it 'returns success' do + put :update, + id: notification_setting, + notification_setting: { level: :participating } + + expect(response.status).to eq 200 + end + + context 'and setting custom notification setting' do + let(:custom_events) do + events = {} + + NotificationSetting::EMAIL_EVENTS.each do |event| + events[event] = "true" + end + end + + it 'returns success' do + put :update, + id: notification_setting, + notification_setting: { level: :participating, events: custom_events } + + expect(response.status).to eq 200 + end + end + end + + context 'not authorized' do + let(:other_user) { create(:user) } + + before { sign_in(other_user) } + + it 'returns 404' do + put :update, + id: notification_setting, + notification_setting: { level: :participating } + + expect(response.status).to eq(404) + end + end + end +end diff --git a/spec/controllers/profiles/accounts_controller_spec.rb b/spec/controllers/profiles/accounts_controller_spec.rb new file mode 100644 index 0000000000..4eafc11aba --- /dev/null +++ b/spec/controllers/profiles/accounts_controller_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Profiles::AccountsController do + + let(:user) { create(:omniauth_user, provider: 'saml') } + + before do + sign_in(user) + end + + it 'does not allow to unlink SAML connected account' do + identity = user.identities.last + delete :unlink, provider: 'saml' + updated_user = User.find(user.id) + + expect(response.status).to eq(302) + expect(updated_user.identities.size).to eq(1) + expect(updated_user.identities).to include(identity) + end + + it 'does allow to delete other linked accounts' do + user.identities.create(provider: 'twitter', extern_uid: 'twitter_123') + + expect { delete :unlink, provider: 'twitter' }.to change(Identity.all, :size).by(-1) + end +end diff --git a/spec/controllers/projects/notification_settings_controller_spec.rb b/spec/controllers/projects/notification_settings_controller_spec.rb deleted file mode 100644 index c5d17d97ec..0000000000 --- a/spec/controllers/projects/notification_settings_controller_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -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 - - context 'not authorized' do - let(:private_project) { create(:project, :private) } - before { sign_in(user) } - - it 'returns 404' do - put :update, - namespace_id: private_project.namespace.to_param, - project_id: private_project.to_param, - notification_setting: { level: :participating } - - expect(response.status).to eq(404) - end - end - end -end diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb new file mode 100644 index 0000000000..40a3403b66 --- /dev/null +++ b/spec/controllers/projects/todo_controller_spec.rb @@ -0,0 +1,102 @@ +require('spec_helper') + +describe Projects::TodosController do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + let(:merge_request) { create(:merge_request, source_project: project) } + + context 'Issues' do + describe 'POST create' do + context 'when authorized' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it 'should create todo for issue' do + expect do + post(:create, namespace_id: project.namespace.path, + project_id: project.path, + issuable_id: issue.id, + issuable_type: 'issue') + end.to change { user.todos.count }.by(1) + + expect(response.status).to eq(200) + end + end + + context 'when not authorized' do + it 'should not create todo for issue that user has no access to' do + sign_in(user) + expect do + post(:create, namespace_id: project.namespace.path, + project_id: project.path, + issuable_id: issue.id, + issuable_type: 'issue') + end.to change { user.todos.count }.by(0) + + expect(response.status).to eq(404) + end + + it 'should not create todo for issue when user not logged in' do + expect do + post(:create, namespace_id: project.namespace.path, + project_id: project.path, + issuable_id: issue.id, + issuable_type: 'issue') + end.to change { user.todos.count }.by(0) + + expect(response.status).to eq(302) + end + end + end + end + + context 'Merge Requests' do + describe 'POST create' do + context 'when authorized' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it 'should create todo for merge request' do + expect do + post(:create, namespace_id: project.namespace.path, + project_id: project.path, + issuable_id: merge_request.id, + issuable_type: 'merge_request') + end.to change { user.todos.count }.by(1) + + expect(response.status).to eq(200) + end + end + + context 'when not authorized' do + it 'should not create todo for merge request user has no access to' do + sign_in(user) + expect do + post(:create, namespace_id: project.namespace.path, + project_id: project.path, + issuable_id: merge_request.id, + issuable_type: 'merge_request') + end.to change { user.todos.count }.by(0) + + expect(response.status).to eq(404) + end + + it 'should not create todo for merge request user has no access to' do + expect do + post(:create, namespace_id: project.namespace.path, + project_id: project.path, + issuable_id: merge_request.id, + issuable_type: 'merge_request') + end.to change { user.todos.count }.by(0) + + expect(response.status).to eq(302) + end + end + end + end +end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index fba545560c..146b2c2e13 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -237,4 +237,24 @@ describe ProjectsController do expect(response.status).to eq(401) end end + + describe "GET refs" do + it "should get a list of branches and tags" do + get :refs, namespace_id: public_project.namespace.path, id: public_project.path + + parsed_body = JSON.parse(response.body) + expect(parsed_body["Branches"]).to include("master") + expect(parsed_body["Tags"]).to include("v1.0.0") + expect(parsed_body["Commits"]).to be_nil + end + + it "should get a list of branches, tags and commits" do + get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456" + + parsed_body = JSON.parse(response.body) + expect(parsed_body["Branches"]).to include("master") + expect(parsed_body["Tags"]).to include("v1.0.0") + expect(parsed_body["Commits"]).to include("123456") + end + end end diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/pipelines.rb similarity index 100% rename from spec/factories/ci/commits.rb rename to spec/factories/ci/pipelines.rb diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb new file mode 100644 index 0000000000..da4c72bcb5 --- /dev/null +++ b/spec/factories/personal_access_tokens.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + factory :personal_access_token do + user + token { SecureRandom.hex(50) } + name { FFaker::Product.brand } + revoked false + expires_at { 5.days.from_now } + end +end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 9499cd4e02..2d297776cb 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -60,6 +60,40 @@ describe "Admin Runners" do it { expect(page).to have_content(@project1.name_with_namespace) } it { expect(page).not_to have_content(@project2.name_with_namespace) } end + + describe 'enable/create' do + before do + @project1.runners << runner + visit admin_runner_path(runner) + end + + it 'enables specific runner for project' do + within '.unassigned-projects' do + click_on 'Enable' + end + + assigned_project = page.find('.assigned-projects') + + expect(assigned_project).to have_content(@project2.path) + end + end + + describe 'disable/destroy' do + before do + @project1.runners << runner + visit admin_runner_path(runner) + end + + it 'enables specific runner for project' do + within '.assigned-projects' do + click_on 'Disable' + end + + new_runner_project = page.find('.unassigned-projects') + + expect(new_runner_project).to have_content(@project1.path) + end + end end describe 'runners registration token' do diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index 53b4f02711..203e55a36f 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -26,7 +26,8 @@ describe "Container Registry" do end context 'when there are tags' do - it { expect(page).to have_content(tag_name)} + it { expect(page).to have_content(tag_name) } + it { expect(page).to have_content('d7a513a66') } end end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 40fea5211e..7fb28f4174 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -20,7 +20,7 @@ feature 'Environments', feature: true do context 'without environments' do scenario 'does show no environments' do - expect(page).to have_content('No environments to show') + expect(page).to have_content('You don\'t have any environments right now.') end end @@ -61,7 +61,7 @@ feature 'Environments', feature: true do context 'without deployments' do scenario 'does show no deployments' do - expect(page).to have_content('No deployments for') + expect(page).to have_content('You don\'t have any deployments right now.') end end @@ -108,7 +108,7 @@ feature 'Environments', feature: true do end scenario 'does create a new pipeline' do - expect(page).to have_content('production') + expect(page).to have_content('Production') end end diff --git a/spec/features/issues/bulk_assigment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb similarity index 97% rename from spec/features/issues/bulk_assigment_labels_spec.rb rename to spec/features/issues/bulk_assignment_labels_spec.rb index 0fbc2062e3..7143d0e40f 100644 --- a/spec/features/issues/bulk_assigment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -190,7 +190,8 @@ feature 'Issues > Labels bulk assignment', feature: true do end if unmark items.map do |item| - click_link item + # Make sure we are unmarking the item no matter the state it has currently + click_link item until find('a', text: item)[:class] == 'label-item' end end end diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb index b69cce3e7d..bc0f437a8c 100644 --- a/spec/features/issues/todo_spec.rb +++ b/spec/features/issues/todo_spec.rb @@ -20,6 +20,12 @@ feature 'Manually create a todo item from issue', feature: true, js: true do page.within '.header-content .todos-pending-count' do expect(page).to have_content '1' end + + visit namespace_project_issue_path(project.namespace, project, issue) + + page.within '.header-content .todos-pending-count' do + expect(page).to have_content '1' + end end it 'should mark a todo as done' do @@ -29,5 +35,9 @@ feature 'Manually create a todo item from issue', feature: true, js: true do end expect(page).to have_selector('.todos-pending-count', visible: false) + + visit namespace_project_issue_path(project.namespace, project, issue) + + expect(page).to have_selector('.todos-pending-count', visible: false) end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 65fe918e2e..c3cb337944 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -556,10 +556,10 @@ describe 'Issues', feature: true do first('.ui-state-default').click end - expect(page).to have_no_content 'None' + expect(page).to have_no_content 'No due date' click_link 'remove due date' - expect(page).to have_content 'None' + expect(page).to have_content 'No due date' end end end diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb new file mode 100644 index 0000000000..a85930c754 --- /dev/null +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe 'Profile > Personal Access Tokens', feature: true, js: true do + let(:user) { create(:user) } + + def active_personal_access_tokens + find(".table.active-personal-access-tokens") + end + + def inactive_personal_access_tokens + find(".table.inactive-personal-access-tokens") + end + + def created_personal_access_token + find("#created-personal-access-token").value + end + + def disallow_personal_access_token_saves! + allow_any_instance_of(PersonalAccessToken).to receive(:save).and_return(false) + errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") } + allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors) + end + + before do + login_as(user) + end + + describe "token creation" do + it "allows creation of a token" do + visit profile_personal_access_tokens_path + fill_in "Name", with: FFaker::Product.brand + + expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1) + expect(created_personal_access_token).to eq(PersonalAccessToken.last.token) + expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name) + expect(active_personal_access_tokens).to have_text("Never") + end + + it "allows creation of a token with an expiry date" do + visit profile_personal_access_tokens_path + fill_in "Name", with: FFaker::Product.brand + + # Set date to 1st of next month + find_field("Expires at").trigger('focus') + find("a[title='Next']").click + click_on "1" + + expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1) + expect(created_personal_access_token).to eq(PersonalAccessToken.last.token) + expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name) + expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium)) + end + + context "when creation fails" do + it "displays an error message" do + disallow_personal_access_token_saves! + visit profile_personal_access_tokens_path + fill_in "Name", with: FFaker::Product.brand + + expect { click_on "Create Personal Access Token" }.not_to change { PersonalAccessToken.count } + expect(page).to have_content("Name cannot be nil") + end + end + end + + describe "inactive tokens" do + let!(:personal_access_token) { create(:personal_access_token, user: user) } + + it "allows revocation of an active token" do + visit profile_personal_access_tokens_path + click_on "Revoke" + + expect(inactive_personal_access_tokens).to have_text(personal_access_token.name) + end + + it "moves expired tokens to the 'inactive' section" do + personal_access_token.update(expires_at: 5.days.ago) + visit profile_personal_access_tokens_path + + expect(inactive_personal_access_tokens).to have_text(personal_access_token.name) + end + + context "when revocation fails" do + it "displays an error message" do + disallow_personal_access_token_saves! + visit profile_personal_access_tokens_path + + expect { click_on "Revoke" }.not_to change { PersonalAccessToken.inactive.count } + expect(active_personal_access_tokens).to have_text(personal_access_token.name) + expect(page).to have_content("Could not revoke") + end + end + end +end diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb index 51be81d634..01e90618a9 100644 --- a/spec/features/projects/badges/list_spec.rb +++ b/spec/features/projects/badges/list_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'list of badges' do - include Select2Helper - background do user = create(:user) project = create(:project) @@ -24,7 +22,11 @@ feature 'list of badges' do end scenario 'user changes current ref on badges list page', js: true do - select2('improve/awesome', from: '#ref') + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link 'improve/awesome' + end expect(page).to have_content 'badges/improve/awesome/build.svg' end diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb new file mode 100644 index 0000000000..d516e8ce55 --- /dev/null +++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +feature 'User wants to add a .gitlab-ci.yml file', feature: true do + include WaitForAjax + + before do + user = create(:user) + project = create(:project) + project.team << [user, :master] + login_as user + visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitlab-ci.yml') + end + + scenario 'user can see .gitlab-ci.yml dropdown' do + expect(page).to have_css('.gitlab-ci-yml-selector') + end + + scenario 'user can pick a template from the dropdown', js: true do + find('.js-gitlab-ci-yml-selector').click + wait_for_ajax + within '.gitlab-ci-yml-selector' do + find('.dropdown-input-field').set('jekyll') + find('.dropdown-content li', text: 'jekyll').click + end + wait_for_ajax + + expect(page).to have_content('This file is a template, and might need editing before it works on your project') + expect(page).to have_content('jekyll build -d test') + end +end diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index ecc818eb1e..e1e105e6bb 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' feature 'project owner creates a license file', feature: true, js: true do - include Select2Helper + include WaitForAjax let(:project_master) { create(:user) } let(:project) { create(:project) } @@ -21,7 +21,7 @@ feature 'project owner creates a license file', feature: true, js: true do expect(page).to have_selector('.license-selector') - select2('mit', from: '#license_type') + select_template('MIT License') file_content = find('.file-content') expect(file_content).to have_content('The MIT License (MIT)') @@ -44,7 +44,7 @@ feature 'project owner creates a license file', feature: true, js: true do expect(find('#file_name').value).to eq('LICENSE') expect(page).to have_selector('.license-selector') - select2('mit', from: '#license_type') + select_template('MIT License') file_content = find('.file-content') expect(file_content).to have_content('The MIT License (MIT)') @@ -58,4 +58,12 @@ feature 'project owner creates a license file', feature: true, js: true do expect(page).to have_content('The MIT License (MIT)') expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end + + def select_template(template) + page.within('.js-license-selector-wrap') do + click_button 'Choose a License template' + click_link template + wait_for_ajax + end + 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 index 34eda29c28..67aac25e42 100644 --- 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 @@ -1,7 +1,7 @@ require 'spec_helper' feature 'project owner sees a link to create a license file in empty project', feature: true, js: true do - include Select2Helper + include WaitForAjax let(:project_master) { create(:user) } let(:project) { create(:empty_project) } @@ -20,7 +20,7 @@ feature 'project owner sees a link to create a license file in empty project', f expect(find('#file_name').value).to eq('LICENSE') expect(page).to have_selector('.license-selector') - select2('mit', from: '#license_type') + select_template('MIT License') file_content = find('.file-content') expect(file_content).to have_content('The MIT License (MIT)') @@ -36,4 +36,12 @@ feature 'project owner sees a link to create a license file in empty project', f expect(page).to have_content('The MIT License (MIT)') expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end + + def select_template(template) + page.within('.js-license-selector-wrap') do + click_button 'Choose a License template' + click_link template + wait_for_ajax + end + end end diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb new file mode 100644 index 0000000000..c5fb0fc783 --- /dev/null +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +feature 'project import', feature: true, js: true do + include Select2Helper + + let(:user) { create(:admin) } + let!(:namespace) { create(:namespace, name: "asd", owner: user) } + let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } + let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } + let(:project) { Project.last } + + background do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + login_as(user) + end + + after(:each) do + FileUtils.rm_rf(export_path, secure: true) + end + + scenario 'user imports an exported project successfully' do + expect(Project.all.count).to be_zero + + visit new_project_path + + select2('2', from: '#project_namespace_id') + fill_in :project_path, with:'test-project-path', visible: true + click_link 'GitLab export' + + expect(page).to have_content('GitLab project export') + expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path') + + attach_file('file', file) + + click_on 'Import project' # import starts + + expect(project).not_to be_nil + expect(project.issues).not_to be_empty + expect(project.merge_requests).not_to be_empty + expect(project.repo_exists?).to be true + expect(wiki_exists?).to be true + expect(project.import_status).to eq('finished') + end + + def wiki_exists? + wiki = ProjectWiki.new(project) + File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty? + end +end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz new file mode 100644 index 0000000000..1fd04416d9 Binary files /dev/null and b/spec/features/projects/import_export/test_project_export.tar.gz differ diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index 8550d279d0..6a39c302f5 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -77,6 +77,7 @@ feature 'Prioritize labels', feature: true do end visit current_url + wait_for_ajax page.within('.prioritized-labels') do expect(first('li')).to have_content('wontfix') diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb new file mode 100644 index 0000000000..728c0e1636 --- /dev/null +++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +feature 'Projects > Members > Group member cannot leave group project', feature: true do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + + background do + group.add_developer(user) + login_as(user) + visit namespace_project_path(project.namespace, project) + end + + scenario 'user does not see a "Leave project" link' do + expect(page).not_to have_content 'Leave Project' + end +end diff --git a/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb new file mode 100644 index 0000000000..4d5d656f00 --- /dev/null +++ b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +feature 'Projects > Members > Group member cannot request access to his group project', feature: true do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + + background do + end + + scenario 'owner does not see the request access button' do + group.add_owner(user) + login_and_visit_project_page(user) + + expect(page).not_to have_content 'Request Access' + end + + scenario 'master does not see the request access button' do + group.add_master(user) + login_and_visit_project_page(user) + + expect(page).not_to have_content 'Request Access' + end + + scenario 'developer does not see the request access button' do + group.add_developer(user) + login_and_visit_project_page(user) + + expect(page).not_to have_content 'Request Access' + end + + scenario 'reporter does not see the request access button' do + group.add_reporter(user) + login_and_visit_project_page(user) + + expect(page).not_to have_content 'Request Access' + end + + scenario 'guest does not see the request access button' do + group.add_guest(user) + login_and_visit_project_page(user) + + expect(page).not_to have_content 'Request Access' + end + + def login_and_visit_project_page(user) + login_as(user) + visit namespace_project_path(project.namespace, project) + end +end diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb new file mode 100644 index 0000000000..c4ed92d278 --- /dev/null +++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'Projects > Members > Group requester cannot request access to project', feature: true do + let(:user) { create(:user) } + let(:owner) { create(:user) } + let(:group) { create(:group, :public) } + let(:project) { create(:project, :public, namespace: group) } + + background do + group.add_owner(owner) + login_as(user) + visit group_path(group) + perform_enqueued_jobs { click_link 'Request Access' } + visit namespace_project_path(project.namespace, project) + end + + scenario 'group requester does not see the request access / withdraw access request button' do + expect(page).not_to have_content 'Request Access' + expect(page).not_to have_content 'Withdraw Access Request' + end +end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 029a11ea43..b9e63a7152 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -47,4 +47,83 @@ describe "Search", feature: true do expect(page).to have_link(snippet.title) end end + + + describe 'Right header search field', feature: true do + + describe 'Search in project page' do + before do + visit namespace_project_path(project.namespace, project) + end + + it 'top right search form is present' do + expect(page).to have_selector('#search') + end + + it 'top right search form contains location badge' do + expect(page).to have_selector('.has-location-badge') + end + + context 'clicking the search field', js: true do + it 'should show category search dropdown' do + page.find('#search').click + + expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i) + end + end + + context 'click the links in the category search dropdown', js: true do + + before do + page.find('#search').click + end + + it 'should take user to her issues page when issues assigned is clicked' do + find('.dropdown-menu').click_link 'Issues assigned to me' + sleep 2 + + expect(page).to have_selector('.issues-holder') + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + + it 'should take user to her issues page when issues authored is clicked' do + find('.dropdown-menu').click_link "Issues I've created" + sleep 2 + + expect(page).to have_selector('.issues-holder') + expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name) + end + + it 'should take user to her MR page when MR assigned is clicked' do + find('.dropdown-menu').click_link 'Merge requests assigned to me' + sleep 2 + + expect(page).to have_selector('.merge-requests-holder') + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + + it 'should take user to her MR page when MR authored is clicked' do + find('.dropdown-menu').click_link "Merge requests I've created" + sleep 2 + + expect(page).to have_selector('.merge-requests-holder') + expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name) + end + end + + context 'entering text into the search field', js: true do + before do + page.within '.search-input-wrap' do + fill_in "search", with: project.name[0..3] + end + end + + it 'should not display the category search dropdown' do + expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i) + end + end + end + end + + end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 8e1833a069..0bdb1628c7 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -103,11 +103,15 @@ describe 'Dashboard Todos', feature: true do before do deleted_project = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC, pending_delete: true) create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author) + create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author, state: :done) login_as(user) visit dashboard_todos_path end it 'shows "All done" message' do + within('.todos-pending-count') { expect(page).to have_content '0' } + expect(page).to have_content 'To do 0' + expect(page).to have_content 'Done 0' expect(page).to have_content "You're all done!" end end diff --git a/spec/fixtures/container_registry/tag_manifest.json b/spec/fixtures/container_registry/tag_manifest.json index 1b6008e287..8d1b874c29 100644 --- a/spec/fixtures/container_registry/tag_manifest.json +++ b/spec/fixtures/container_registry/tag_manifest.json @@ -1 +1,16 @@ -{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/octet-stream","size":1145,"digest":"sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":2319870,"digest":"sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"}]} +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/octet-stream", + "size": 1145, + "digest": "sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 2319870, + "digest": "sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d" + } + ] +} diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index f6c1005d26..bb28866f01 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -174,51 +174,6 @@ describe ApplicationHelper do end end - describe 'grouped_options_refs' do - let(:options) { helper.grouped_options_refs } - let(:project) { create(:project) } - - before do - assign(:project, project) - - # Override Rails' grouped_options_for_select helper to just return the - # first argument (`options`), since it's easier to work with than the - # generated HTML. - allow(helper).to receive(:grouped_options_for_select). - and_wrap_original { |_, *args| args.first } - end - - it 'includes a list of branch names' do - expect(options[0][0]).to eq('Branches') - expect(options[0][1]).to include('master', 'feature') - end - - it 'includes a list of tag names' do - expect(options[1][0]).to eq('Tags') - expect(options[1][1]).to include('v1.0.0', 'v1.1.0') - end - - it 'includes a specific commit ref if defined' do - # Must be an instance variable - ref = '2ed06dc41dbb5936af845b87d79e05bbf24c73b8' - assign(:ref, ref) - - expect(options[2][0]).to eq('Commit') - expect(options[2][1]).to eq([ref]) - end - - it 'sorts tags in a natural order' do - # Stub repository.tag_names to make sure we get some valid testing data - expect(project.repository).to receive(:tag_names). - and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v2.0rc1¿', - 'v1.0.9a', 'v2.0-rc1', 'v2.0rc2']) - - expect(options[1][1]). - to eq(['v3.1.4.2', 'v2.0', 'v2.0rc2', 'v2.0rc1¿', 'v2.0-rc1', 'v1.0.10', - 'v1.0.9', 'v1.0.9a']) - end - end - describe 'simple_sanitize' do let(:a_tag) { 'Foo' } diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb index 0b1a76156e..f75fdb739f 100644 --- a/spec/helpers/members_helper_spec.rb +++ b/spec/helpers/members_helper_spec.rb @@ -9,20 +9,52 @@ describe MembersHelper do it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member } end - describe '#can_see_member_roles?' do - let(:project) { create(:empty_project) } - let(:group) { create(:group) } - let(:user) { build(:user) } - let(:admin) { build(:user, :admin) } - let(:project_member) { create(:project_member, project: project) } - let(:group_member) { create(:group_member, group: group) } + describe '#default_show_roles' do + let(:user) { double } + let(:member) { build(:project_member) } - it { expect(can_see_member_roles?(source: project, user: nil)).to be_falsy } - it { expect(can_see_member_roles?(source: group, user: nil)).to be_falsy } - it { expect(can_see_member_roles?(source: project, user: admin)).to be_truthy } - it { expect(can_see_member_roles?(source: group, user: admin)).to be_truthy } - it { expect(can_see_member_roles?(source: project, user: project_member.user)).to be_truthy } - it { expect(can_see_member_roles?(source: group, user: group_member.user)).to be_truthy } + before do + allow(helper).to receive(:current_user).and_return(user) + allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(false) + allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(false) + allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(false) + end + + context 'when the current cannot update, destroy or admin the passed member' do + it 'returns false' do + expect(helper.default_show_roles(member)).to be_falsy + end + end + + context 'when the current can update the passed member' do + before do + allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(true) + end + + it 'returns true' do + expect(helper.default_show_roles(member)).to be_truthy + end + end + + context 'when the current can destroy the passed member' do + before do + allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(true) + end + + it 'returns true' do + expect(helper.default_show_roles(member)).to be_truthy + end + end + + context 'when the current can admin the passed member source' do + before do + allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(true) + end + + it 'returns true' do + expect(helper.default_show_roles(member)).to be_truthy + end + end end describe '#remove_member_message' do diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index a3336c8717..903224589d 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -33,9 +33,9 @@ describe MergeRequestsHelper do let(:project) { create(:project) } let(:issues) do [ - JiraIssue.new('JIRA-123', project), - JiraIssue.new('JIRA-456', project), - JiraIssue.new('FOOBAR-7890', project) + ExternalIssue.new('JIRA-123', project), + ExternalIssue.new('JIRA-456', project), + ExternalIssue.new('FOOBAR-7890', project) ] end diff --git a/spec/javascripts/fixtures/search_autocomplete.html.haml b/spec/javascripts/fixtures/search_autocomplete.html.haml new file mode 100644 index 0000000000..7785120da5 --- /dev/null +++ b/spec/javascripts/fixtures/search_autocomplete.html.haml @@ -0,0 +1,10 @@ +.search.search-form.has-location-badge + %form.navbar-form + .search-input-container + %div.location-badge + This project + .search-input-wrap + .dropdown + %input#search.search-input.dropdown-menu-toggle + .dropdown-menu.dropdown-select + .dropdown-content diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee index dd160e821b..3a3c8d63e8 100644 --- a/spec/javascripts/notes_spec.js.coffee +++ b/spec/javascripts/notes_spec.js.coffee @@ -1,7 +1,7 @@ #= require notes #= require gl_form -window.gon = {} +window.gon or= {} window.disableButtonIfEmptyField = -> null describe 'Notes', -> diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee index 1cf34d4d2d..9be29097f4 100644 --- a/spec/javascripts/project_title_spec.js.coffee +++ b/spec/javascripts/project_title_spec.js.coffee @@ -6,7 +6,7 @@ #= require project_select #= require project -window.gon = {} +window.gon or= {} window.gon.api_version = 'v3' describe 'Project Title', -> diff --git a/spec/javascripts/search_autocomplete_spec.js.coffee b/spec/javascripts/search_autocomplete_spec.js.coffee new file mode 100644 index 0000000000..e77177783a --- /dev/null +++ b/spec/javascripts/search_autocomplete_spec.js.coffee @@ -0,0 +1,149 @@ +#= require gl_dropdown +#= require search_autocomplete +#= require jquery +#= require lib/common_utils +#= require lib/type_utility +#= require fuzzaldrin-plus + + +widget = null +userId = 1 +window.gon or= {} +window.gon.current_user_id = userId + +dashboardIssuesPath = '/dashboard/issues' +dashboardMRsPath = '/dashboard/merge_requests' +projectIssuesPath = '/gitlab-org/gitlab-ce/issues' +projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests' +groupIssuesPath = '/groups/gitlab-org/issues' +groupMRsPath = '/groups/gitlab-org/merge_requests' +projectName = 'GitLab Community Edition' +groupName = 'Gitlab Org' + + +# Add required attributes to body before starting the test. +# section would be dashboard|group|project +addBodyAttributes = (section = 'dashboard') -> + + $body = $ 'body' + + $body.removeAttr 'data-page' + $body.removeAttr 'data-project' + $body.removeAttr 'data-group' + + switch section + when 'dashboard' + $body.data 'page', 'root:index' + when 'group' + $body.data 'page', 'groups:show' + $body.data 'group', 'gitlab-org' + when 'project' + $body.data 'page', 'projects:show' + $body.data 'project', 'gitlab-ce' + + +# Mock `gl` object in window for dashboard specific page. App code will need it. +mockDashboardOptions = -> + + window.gl or= {} + window.gl.dashboardOptions = + issuesPath: dashboardIssuesPath + mrPath : dashboardMRsPath + + +# Mock `gl` object in window for project specific page. App code will need it. +mockProjectOptions = -> + + window.gl or= {} + window.gl.projectOptions = + 'gitlab-ce' : + issuesPath : projectIssuesPath + mrPath : projectMRsPath + projectName : projectName + + +mockGroupOptions = -> + + window.gl or= {} + window.gl.groupOptions = + 'gitlab-org' : + issuesPath : groupIssuesPath + mrPath : groupMRsPath + projectName : groupName + + +assertLinks = (list, issuesPath, mrsPath) -> + + issuesAssignedToMeLink = "#{issuesPath}/?assignee_id=#{userId}" + issuesIHaveCreatedLink = "#{issuesPath}/?author_id=#{userId}" + mrsAssignedToMeLink = "#{mrsPath}/?assignee_id=#{userId}" + mrsIHaveCreatedLink = "#{mrsPath}/?author_id=#{userId}" + + a1 = "a[href='#{issuesAssignedToMeLink}']" + a2 = "a[href='#{issuesIHaveCreatedLink}']" + a3 = "a[href='#{mrsAssignedToMeLink}']" + a4 = "a[href='#{mrsIHaveCreatedLink}']" + + expect(list.find(a1).length).toBe 1 + expect(list.find(a1).text()).toBe ' Issues assigned to me ' + + expect(list.find(a2).length).toBe 1 + expect(list.find(a2).text()).toBe " Issues I've created " + + expect(list.find(a3).length).toBe 1 + expect(list.find(a3).text()).toBe ' Merge requests assigned to me ' + + expect(list.find(a4).length).toBe 1 + expect(list.find(a4).text()).toBe " Merge requests I've created " + + +describe 'Search autocomplete dropdown', -> + + fixture.preload 'search_autocomplete.html' + + beforeEach -> + + fixture.load 'search_autocomplete.html' + widget = new SearchAutocomplete + + + it 'should show Dashboard specific dropdown menu', -> + + addBodyAttributes() + mockDashboardOptions() + widget.searchInput.focus() + + list = widget.wrap.find('.dropdown-menu').find 'ul' + assertLinks list, dashboardIssuesPath, dashboardMRsPath + + + it 'should show Group specific dropdown menu', -> + + addBodyAttributes 'group' + mockGroupOptions() + widget.searchInput.focus() + + list = widget.wrap.find('.dropdown-menu').find 'ul' + assertLinks list, groupIssuesPath, groupMRsPath + + + it 'should show Project specific dropdown menu', -> + + addBodyAttributes 'project' + mockProjectOptions() + widget.searchInput.focus() + + list = widget.wrap.find('.dropdown-menu').find 'ul' + assertLinks list, projectIssuesPath, projectMRsPath + + + it 'should not show category related menu if there is text in the input', -> + + addBodyAttributes 'project' + mockProjectOptions() + widget.searchInput.val 'help' + widget.searchInput.focus() + + list = widget.wrap.find('.dropdown-menu').find 'ul' + link = "a[href='#{projectIssuesPath}/?assignee_id=#{userId}']" + expect(list.find(link).length).toBe 0 diff --git a/spec/lib/banzai/filter/abstract_link_filter_spec.rb b/spec/lib/banzai/filter/abstract_link_filter_spec.rb new file mode 100644 index 0000000000..1ee31a603e --- /dev/null +++ b/spec/lib/banzai/filter/abstract_link_filter_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Banzai::Filter::AbstractReferenceFilter do + let(:project) { create(:empty_project) } + + describe '#references_per_project' do + it 'returns a Hash containing references grouped per project paths' do + doc = Nokogiri::HTML.fragment("#1 #{project.to_reference}#2") + filter = described_class.new(doc, project: project) + + expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue) + expect(filter).to receive(:object_sym).twice.and_return(:issue) + + refs = filter.references_per_project + + expect(refs).to be_an_instance_of(Hash) + expect(refs[project.to_reference]).to eq(Set.new(%w[1 2])) + end + end + + describe '#projects_per_reference' do + it 'returns a Hash containing projects grouped per project paths' do + doc = Nokogiri::HTML.fragment('') + filter = described_class.new(doc, project: project) + + expect(filter).to receive(:references_per_project). + and_return({ project.path_with_namespace => Set.new(%w[1]) }) + + expect(filter.projects_per_reference). + to eq({ project.path_with_namespace => project }) + end + end + + describe '#find_projects_for_paths' do + it 'returns a list of Projects for a list of paths' do + doc = Nokogiri::HTML.fragment('') + filter = described_class.new(doc, project: project) + + expect(filter.find_projects_for_paths([project.path_with_namespace])). + to eq([project]) + end + end + + describe '#current_project_path' do + it 'returns the path of the current project' do + doc = Nokogiri::HTML.fragment('') + filter = described_class.new(doc, project: project) + + expect(filter.current_project_path).to eq(project.path_with_namespace) + end + end +end diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb index f4c5c621bd..695a5bc6fd 100644 --- a/spec/lib/banzai/filter/external_link_filter_spec.rb +++ b/spec/lib/banzai/filter/external_link_filter_spec.rb @@ -19,19 +19,31 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do expect(filter(act).to_html).to eq exp end - it 'adds rel="nofollow" to external links' do - act = %q(Google) - doc = filter(act) + context 'for root links on document' do + let(:doc) { filter %q(Google) } - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to include 'nofollow' + it 'adds rel="nofollow" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'nofollow' + end + + it 'adds rel="noreferrer" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'noreferrer' + end end - it 'adds rel="noreferrer" to external links' do - act = %q(Google) - doc = filter(act) + context 'for nested links on document' do + let(:doc) { filter %q() } - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to include 'noreferrer' + it 'adds rel="nofollow" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'nofollow' + end + + it 'adds rel="noreferrer" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'noreferrer' + end end end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 8e6a264970..5b63c94611 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -25,7 +25,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do let(:reference) { issue.to_reference } it 'ignores valid references when using non-default tracker' do - expect(project).to receive(:get_issue).with(issue.iid).and_return(nil) + expect_any_instance_of(described_class).to receive(:find_object). + with(project, issue.iid). + and_return(nil) exp = act = "Issue #{reference}" expect(reference_filter(act).to_html).to eq exp @@ -107,8 +109,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do let(:reference) { issue.to_reference(project) } it 'ignores valid references when cross-reference project uses external tracker' do - expect_any_instance_of(Project).to receive(:get_issue). - with(issue.iid).and_return(nil) + expect_any_instance_of(described_class).to receive(:find_object). + with(project2, issue.iid). + and_return(nil) exp = act = "Issue #{reference}" expect(reference_filter(act).to_html).to eq exp @@ -131,6 +134,12 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end + + it 'ignores out-of-bounds issue IDs on the referenced project' do + exp = act = "Fixed ##{Gitlab::Database::MAX_INT_VALUE + 1}" + + expect(reference_filter(act).to_html).to eq exp + end end context 'cross-project URL reference' do diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index 3185e41fe5..805acf1c8b 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -38,6 +38,12 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end + it 'ignores out-of-bounds merge request IDs on the referenced project' do + exp = act = "Merge !#{Gitlab::Database::MAX_INT_VALUE + 1}" + + expect(reference_filter(act).to_html).to eq exp + end + it 'includes a title attribute' do doc = reference_filter("Merge #{reference}") expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}" diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 0e6685f0ff..b9e4a4eaf0 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -132,11 +132,8 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do path = 'files/images/한글.png' escaped = Addressable::URI.escape(path) - # Stub these methods so the file doesn't actually need to be in the repo - allow_any_instance_of(described_class). - to receive(:file_exists?).and_return(true) - allow_any_instance_of(described_class). - to receive(:image?).with(path).and_return(true) + # Stub this method so the file doesn't actually need to be in the repo + allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw) doc = filter(image(escaped)) expect(doc.at_css('img')['src']).to match '/raw/' diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb index b83be54746..273d2ed709 100644 --- a/spec/lib/banzai/filter/upload_link_filter_spec.rb +++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb @@ -23,6 +23,14 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do %(#{path}) end + def nested_image(path) + %() + end + + def nested_link(path) + %() + end + let(:project) { create(:project) } shared_examples :preserve_unchanged do @@ -47,11 +55,19 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) expect(doc.at_css('a')['href']). to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + + doc = filter(nested_link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('a')['href']). + to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" end it 'rebuilds relative URL for an image' do - doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('a')['href']). + doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('img')['src']). + to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + + doc = filter(nested_image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('img')['src']). to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 143e2e6d23..d562d8b25e 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -157,6 +157,35 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1) expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1) end + + context 'for invalid value' do + let(:config) { { rspec: { script: "rspec", type: "test", only: only } } } + let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } + + shared_examples 'raises an error' do + it do + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: only parameter should be an array of strings or regexps') + end + end + + context 'when it is integer' do + let(:only) { 1 } + + it_behaves_like 'raises an error' + end + + context 'when it is an array of integers' do + let(:only) { [1, 1] } + + it_behaves_like 'raises an error' + end + + context 'when it is invalid regex' do + let(:only) { ["/*invalid/"] } + + it_behaves_like 'raises an error' + end + end end describe :except do @@ -284,16 +313,44 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0) expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0) end - end + context 'for invalid value' do + let(:config) { { rspec: { script: "rspec", except: except } } } + let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } + + shared_examples 'raises an error' do + it do + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: except parameter should be an array of strings or regexps') + end + end + + context 'when it is integer' do + let(:except) { 1 } + + it_behaves_like 'raises an error' + end + + context 'when it is an array of integers' do + let(:except) { [1, 1] } + + it_behaves_like 'raises an error' + end + + context 'when it is invalid regex' do + let(:except) { ["/*invalid/"] } + + it_behaves_like 'raises an error' + end + end + 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 @@ -302,12 +359,12 @@ module Ci 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 { @@ -465,19 +522,41 @@ module Ci end context 'when syntax is incorrect' do - it 'raises error' do - variables = [:KEY1, 'value1', :KEY2, 'value2'] + context 'when variables defined but invalid' do + it 'raises error' do + variables = [:KEY1, 'value1', :KEY2, 'value2'] - config = YAML.dump( - { before_script: ['pwd'], - rspec: { - variables: variables, - script: 'rspec' } - }) + 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/) + expect { GitlabCiYamlProcessor.new(config, path) } + .to raise_error(GitlabCiYamlProcessor::ValidationError, + /job: variables should be a map/) + end + end + + context 'when variables key defined but value not specified' do + it 'returns empty array' do + config = YAML.dump( + { before_script: ['pwd'], + rspec: { + variables: nil, + script: 'rspec' } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + ## + # TODO, in next version of CI configuration processor this + # should be invalid configuration, see #18775 and #15060 + # + expect(config_processor.job_variables(:rspec)) + .to be_an_instance_of(Array).and be_empty + end end end end diff --git a/spec/lib/container_registry/repository_spec.rb b/spec/lib/container_registry/repository_spec.rb index 279709521c..c364e75910 100644 --- a/spec/lib/container_registry/repository_spec.rb +++ b/spec/lib/container_registry/repository_spec.rb @@ -21,7 +21,7 @@ describe ContainerRegistry::Repository do to_return( status: 200, body: JSON.dump(tags: ['test']), - headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' }) + headers: { 'Content-Type' => 'application/json' }) end context '#manifest' do diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 1ec539066a..9096ad101b 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -71,6 +71,18 @@ describe Gitlab::Database::MigrationHelpers, lib: true do expect(Project.where(archived: true).count).to eq(5) end + + context 'when a block is supplied' do + it 'yields an Arel table and query object to the supplied block' do + first_id = Project.first.id + + model.update_column_in_batches(:projects, :archived, true) do |t, query| + query.where(t[:id].eq(first_id)) + end + + expect(Project.where(archived: true).count).to eq(1) + end + end end describe '#add_column_with_default' do @@ -78,7 +90,7 @@ describe Gitlab::Database::MigrationHelpers, lib: true do before do expect(model).to receive(:transaction_open?).and_return(false) - expect(model).to receive(:transaction).twice.and_yield + expect(model).to receive(:transaction).and_yield expect(model).to receive(:add_column). with(:projects, :foo, :integer, default: nil) 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 0000000000..f135a285df --- /dev/null +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::MembersMapper, services: true do + describe 'map members' 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 + described_class.new( + exported_members: exported_members, user: user, project: project) + 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 + + it 'updates missing author IDs on missing project member' do + members_mapper.map[-1] + + expect(members_mapper.missing_author_ids.first).to eq(-1) + end + end +end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json new file mode 100644 index 0000000000..400d44ac16 --- /dev/null +++ b/spec/lib/gitlab/import_export/project.json @@ -0,0 +1,5341 @@ +{ + "name": "Gitlab Test", + "path": "gitlab-test", + "description": "Aut saepe in eos dolorem aliquam hic.", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "visibility_level": 20, + "archived": false, + "issues": [ + { + "id": 40, + "title": "Voluptatem modi rerum ipsum vero voluptas repudiandae veniam quibusdam.", + "assignee_id": 1, + "author_id": 4, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.411Z", + "updated_at": "2016-04-12T13:08:26.029Z", + "position": 0, + "branch_name": null, + "description": "Aut minima non sit qui nulla rerum laborum.", + "milestone_id": 10, + "state": "opened", + "iid": 10, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 1357, + "note": "test", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-04-12T13:08:26.006Z", + "updated_at": "2016-04-12T13:08:26.006Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": "", + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 338, + "note": "Fugit in aliquid voluptas dolor.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:19:59.213Z", + "updated_at": "2016-03-22T15:19:59.213Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 337, + "note": "Occaecati consequatur facilis doloribus omnis hic placeat nihil.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:19:59.186Z", + "updated_at": "2016-03-22T15:19:59.186Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 336, + "note": "Nostrum et et est repudiandae non dolores voluptatem.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:19:59.156Z", + "updated_at": "2016-03-22T15:19:59.156Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 335, + "note": "Nihil et aut dolorum aut sit maxime.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:19:59.130Z", + "updated_at": "2016-03-22T15:19:59.130Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 334, + "note": "Non blanditiis voluptatem sit earum accusantium distinctio voluptas officiis.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:19:59.101Z", + "updated_at": "2016-03-22T15:19:59.101Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 333, + "note": "Nesciunt non dolorem similique nam ipsa et.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:19:59.075Z", + "updated_at": "2016-03-22T15:19:59.075Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 332, + "note": "Sed aut fugit et officiis dolor.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:19:59.047Z", + "updated_at": "2016-03-22T15:19:59.047Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 331, + "note": "Officiis iste eum recusandae suscipit consequatur consequatur.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:19:59.015Z", + "updated_at": "2016-03-22T15:19:59.015Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 39, + "title": "Sit ut adipisci sint temporibus velit quis.", + "assignee_id": 1, + "author_id": 12, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.278Z", + "updated_at": "2016-03-22T15:19:59.473Z", + "position": 0, + "branch_name": null, + "description": "Ab sint nostrum aliquam laudantium magni recusandae qui.", + "milestone_id": 10, + "state": "closed", + "iid": 9, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 346, + "note": "Natus rerum qui dolorem dolorum voluptas.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:19:59.469Z", + "updated_at": "2016-03-22T15:19:59.469Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 345, + "note": "Voluptatibus et qui quis id sed necessitatibus quos.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:19:59.438Z", + "updated_at": "2016-03-22T15:19:59.438Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 344, + "note": "Aperiam possimus ipsam quibusdam in.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:19:59.410Z", + "updated_at": "2016-03-22T15:19:59.410Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 343, + "note": "Ad vel hic molestiae tempora.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:19:59.379Z", + "updated_at": "2016-03-22T15:19:59.379Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 342, + "note": "Vel magnam sed quidem aut molestiae facilis alias.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:19:59.348Z", + "updated_at": "2016-03-22T15:19:59.348Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 341, + "note": "Veritatis dolorum aut qui quod.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:19:59.319Z", + "updated_at": "2016-03-22T15:19:59.319Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 340, + "note": "Illum at cumque dolorum et quia.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:19:59.289Z", + "updated_at": "2016-03-22T15:19:59.289Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 339, + "note": "Fugiat et error molestiae cumque quos aperiam.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:19:59.255Z", + "updated_at": "2016-03-22T15:19:59.255Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 38, + "title": "Quod quo est quis vel natus nulla eos reiciendis.", + "assignee_id": 12, + "author_id": 3, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.137Z", + "updated_at": "2016-03-22T15:19:59.712Z", + "position": 0, + "branch_name": null, + "description": "Fugit dolor accusantium suscipit facere voluptate.", + "milestone_id": 10, + "state": "opened", + "iid": 8, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 354, + "note": "Id commodi natus vel corrupti ea placeat cum nihil.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:19:59.708Z", + "updated_at": "2016-03-22T15:19:59.708Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 353, + "note": "Quia hic sed ratione eos voluptate dolor occaecati dolorem.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:19:59.680Z", + "updated_at": "2016-03-22T15:19:59.680Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 352, + "note": "Commodi sint voluptatem est aut.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:19:59.650Z", + "updated_at": "2016-03-22T15:19:59.650Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 351, + "note": "Et quibusdam voluptatibus dolores aut quam architecto optio.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:19:59.622Z", + "updated_at": "2016-03-22T15:19:59.622Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 350, + "note": "Fugit natus explicabo sed pariatur et quasi autem.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:19:59.590Z", + "updated_at": "2016-03-22T15:19:59.590Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 349, + "note": "Corporis commodi eos quia optio sunt corrupti.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:19:59.562Z", + "updated_at": "2016-03-22T15:19:59.562Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 348, + "note": "Occaecati nostrum hic dolor tenetur aliquid maxime animi.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:19:59.536Z", + "updated_at": "2016-03-22T15:19:59.536Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 347, + "note": "Inventore ullam sed repellendus laudantium itaque et quia.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:19:59.506Z", + "updated_at": "2016-03-22T15:19:59.506Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 37, + "title": "Animi suscipit quia ut hic asperiores perferendis nisi ut.", + "assignee_id": 22, + "author_id": 10, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.994Z", + "updated_at": "2016-03-22T15:19:59.972Z", + "position": 0, + "branch_name": null, + "description": "Non quibusdam in maxime earum eveniet itaque culpa.", + "milestone_id": 11, + "state": "closed", + "iid": 7, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 362, + "note": "Quia qui quis molestiae in praesentium.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:19:59.966Z", + "updated_at": "2016-03-22T15:19:59.966Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 361, + "note": "Maxime sed eius qui consequatur beatae.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:19:59.924Z", + "updated_at": "2016-03-22T15:19:59.924Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 360, + "note": "Voluptatum quasi corrupti eveniet sed ut quis quibusdam.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:19:59.897Z", + "updated_at": "2016-03-22T15:19:59.897Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 359, + "note": "Molestias quia eius ipsum non.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:19:59.866Z", + "updated_at": "2016-03-22T15:19:59.866Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 358, + "note": "Aut non est accusantium aliquam.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:19:59.834Z", + "updated_at": "2016-03-22T15:19:59.834Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 357, + "note": "Aspernatur voluptas id voluptas vel cum ipsam.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:19:59.805Z", + "updated_at": "2016-03-22T15:19:59.805Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 356, + "note": "Harum dignissimos provident tempora sit numquam est qui.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:19:59.773Z", + "updated_at": "2016-03-22T15:19:59.773Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 355, + "note": "Sint dignissimos molestiae recusandae delectus.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:19:59.746Z", + "updated_at": "2016-03-22T15:19:59.746Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 36, + "title": "Quia dolores commodi eligendi ut nemo totam.", + "assignee_id": 3, + "author_id": 4, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.814Z", + "updated_at": "2016-03-22T15:20:00.371Z", + "position": 0, + "branch_name": null, + "description": "Molestiae veniam laudantium autem et natus.", + "milestone_id": 11, + "state": "opened", + "iid": 6, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 370, + "note": "Occaecati temporibus tempore harum vero incidunt veniam iste.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:20:00.365Z", + "updated_at": "2016-03-22T15:20:00.365Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 369, + "note": "Modi architecto officiis quia iste voluptas libero nihil quo.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:20:00.331Z", + "updated_at": "2016-03-22T15:20:00.331Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 368, + "note": "Eaque est tenetur ex est molestiae nobis.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:20:00.296Z", + "updated_at": "2016-03-22T15:20:00.296Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 367, + "note": "Odit enim ut a quo qui.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:20:00.261Z", + "updated_at": "2016-03-22T15:20:00.261Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 366, + "note": "Omnis unde cum officiis est.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:20:00.223Z", + "updated_at": "2016-03-22T15:20:00.223Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 365, + "note": "Ab consequuntur aliquam illo voluptatum.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:20:00.178Z", + "updated_at": "2016-03-22T15:20:00.178Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 364, + "note": "Molestiae dolorem est eos dolores aut.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:20:00.127Z", + "updated_at": "2016-03-22T15:20:00.127Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 363, + "note": "Nemo velit nam quod veniam.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:20:00.083Z", + "updated_at": "2016-03-22T15:20:00.083Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 35, + "title": "Rerum tenetur harum molestiae quam aut praesentium quaerat doloremque.", + "assignee_id": 4, + "author_id": 1, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.660Z", + "updated_at": "2016-03-22T15:20:00.665Z", + "position": 0, + "branch_name": null, + "description": "Omnis et voluptatibus expedita qui et explicabo rem ut.", + "milestone_id": 11, + "state": "opened", + "iid": 5, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 378, + "note": "Molestiae atque exercitationem culpa harum nemo.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:20:00.660Z", + "updated_at": "2016-03-22T15:20:00.660Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 377, + "note": "Porro sed nobis neque amet velit velit.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:20:00.625Z", + "updated_at": "2016-03-22T15:20:00.625Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 376, + "note": "Dicta officiis doloremque voluptatum qui omnis.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:20:00.589Z", + "updated_at": "2016-03-22T15:20:00.589Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 375, + "note": "Incidunt rerum omnis cum laudantium aut impedit.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:20:00.553Z", + "updated_at": "2016-03-22T15:20:00.553Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 374, + "note": "Et suscipit omnis dolorum officia vero.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:20:00.517Z", + "updated_at": "2016-03-22T15:20:00.517Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 373, + "note": "Doloremque adipisci et cumque inventore beatae consectetur.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:20:00.485Z", + "updated_at": "2016-03-22T15:20:00.485Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 372, + "note": "Dolores sapiente ea dolorum et quae adipisci id.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:20:00.455Z", + "updated_at": "2016-03-22T15:20:00.455Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 371, + "note": "Accusantium repellat tenetur natus dicta ullam saepe facere.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:20:00.420Z", + "updated_at": "2016-03-22T15:20:00.420Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 34, + "title": "Enim occaecati aut sed quia mollitia eligendi atque dolores voluptatem.", + "assignee_id": 24, + "author_id": 1, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.506Z", + "updated_at": "2016-03-22T15:20:00.961Z", + "position": 0, + "branch_name": null, + "description": "Voluptatem totam magnam fugit assumenda consequatur illo qui.", + "milestone_id": 10, + "state": "opened", + "iid": 4, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 379, + "note": "Praesentium odio quia fugit consequuntur repudiandae ducimus.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:20:00.717Z", + "updated_at": "2016-03-22T15:20:00.717Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + }, + { + "id": 380, + "note": "Dolores aut dolorem quia soluta incidunt commodi quia.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:20:00.754Z", + "updated_at": "2016-03-22T15:20:00.754Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 381, + "note": "Enim et velit iure ad.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:20:00.787Z", + "updated_at": "2016-03-22T15:20:00.787Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 382, + "note": "Impedit nobis quis laudantium ad assumenda.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:20:00.822Z", + "updated_at": "2016-03-22T15:20:00.822Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 383, + "note": "Facere sed numquam quos quas.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:20:00.855Z", + "updated_at": "2016-03-22T15:20:00.855Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 384, + "note": "Ex voluptatem sit provident error.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:20:00.889Z", + "updated_at": "2016-03-22T15:20:00.889Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 385, + "note": "Soluta laboriosam recusandae est cupiditate.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:20:00.925Z", + "updated_at": "2016-03-22T15:20:00.925Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 386, + "note": "Similique dolorem rerum iusto animi perferendis aut inventore.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:20:00.957Z", + "updated_at": "2016-03-22T15:20:00.957Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + } + ] + }, + { + "id": 33, + "title": "Rem fugiat fugit occaecati quibusdam enim consectetur numquam.", + "assignee_id": 22, + "author_id": 22, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.364Z", + "updated_at": "2016-03-22T15:20:01.227Z", + "position": 0, + "branch_name": null, + "description": "Provident nulla architecto neque beatae fuga alias repudiandae.", + "milestone_id": 10, + "state": "closed", + "iid": 3, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 394, + "note": "Suscipit numquam voluptatibus ipsam libero dolorum dolore totam.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:20:01.223Z", + "updated_at": "2016-03-22T15:20:01.223Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 393, + "note": "Et et sed sit sint.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:20:01.194Z", + "updated_at": "2016-03-22T15:20:01.194Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 392, + "note": "Corrupti perferendis voluptas et iure omnis officia.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:20:01.160Z", + "updated_at": "2016-03-22T15:20:01.160Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 391, + "note": "Autem quo fugit in iste nesciunt tempora.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:20:01.131Z", + "updated_at": "2016-03-22T15:20:01.131Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 390, + "note": "Magni porro ut soluta quis et eveniet maiores.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:20:01.101Z", + "updated_at": "2016-03-22T15:20:01.101Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 389, + "note": "Sed consequuntur debitis nisi veniam exercitationem recusandae a quisquam.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:20:01.070Z", + "updated_at": "2016-03-22T15:20:01.070Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 388, + "note": "Aut impedit qui consectetur dicta temporibus.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:20:01.042Z", + "updated_at": "2016-03-22T15:20:01.042Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 387, + "note": "Officia repudiandae ut culpa ipsa reiciendis.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:20:01.005Z", + "updated_at": "2016-03-22T15:20:01.005Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 32, + "title": "Velit nihil est alias blanditiis eius earum autem hic.", + "assignee_id": 22, + "author_id": 26, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.225Z", + "updated_at": "2016-03-22T15:20:01.495Z", + "position": 0, + "branch_name": null, + "description": "Id voluptas ut sint aut laborum nobis commodi.", + "milestone_id": 11, + "state": "opened", + "iid": 2, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 402, + "note": "Magni ut eligendi sit sint recusandae voluptas tempore necessitatibus.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:20:01.489Z", + "updated_at": "2016-03-22T15:20:01.489Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 401, + "note": "Est repellat commodi incidunt tempore earum optio unde sint.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:20:01.455Z", + "updated_at": "2016-03-22T15:20:01.455Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 400, + "note": "Vero unde debitis tempore est laboriosam ut esse.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:20:01.421Z", + "updated_at": "2016-03-22T15:20:01.421Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 399, + "note": "Omnis qui asperiores expedita harum voluptatem eius.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:20:01.391Z", + "updated_at": "2016-03-22T15:20:01.391Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 398, + "note": "Dolorem doloribus delectus quo ratione esse veritatis.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:20:01.358Z", + "updated_at": "2016-03-22T15:20:01.358Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 397, + "note": "Quia esse et odit id est omnis dolorum quia.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:20:01.329Z", + "updated_at": "2016-03-22T15:20:01.329Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 396, + "note": "Exercitationem suscipit non rerum tempore sit.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:20:01.297Z", + "updated_at": "2016-03-22T15:20:01.297Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 395, + "note": "Nihil veniam magni sit officiis.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:20:01.268Z", + "updated_at": "2016-03-22T15:20:01.268Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 31, + "title": "Asperiores recusandae praesentium voluptas pariatur provident qui exercitationem quis.", + "assignee_id": 26, + "author_id": 24, + "project_id": 5, + "created_at": "2016-03-22T15:13:26.889Z", + "updated_at": "2016-03-22T15:20:01.834Z", + "position": 0, + "branch_name": null, + "description": "Ex voluptates qui excepturi cupiditate.", + "milestone_id": 11, + "state": "closed", + "iid": 1, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 410, + "note": "Sit itaque non nihil nisi qui voluptatem dolorem error.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:20:01.828Z", + "updated_at": "2016-03-22T15:20:01.828Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 409, + "note": "Omnis rem nihil molestiae enim laudantium doloremque.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:20:01.783Z", + "updated_at": "2016-03-22T15:20:01.783Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 408, + "note": "Ullam harum sit et optio incidunt.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:20:01.746Z", + "updated_at": "2016-03-22T15:20:01.746Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 407, + "note": "Fugit distinctio ab quo ipsam.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:20:01.716Z", + "updated_at": "2016-03-22T15:20:01.716Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 406, + "note": "Impedit iste possimus ad ea.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:20:01.676Z", + "updated_at": "2016-03-22T15:20:01.676Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 405, + "note": "Nemo recusandae dolore distinctio quam consequuntur ut et aut.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:20:01.641Z", + "updated_at": "2016-03-22T15:20:01.641Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 404, + "note": "Nisi repudiandae repellat nulla culpa quasi expedita quod velit.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:20:01.601Z", + "updated_at": "2016-03-22T15:20:01.601Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 403, + "note": "Quibusdam odio temporibus nemo voluptatibus accusamus.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:20:01.552Z", + "updated_at": "2016-03-22T15:20:01.552Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ] + } + ], + "labels": [ + { + "id": 12, + "title": "test", + "color": "#428bca", + "project_id": 5, + "created_at": "2016-05-10T10:53:14.214Z", + "updated_at": "2016-05-10T10:53:14.214Z", + "template": false, + "description": "test label" + } + ], + "milestones": [ + { + "id": 11, + "title": "v2.0", + "project_id": 5, + "description": "Sapiente facilis architecto reprehenderit aut sed enim.", + "due_date": null, + "created_at": "2016-03-22T15:13:21.631Z", + "updated_at": "2016-03-22T15:13:21.631Z", + "state": "closed", + "iid": 2 + }, + { + "id": 10, + "title": "v1.0", + "project_id": 5, + "description": "Est sed eos minima veniam culpa aut non.", + "due_date": null, + "created_at": "2016-03-22T15:13:21.622Z", + "updated_at": "2016-03-22T15:13:21.622Z", + "state": "closed", + "iid": 1 + } + ], + "snippets": [ + + ], + "releases": [ + + ], + "events": [ + { + "id": 301, + "target_type": "Note", + "target_id": 1357, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-04-12T13:08:30.886Z", + "updated_at": "2016-04-12T13:08:30.886Z", + "action": 6, + "author_id": 1 + }, + { + "id": 227, + "target_type": "MergeRequest", + "target_id": 85, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:19:44.957Z", + "updated_at": "2016-03-22T15:19:44.957Z", + "action": 1, + "author_id": 1 + }, + { + "id": 226, + "target_type": "MergeRequest", + "target_id": 84, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:19:44.600Z", + "updated_at": "2016-03-22T15:19:44.600Z", + "action": 1, + "author_id": 1 + }, + { + "id": 157, + "target_type": "MergeRequest", + "target_id": 15, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:45.936Z", + "updated_at": "2016-03-22T15:13:45.936Z", + "action": 1, + "author_id": 3 + }, + { + "id": 156, + "target_type": "MergeRequest", + "target_id": 14, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:45.500Z", + "updated_at": "2016-03-22T15:13:45.500Z", + "action": 1, + "author_id": 10 + }, + { + "id": 155, + "target_type": "MergeRequest", + "target_id": 13, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:45.242Z", + "updated_at": "2016-03-22T15:13:45.242Z", + "action": 1, + "author_id": 1 + }, + { + "id": 154, + "target_type": "MergeRequest", + "target_id": 12, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:44.940Z", + "updated_at": "2016-03-22T15:13:44.940Z", + "action": 1, + "author_id": 24 + }, + { + "id": 153, + "target_type": "MergeRequest", + "target_id": 11, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:44.568Z", + "updated_at": "2016-03-22T15:13:44.568Z", + "action": 1, + "author_id": 26 + }, + { + "id": 152, + "target_type": "MergeRequest", + "target_id": 10, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:44.225Z", + "updated_at": "2016-03-22T15:13:44.225Z", + "action": 1, + "author_id": 22 + }, + { + "id": 151, + "target_type": "MergeRequest", + "target_id": 9, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:43.868Z", + "updated_at": "2016-03-22T15:13:43.868Z", + "action": 1, + "author_id": 24 + }, + { + "id": 102, + "target_type": "Issue", + "target_id": 40, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.474Z", + "updated_at": "2016-03-22T15:13:28.474Z", + "action": 1, + "author_id": 4 + }, + { + "id": 101, + "target_type": "Issue", + "target_id": 39, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.328Z", + "updated_at": "2016-03-22T15:13:28.328Z", + "action": 1, + "author_id": 12 + }, + { + "id": 100, + "target_type": "Issue", + "target_id": 38, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.204Z", + "updated_at": "2016-03-22T15:13:28.204Z", + "action": 1, + "author_id": 3 + }, + { + "id": 99, + "target_type": "Issue", + "target_id": 37, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.055Z", + "updated_at": "2016-03-22T15:13:28.055Z", + "action": 1, + "author_id": 10 + }, + { + "id": 98, + "target_type": "Issue", + "target_id": 36, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.913Z", + "updated_at": "2016-03-22T15:13:27.913Z", + "action": 1, + "author_id": 4 + }, + { + "id": 97, + "target_type": "Issue", + "target_id": 35, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.731Z", + "updated_at": "2016-03-22T15:13:27.731Z", + "action": 1, + "author_id": 1 + }, + { + "id": 96, + "target_type": "Issue", + "target_id": 34, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.564Z", + "updated_at": "2016-03-22T15:13:27.564Z", + "action": 1, + "author_id": 1 + }, + { + "id": 95, + "target_type": "Issue", + "target_id": 33, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.429Z", + "updated_at": "2016-03-22T15:13:27.429Z", + "action": 1, + "author_id": 22 + }, + { + "id": 94, + "target_type": "Issue", + "target_id": 32, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.287Z", + "updated_at": "2016-03-22T15:13:27.287Z", + "action": 1, + "author_id": 26 + }, + { + "id": 93, + "target_type": "Issue", + "target_id": 31, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:26.997Z", + "updated_at": "2016-03-22T15:13:26.997Z", + "action": 1, + "author_id": 24 + }, + { + "id": 51, + "target_type": "Milestone", + "target_id": 11, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:21.634Z", + "updated_at": "2016-03-22T15:13:21.634Z", + "action": 1, + "author_id": 26 + }, + { + "id": 50, + "target_type": "Milestone", + "target_id": 10, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:21.625Z", + "updated_at": "2016-03-22T15:13:21.625Z", + "action": 1, + "author_id": 22 + }, + { + "id": 24, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:20.750Z", + "updated_at": "2016-03-22T15:13:20.750Z", + "action": 8, + "author_id": 12 + }, + { + "id": 23, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:20.711Z", + "updated_at": "2016-03-22T15:13:20.711Z", + "action": 8, + "author_id": 22 + }, + { + "id": 22, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:20.667Z", + "updated_at": "2016-03-22T15:13:20.667Z", + "action": 8, + "author_id": 26 + }, + { + "id": 21, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:20.646Z", + "updated_at": "2016-03-22T15:13:20.646Z", + "action": 8, + "author_id": 1 + }, + { + "id": 5, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:10.369Z", + "updated_at": "2016-03-22T15:13:10.369Z", + "action": 1, + "author_id": 1 + } + ], + "project_members": [ + { + "id": 35, + "access_level": 40, + "source_id": 5, + "source_type": "Project", + "user_id": 12, + "notification_level": 3, + "created_at": "2016-03-22T15:13:20.743Z", + "updated_at": "2016-03-22T15:13:20.743Z", + "created_by_id": null, + "invite_email": null, + "invite_token": null, + "invite_accepted_at": null, + "user": { + "id": 12, + "email": "maureen.bogisich@russelkessler.com", + "username": "evans" + } + }, + { + "id": 34, + "access_level": 40, + "source_id": 5, + "source_type": "Project", + "user_id": 22, + "notification_level": 3, + "created_at": "2016-03-22T15:13:20.708Z", + "updated_at": "2016-03-22T15:13:20.708Z", + "created_by_id": null, + "invite_email": null, + "invite_token": null, + "invite_accepted_at": null, + "user": { + "id": 22, + "email": "user0@example.com", + "username": "user0" + } + }, + { + "id": 33, + "access_level": 40, + "source_id": 5, + "source_type": "Project", + "user_id": 26, + "notification_level": 3, + "created_at": "2016-03-22T15:13:20.664Z", + "updated_at": "2016-03-22T15:13:20.664Z", + "created_by_id": null, + "invite_email": null, + "invite_token": null, + "invite_accepted_at": null, + "user": { + "id": 26, + "email": "user4@example.com", + "username": "user4" + } + }, + { + "id": 32, + "access_level": 20, + "source_id": 5, + "source_type": "Project", + "user_id": 1, + "notification_level": 3, + "created_at": "2016-03-22T15:13:20.643Z", + "updated_at": "2016-03-22T15:13:20.643Z", + "created_by_id": null, + "invite_email": null, + "invite_token": null, + "invite_accepted_at": null, + "user": { + "id": 1, + "email": "nospam@bluegod.net", + "username": "root" + } + } + ], + "merge_requests": [ + { + "id": 85, + "target_branch": "feature", + "source_branch": "feature_conflict", + "source_project_id": 5, + "author_id": 1, + "assignee_id": null, + "title": "Cannot be automatically merged", + "created_at": "2016-03-22T15:19:44.807Z", + "updated_at": "2016-03-22T15:20:09.557Z", + "milestone_id": null, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 9, + "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, + "notes": [ + { + "id": 638, + "note": "Ab velit ducimus totam sunt ut.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:09.553Z", + "updated_at": "2016-03-22T15:20:09.553Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 637, + "note": "Ipsum aliquam est in unde similique nihil illo ea.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:09.528Z", + "updated_at": "2016-03-22T15:20:09.528Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 636, + "note": "Soluta inventore adipisci et consequatur expedita aliquid earum modi.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:09.496Z", + "updated_at": "2016-03-22T15:20:09.496Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 635, + "note": "Corporis incidunt tempore est deleniti.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:09.469Z", + "updated_at": "2016-03-22T15:20:09.469Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 634, + "note": "Hic dolores voluptatibus qui necessitatibus.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:09.440Z", + "updated_at": "2016-03-22T15:20:09.440Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 633, + "note": "Rerum architecto placeat doloribus voluptates consequuntur quo.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:09.412Z", + "updated_at": "2016-03-22T15:20:09.412Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 632, + "note": "Vel earum aut ut occaecati aut ut rerum qui.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:09.389Z", + "updated_at": "2016-03-22T15:20:09.389Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 631, + "note": "Est voluptatibus dolores animi numquam.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:09.361Z", + "updated_at": "2016-03-22T15:20:09.361Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 85, + "state": "collected", + "st_commits": [ + { + "id": "bb5206fee213d983da88c47f9cf4cc6caf9c66dc", + "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "parent_ids": [ + "5937ac0a7beb003549fc5fd26fc247adbce4a52e" + ], + "authored_date": "2014-08-06T08:35:52.000+02:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-08-06T08:35:52.000+02:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\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 \u003cdmitriy.zaporozhets@gmail.com\u003e\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 \u003cdmitriy.zaporozhets@gmail.com\u003e\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 \u003cdmitriy.zaporozhets@gmail.com\u003e\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 \u003cdmitriy.zaporozhets@gmail.com\u003e\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": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n", + "new_path": "files/ruby/feature.rb", + "old_path": "files/ruby/feature.rb", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "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\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e 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 \u003c\u003c stdout.read\n @cmd_output \u003c\u003c 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": 85, + "created_at": "2016-03-22T15:19:44.810Z", + "updated_at": "2016-03-22T15:19:44.901Z", + "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", + "real_size": "9" + } + }, + { + "id": 84, + "target_branch": "master", + "source_branch": "feature", + "source_project_id": 5, + "author_id": 1, + "assignee_id": null, + "title": "Can be automatically merged", + "created_at": "2016-03-22T15:19:44.482Z", + "updated_at": "2016-03-22T15:20:09.773Z", + "milestone_id": null, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 8, + "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, + "notes": [ + { + "id": 646, + "note": "Temporibus debitis veniam est ut sit nihil.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:09.770Z", + "updated_at": "2016-03-22T15:20:09.770Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 645, + "note": "Ut assumenda dignissimos quibusdam veritatis sequi dolores.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:09.740Z", + "updated_at": "2016-03-22T15:20:09.740Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 644, + "note": "Velit quae quidem cupiditate laudantium nihil ut eveniet.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:09.717Z", + "updated_at": "2016-03-22T15:20:09.717Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 643, + "note": "Repellat quas porro sed mollitia laborum ut fugiat.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:09.690Z", + "updated_at": "2016-03-22T15:20:09.690Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 642, + "note": "Qui aut debitis perspiciatis et voluptatem.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:09.665Z", + "updated_at": "2016-03-22T15:20:09.665Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 641, + "note": "Quia id quia velit et.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:09.639Z", + "updated_at": "2016-03-22T15:20:09.639Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 640, + "note": "Corporis commodi doloremque itaque non animi.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:09.617Z", + "updated_at": "2016-03-22T15:20:09.617Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 639, + "note": "Possimus dignissimos voluptatum in tenetur.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:09.589Z", + "updated_at": "2016-03-22T15:20:09.589Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 84, + "state": "collected", + "st_commits": [ + { + "id": "0b4bc9a49b562e85de7cc9e834518ea6828729b9", + "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "parent_ids": [ + "ae73cb07c9eeaf35924a10f713b364d32b2dd34f" + ], + "authored_date": "2014-02-27T09:26:01.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:26:01.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n", + "new_path": "files/ruby/feature.rb", + "old_path": "files/ruby/feature.rb", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 84, + "created_at": "2016-03-22T15:19:44.485Z", + "updated_at": "2016-03-22T15:19:44.577Z", + "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", + "real_size": "1" + } + }, + { + "id": 15, + "target_branch": "markdown", + "source_branch": "master", + "source_project_id": 5, + "author_id": 3, + "assignee_id": 3, + "title": "Nulla explicabo iure voluptas perferendis autem autem unde nemo totam optio.", + "created_at": "2016-03-22T15:13:45.689Z", + "updated_at": "2016-03-22T15:20:30.476Z", + "milestone_id": 10, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 7, + "description": "Doloribus dignissimos impedit qui et provident exercitationem. Veniam quis magni qui fugiat. Et quia voluptate et vel consequatur pariatur ea est.", + "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, + "notes": [ + { + "id": 1231, + "note": "Rerum optio quibusdam provident possimus quis cum.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:30.472Z", + "updated_at": "2016-03-22T15:20:30.472Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 1230, + "note": "Quasi odit repudiandae ut officiis ut nihil illo.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:30.444Z", + "updated_at": "2016-03-22T15:20:30.444Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1229, + "note": "Aut vero dolores facere sed.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:30.412Z", + "updated_at": "2016-03-22T15:20:30.412Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1228, + "note": "Autem voluptatem et blanditiis accusantium deserunt et et.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:30.383Z", + "updated_at": "2016-03-22T15:20:30.383Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1227, + "note": "Voluptatem aliquam voluptatem molestiae est.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:30.352Z", + "updated_at": "2016-03-22T15:20:30.352Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1226, + "note": "Ea aut cupiditate est consequatur animi error qui et.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:30.319Z", + "updated_at": "2016-03-22T15:20:30.319Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 1225, + "note": "Voluptates est voluptas et nostrum modi beatae inventore et.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:30.289Z", + "updated_at": "2016-03-22T15:20:30.289Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 1224, + "note": "Quia est rerum adipisci cupiditate.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:30.260Z", + "updated_at": "2016-03-22T15:20:30.260Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 15, + "state": "collected", + "st_commits": [ + { + "id": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6", + "parent_ids": [ + "5f923865dde3436854e9ceb9cdb7815618d4e849", + "048721d90c449b244b7b4c53a9186b04330174ec" + ], + "authored_date": "2015-12-07T12:52:12.000+01:00", + "author_name": "Marin Jankovski", + "author_email": "marin@gitlab.com", + "committed_date": "2015-12-07T12:52:12.000+01:00", + "committer_name": "Marin Jankovski", + "committer_email": "marin@gitlab.com" + }, + { + "id": "048721d90c449b244b7b4c53a9186b04330174ec", + "message": "LFS object pointer.\n", + "parent_ids": [ + "5f923865dde3436854e9ceb9cdb7815618d4e849" + ], + "authored_date": "2015-12-07T11:54:28.000+01:00", + "author_name": "Marin Jankovski", + "author_email": "maxlazio@gmail.com", + "committed_date": "2015-12-07T11:54:28.000+01:00", + "committer_name": "Marin Jankovski", + "committer_email": "maxlazio@gmail.com" + }, + { + "id": "5f923865dde3436854e9ceb9cdb7815618d4e849", + "message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n", + "parent_ids": [ + "d2d430676773caa88cdaf7c55944073b2fd5561a" + ], + "authored_date": "2015-11-13T16:27:12.000+01:00", + "author_name": "Stan Hu", + "author_email": "stanhu@gmail.com", + "committed_date": "2015-11-13T16:27:12.000+01:00", + "committer_name": "Stan Hu", + "committer_email": "stanhu@gmail.com" + }, + { + "id": "d2d430676773caa88cdaf7c55944073b2fd5561a", + "message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5", + "parent_ids": [ + "59e29889be61e6e0e5e223bfa9ac2721d31605b8", + "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73" + ], + "authored_date": "2015-11-13T08:50:17.000+01:00", + "author_name": "Stan Hu", + "author_email": "stanhu@gmail.com", + "committed_date": "2015-11-13T08:50:17.000+01:00", + "committer_name": "Stan Hu", + "committer_email": "stanhu@gmail.com" + }, + { + "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73", + "message": "Add GitLab SVG\n", + "parent_ids": [ + "59e29889be61e6e0e5e223bfa9ac2721d31605b8" + ], + "authored_date": "2015-11-13T08:39:43.000+01:00", + "author_name": "Stan Hu", + "author_email": "stanhu@gmail.com", + "committed_date": "2015-11-13T08:39:43.000+01:00", + "committer_name": "Stan Hu", + "committer_email": "stanhu@gmail.com" + }, + { + "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8", + "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4", + "parent_ids": [ + "19e2e9b4ef76b422ce1154af39a91323ccc57434", + "66eceea0db202bb39c4e445e8ca28689645366c5" + ], + "authored_date": "2015-11-13T07:21:40.000+01:00", + "author_name": "Stan Hu", + "author_email": "stanhu@gmail.com", + "committed_date": "2015-11-13T07:21:40.000+01:00", + "committer_name": "Stan Hu", + "committer_email": "stanhu@gmail.com" + }, + { + "id": "66eceea0db202bb39c4e445e8ca28689645366c5", + "message": "add spaces in whitespace file\n", + "parent_ids": [ + "08f22f255f082689c0d7d39d19205085311542bc" + ], + "authored_date": "2015-11-13T06:01:27.000+01:00", + "author_name": "윤민식", + "author_email": "minsik.yoon@samsung.com", + "committed_date": "2015-11-13T06:01:27.000+01:00", + "committer_name": "윤민식", + "committer_email": "minsik.yoon@samsung.com" + }, + { + "id": "08f22f255f082689c0d7d39d19205085311542bc", + "message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n", + "parent_ids": [ + "c642fe9b8b9f28f9225d7ea953fe14e74748d53b" + ], + "authored_date": "2015-11-13T06:00:16.000+01:00", + "author_name": "윤민식", + "author_email": "minsik.yoon@samsung.com", + "committed_date": "2015-11-13T06:00:16.000+01:00", + "committer_name": "윤민식", + "committer_email": "minsik.yoon@samsung.com" + }, + { + "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434", + "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3", + "parent_ids": [ + "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd", + "c642fe9b8b9f28f9225d7ea953fe14e74748d53b" + ], + "authored_date": "2015-11-13T05:23:14.000+01:00", + "author_name": "Stan Hu", + "author_email": "stanhu@gmail.com", + "committed_date": "2015-11-13T05:23:14.000+01:00", + "committer_name": "Stan Hu", + "committer_email": "stanhu@gmail.com" + }, + { + "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b", + "message": "add whitespace in empty\n", + "parent_ids": [ + "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0" + ], + "authored_date": "2015-11-13T05:08:45.000+01:00", + "author_name": "윤민식", + "author_email": "minsik.yoon@samsung.com", + "committed_date": "2015-11-13T05:08:45.000+01:00", + "committer_name": "윤민식", + "committer_email": "minsik.yoon@samsung.com" + }, + { + "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0", + "message": "add empty file\n", + "parent_ids": [ + "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd" + ], + "authored_date": "2015-11-13T05:08:04.000+01:00", + "author_name": "윤민식", + "author_email": "minsik.yoon@samsung.com", + "committed_date": "2015-11-13T05:08:04.000+01:00", + "committer_name": "윤민식", + "committer_email": "minsik.yoon@samsung.com" + }, + { + "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd", + "message": "Add ISO-8859 test file\n", + "parent_ids": [ + "e56497bb5f03a90a51293fc6d516788730953899" + ], + "authored_date": "2015-08-25T17:53:12.000+02:00", + "author_name": "Stan Hu", + "author_email": "stanhu@packetzoom.com", + "committed_date": "2015-08-25T17:53:12.000+02:00", + "committer_name": "Stan Hu", + "committer_email": "stanhu@packetzoom.com" + }, + { + "id": "e56497bb5f03a90a51293fc6d516788730953899", + "message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)\n\nSee merge request !2\n", + "parent_ids": [ + "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "4cd80ccab63c82b4bad16faa5193fbd2aa06df40" + ], + "authored_date": "2015-01-10T22:23:29.000+01:00", + "author_name": "Sytse Sijbrandij", + "author_email": "sytse@gitlab.com", + "committed_date": "2015-01-10T22:23:29.000+01:00", + "committer_name": "Sytse Sijbrandij", + "committer_email": "sytse@gitlab.com" + }, + { + "id": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40", + "message": "add directory structure for tree_helper spec\n", + "parent_ids": [ + "5937ac0a7beb003549fc5fd26fc247adbce4a52e" + ], + "authored_date": "2015-01-10T21:28:18.000+01:00", + "author_name": "marmis85", + "author_email": "marmis85@gmail.com", + "committed_date": "2015-01-10T21:28:18.000+01:00", + "committer_name": "marmis85", + "committer_email": "marmis85@gmail.com" + } + ], + "st_diffs": [ + { + "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n", + "new_path": "CHANGELOG", + "old_path": "CHANGELOG", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n", + "new_path": "encoding/iso8859.txt", + "old_path": "encoding/iso8859.txt", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n", + "new_path": "files/images/wm.svg", + "old_path": "files/images/wm.svg", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n", + "new_path": "files/lfs/lfs_object.iso", + "old_path": "files/lfs/lfs_object.iso", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n", + "new_path": "files/whitespace", + "old_path": "files/whitespace", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n", + "new_path": "foo/bar/.gitkeep", + "old_path": "foo/bar/.gitkeep", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 15, + "created_at": "2016-03-22T15:13:45.692Z", + "updated_at": "2016-03-22T15:13:45.808Z", + "base_commit_sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "real_size": "6" + } + }, + { + "id": 14, + "target_branch": "test-1", + "source_branch": "test-10", + "source_project_id": 5, + "author_id": 10, + "assignee_id": 1, + "title": "Tempore aliquid sit amet odit qui cum iusto voluptatibus asperiores.", + "created_at": "2016-03-22T15:13:45.442Z", + "updated_at": "2016-03-22T15:20:30.735Z", + "milestone_id": 10, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 6, + "description": "Quis et et autem saepe ut. Eum corporis tempore cum dolore. Molestiae pariatur voluptatem officia perferendis aut veniam.", + "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, + "notes": [ + { + "id": 1239, + "note": "Aspernatur suscipit veritatis aliquid rerum.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:30.731Z", + "updated_at": "2016-03-22T15:20:30.731Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 1238, + "note": "Rerum deleniti omnis porro commodi.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:30.701Z", + "updated_at": "2016-03-22T15:20:30.701Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1237, + "note": "Eaque ut magnam rerum non dolores esse.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:30.667Z", + "updated_at": "2016-03-22T15:20:30.667Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1236, + "note": "Fugit et aut similique illum ut natus maiores et.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:30.637Z", + "updated_at": "2016-03-22T15:20:30.637Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1235, + "note": "Qui qui temporibus eos aliquam.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:30.608Z", + "updated_at": "2016-03-22T15:20:30.608Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1234, + "note": "Voluptates hic dolorum aut inventore.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:30.575Z", + "updated_at": "2016-03-22T15:20:30.575Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 1233, + "note": "Dolorum iure at dolor dolores numquam iusto.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:30.548Z", + "updated_at": "2016-03-22T15:20:30.548Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 1232, + "note": "Nihil est eum aspernatur amet minus et corporis consectetur.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:30.517Z", + "updated_at": "2016-03-22T15:20:30.517Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 14, + "state": "collected", + "st_commits": [ + { + "id": "bce96ecee98f51fa5d91021e6c42859a35a701ad", + "message": "fixes #10\n", + "parent_ids": [ + "be93687618e4b132087f430a4d8fc3a609c9b77c" + ], + "authored_date": "2016-01-19T15:40:05.000+01:00", + "author_name": "Test Lopez", + "author_email": "Test@Testlopez.es", + "committed_date": "2016-01-19T15:40:05.000+01:00", + "committer_name": "Test Lopez", + "committer_email": "Test@Testlopez.es" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/test\n", + "new_path": "test", + "old_path": "test", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 14, + "created_at": "2016-03-22T15:13:45.444Z", + "updated_at": "2016-03-22T15:13:45.486Z", + "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "real_size": "1" + } + }, + { + "id": 13, + "target_branch": "test-11", + "source_branch": "test-12", + "source_project_id": 5, + "author_id": 1, + "assignee_id": 26, + "title": "Voluptas minus sunt voluptatum quis quia ut velit distinctio itaque.", + "created_at": "2016-03-22T15:13:45.164Z", + "updated_at": "2016-03-22T15:20:30.994Z", + "milestone_id": 11, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 5, + "description": "Ea ut modi consectetur et minus beatae. Et sunt ducimus praesentium libero officia maiores voluptas cumque. Rerum in aut corporis et ullam omnis.", + "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, + "notes": [ + { + "id": 1247, + "note": "Non error magnam placeat cupiditate eum.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:30.989Z", + "updated_at": "2016-03-22T15:20:30.989Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 1246, + "note": "Eos optio et architecto eligendi ea est nihil.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:30.957Z", + "updated_at": "2016-03-22T15:20:30.957Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1245, + "note": "Reprehenderit in atque dolor et repudiandae a est.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:30.928Z", + "updated_at": "2016-03-22T15:20:30.928Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1244, + "note": "Numquam fugit doloremque iure odio et.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:30.902Z", + "updated_at": "2016-03-22T15:20:30.902Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1243, + "note": "Doloribus laboriosam id harum voluptatum vitae ut quam.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:30.863Z", + "updated_at": "2016-03-22T15:20:30.863Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1242, + "note": "Harum et ut ipsum dolore ea.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:30.832Z", + "updated_at": "2016-03-22T15:20:30.832Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 1241, + "note": "Corporis sed soluta ut est modi natus ab.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:30.802Z", + "updated_at": "2016-03-22T15:20:30.802Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 1240, + "note": "Corrupti totam tenetur officiis ratione dolores est qui vel.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:30.771Z", + "updated_at": "2016-03-22T15:20:30.771Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 13, + "state": "collected", + "st_commits": [ + { + "id": "a4e5dfebf42e34596526acb8611bc7ed80e4eb3f", + "message": "fixes #10\n", + "parent_ids": [ + "be93687618e4b132087f430a4d8fc3a609c9b77c" + ], + "authored_date": "2016-01-19T15:44:02.000+01:00", + "author_name": "Test Lopez", + "author_email": "Test@Testlopez.es", + "committed_date": "2016-01-19T15:44:02.000+01:00", + "committer_name": "Test Lopez", + "committer_email": "Test@Testlopez.es" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/test\n", + "new_path": "test", + "old_path": "test", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 13, + "created_at": "2016-03-22T15:13:45.167Z", + "updated_at": "2016-03-22T15:13:45.216Z", + "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "real_size": "1" + } + }, + { + "id": 12, + "target_branch": "test-15", + "source_branch": "test-2", + "source_project_id": 5, + "author_id": 24, + "assignee_id": 12, + "title": "In assumenda nam quaerat qui eos sit facilis enim quia quis.", + "created_at": "2016-03-22T15:13:44.837Z", + "updated_at": "2016-03-22T15:20:31.258Z", + "milestone_id": 10, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 4, + "description": "Soluta excepturi quis iste vero delectus rerum. Consequatur possimus aliquam necessitatibus deleniti rerum est impedit. Eius rem et consequatur assumenda est commodi.", + "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, + "notes": [ + { + "id": 1255, + "note": "Quibusdam rem aut similique ipsum recusandae ut accusamus.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:31.253Z", + "updated_at": "2016-03-22T15:20:31.253Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 1254, + "note": "Cumque sed omnis ipsa et magnam dolorem et.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:31.224Z", + "updated_at": "2016-03-22T15:20:31.224Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1253, + "note": "Molestiae beatae id consequatur nam minus quia.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:31.195Z", + "updated_at": "2016-03-22T15:20:31.195Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1252, + "note": "Voluptatem dolorem dignissimos itaque tempora quas ut.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:31.166Z", + "updated_at": "2016-03-22T15:20:31.166Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1251, + "note": "Debitis qui quibusdam voluptas repellat veritatis dicta rerum id.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:31.137Z", + "updated_at": "2016-03-22T15:20:31.137Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1250, + "note": "Suscipit optio ad voluptatem dignissimos temporibus amet molestias ut.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:31.107Z", + "updated_at": "2016-03-22T15:20:31.107Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 1249, + "note": "Nemo aut vitae et ducimus autem ex dolores.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:31.073Z", + "updated_at": "2016-03-22T15:20:31.073Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 1248, + "note": "Repellendus eaque ex molestiae laudantium placeat quidem vitae recusandae.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:31.038Z", + "updated_at": "2016-03-22T15:20:31.038Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 12, + "state": "collected", + "st_commits": [ + { + "id": "97a0df9696e2aebf10c31b3016f40214e0e8f243", + "message": "fixes #10\n", + "parent_ids": [ + "be93687618e4b132087f430a4d8fc3a609c9b77c" + ], + "authored_date": "2016-01-19T14:08:21.000+01:00", + "author_name": "Test Lopez", + "author_email": "Test@Testlopez.es", + "committed_date": "2016-01-19T14:08:21.000+01:00", + "committer_name": "Test Lopez", + "committer_email": "Test@Testlopez.es" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/test\n", + "new_path": "test", + "old_path": "test", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 12, + "created_at": "2016-03-22T15:13:44.840Z", + "updated_at": "2016-03-22T15:13:44.908Z", + "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "real_size": "1" + } + }, + { + "id": 11, + "target_branch": "test-3", + "source_branch": "test-5", + "source_project_id": 5, + "author_id": 26, + "assignee_id": 12, + "title": "Magni aut reprehenderit ut accusantium est eum.", + "created_at": "2016-03-22T15:13:44.494Z", + "updated_at": "2016-03-22T15:20:31.886Z", + "milestone_id": 10, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 3, + "description": "Et hic maxime harum ullam. Nulla velit pariatur libero recusandae. Dolor est earum laboriosam harum quo.", + "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, + "notes": [ + { + "id": 1263, + "note": "Beatae incidunt exercitationem voluptates recusandae fuga quia enim.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:31.883Z", + "updated_at": "2016-03-22T15:20:31.883Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 1262, + "note": "Illum sunt id consequuntur fugit et quo ullam eum.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:31.860Z", + "updated_at": "2016-03-22T15:20:31.860Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1261, + "note": "Alias reiciendis autem ipsa sequi autem nemo odio.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:31.456Z", + "updated_at": "2016-03-22T15:20:31.456Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1260, + "note": "Maxime nisi odit eos nulla vel ex accusamus velit.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:31.426Z", + "updated_at": "2016-03-22T15:20:31.426Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1259, + "note": "Excepturi et qui sapiente ut ducimus sunt nesciunt.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:31.397Z", + "updated_at": "2016-03-22T15:20:31.397Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1258, + "note": "Quis rerum dolores et dolorem modi neque ullam doloribus.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:31.364Z", + "updated_at": "2016-03-22T15:20:31.364Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 1257, + "note": "Voluptatum et mollitia neque aut.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:31.328Z", + "updated_at": "2016-03-22T15:20:31.328Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 1256, + "note": "Rerum laudantium dolor natus doloribus voluptas aliquid a.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:31.298Z", + "updated_at": "2016-03-22T15:20:31.298Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 11, + "state": "collected", + "st_commits": [ + { + "id": "f998ac87ac9244f15e9c15109a6f4e62a54b779d", + "message": "fixes #10\n", + "parent_ids": [ + "be93687618e4b132087f430a4d8fc3a609c9b77c" + ], + "authored_date": "2016-01-19T14:43:23.000+01:00", + "author_name": "Test Lopez", + "author_email": "Test@Testlopez.es", + "committed_date": "2016-01-19T14:43:23.000+01:00", + "committer_name": "Test Lopez", + "committer_email": "Test@Testlopez.es" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/test\n", + "new_path": "test", + "old_path": "test", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 11, + "created_at": "2016-03-22T15:13:44.497Z", + "updated_at": "2016-03-22T15:13:44.547Z", + "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "real_size": "1" + } + }, + { + "id": 10, + "target_branch": "test-6", + "source_branch": "test-7", + "source_project_id": 5, + "author_id": 22, + "assignee_id": 4, + "title": "Rerum commodi corporis quis qui fugit sed ut.", + "created_at": "2016-03-22T15:13:44.103Z", + "updated_at": "2016-03-22T15:20:32.096Z", + "milestone_id": 11, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 2, + "description": "Laudantium vel dignissimos aspernatur quis aut. Dolores et doloremque ipsa quia voluptate modi labore. Ipsa provident repellat error et nihil.", + "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, + "notes": [ + { + "id": 1271, + "note": "Quod ut ut quisquam et ut dolorem dolor.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:32.093Z", + "updated_at": "2016-03-22T15:20:32.093Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 1270, + "note": "Sed deserunt et explicabo rem repellat voluptatem.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:32.070Z", + "updated_at": "2016-03-22T15:20:32.070Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1269, + "note": "Veritatis architecto omnis consequatur et optio.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:32.046Z", + "updated_at": "2016-03-22T15:20:32.046Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1268, + "note": "Omnis suscipit odio molestiae debitis quia autem magni.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:32.019Z", + "updated_at": "2016-03-22T15:20:32.019Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1267, + "note": "Molestias est sunt est tempora consequatur cupiditate magnam.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:31.993Z", + "updated_at": "2016-03-22T15:20:31.993Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1266, + "note": "Ratione blanditiis eveniet voluptatem nostrum rerum excepturi in molestiae.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:31.969Z", + "updated_at": "2016-03-22T15:20:31.969Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 1265, + "note": "Illo voluptatibus vel odio ea.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:31.944Z", + "updated_at": "2016-03-22T15:20:31.944Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 1264, + "note": "Earum veritatis quis facere itaque iure.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:31.919Z", + "updated_at": "2016-03-22T15:20:31.919Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 10, + "state": "collected", + "st_commits": [ + { + "id": "b42bb86cea49bdcef943e521584b7f417d8ddd3d", + "message": "fixes #10\n", + "parent_ids": [ + "be93687618e4b132087f430a4d8fc3a609c9b77c" + ], + "authored_date": "2016-01-19T15:03:09.000+01:00", + "author_name": "Test Lopez", + "author_email": "Test@Testlopez.es", + "committed_date": "2016-01-19T15:03:09.000+01:00", + "committer_name": "Test Lopez", + "committer_email": "Test@Testlopez.es" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/test\n", + "new_path": "test", + "old_path": "test", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 10, + "created_at": "2016-03-22T15:13:44.107Z", + "updated_at": "2016-03-22T15:13:44.190Z", + "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "real_size": "1" + } + }, + { + "id": 9, + "target_branch": "test-8", + "source_branch": "test-9", + "source_project_id": 5, + "author_id": 24, + "assignee_id": 3, + "title": "Saepe et neque ut vero nobis et voluptatum facere qui minima.", + "created_at": "2016-03-22T15:13:43.792Z", + "updated_at": "2016-03-22T15:20:32.309Z", + "milestone_id": 10, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 1, + "description": "Autem enim aliquam labore qui voluptas ut voluptatem. Et corrupti sit fuga dolores alias iusto voluptatem. Excepturi ut saepe accusamus neque distinctio.", + "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, + "notes": [ + { + "id": 1279, + "note": "A corrupti nesciunt pariatur ea.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:32.307Z", + "updated_at": "2016-03-22T15:20:32.307Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Administrator" + } + }, + { + "id": 1278, + "note": "Adipisci aut ut et voluptate numquam.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:32.281Z", + "updated_at": "2016-03-22T15:20:32.281Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1277, + "note": "Adipisci voluptatem quod ut placeat repellendus deleniti.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:32.255Z", + "updated_at": "2016-03-22T15:20:32.255Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1276, + "note": "Vitae et doloremque aut et aspernatur velit placeat sed.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:32.230Z", + "updated_at": "2016-03-22T15:20:32.230Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1275, + "note": "Quos cupiditate nesciunt expedita aspernatur.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:32.207Z", + "updated_at": "2016-03-22T15:20:32.207Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1274, + "note": "Optio rem inventore dicta praesentium sit.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:32.181Z", + "updated_at": "2016-03-22T15:20:32.181Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 0" + } + }, + { + "id": 1273, + "note": "Sit incidunt molestiae maxime officiis rerum necessitatibus.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:32.159Z", + "updated_at": "2016-03-22T15:20:32.159Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 2" + } + }, + { + "id": 1272, + "note": "Autem ut non itaque molestiae nisi quia officiis doloribus.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:32.129Z", + "updated_at": "2016-03-22T15:20:32.129Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 9, + "state": "collected", + "st_commits": [ + { + "id": "e239ba8c97b80b2874579a4d625ea9628f4c8ff5", + "message": "fixes #10\n", + "parent_ids": [ + "be93687618e4b132087f430a4d8fc3a609c9b77c" + ], + "authored_date": "2016-01-19T15:38:06.000+01:00", + "author_name": "Test Lopez", + "author_email": "Test@Testlopez.es", + "committed_date": "2016-01-19T15:38:06.000+01:00", + "committer_name": "Test Lopez", + "committer_email": "Test@Testlopez.es" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/test\n", + "new_path": "test", + "old_path": "test", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 9, + "created_at": "2016-03-22T15:13:43.794Z", + "updated_at": "2016-03-22T15:13:43.848Z", + "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "real_size": "1" + } + } + ], + "pipelines": [ + { + "id": 36, + "project_id": 5, + "ref": "master", + "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "before_sha": null, + "push_data": null, + "created_at": "2016-03-22T15:20:35.755Z", + "updated_at": "2016-03-22T15:20:35.755Z", + "tag": null, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 5, + "status": "failed", + "started_at": null, + "finished_at": null, + "duration": null, + "statuses": [ + { + "id": 71, + "project_id": 5, + "status": "failed", + "finished_at": "2016-03-29T06:28:12.630Z", + "trace": null, + "created_at": "2016-03-22T15:20:35.772Z", + "updated_at": "2016-03-29T06:28:12.634Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 36, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": null + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": null + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 72, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.", + "created_at": "2016-03-22T15:20:35.777Z", + "updated_at": "2016-03-22T15:20:35.777Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 36, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + } + ] + }, + { + "id": 37, + "project_id": 5, + "ref": "master", + "sha": "048721d90c449b244b7b4c53a9186b04330174ec", + "before_sha": null, + "push_data": null, + "created_at": "2016-03-22T15:20:35.757Z", + "updated_at": "2016-03-22T15:20:35.757Z", + "tag": null, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 5, + "status": "failed", + "started_at": null, + "finished_at": null, + "duration": null, + "statuses": [ + { + "id": 74, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Ad ut quod repudiandae iste dolor doloribus. Adipisci consequuntur deserunt omnis quasi eveniet et sed fugit. Aut nemo omnis molestiae impedit ex consequatur ducimus. Voluptatum exercitationem quia aut est et hic dolorem.\n\nQuasi repellendus et eaque magni eum facilis. Dolorem aperiam nam nihil pariatur praesentium ad aliquam. Commodi enim et eos tenetur. Odio voluptatibus laboriosam mollitia rerum exercitationem magnam consequuntur. Tenetur ea vel eum corporis.\n\nVoluptatibus optio in aliquid est voluptates. Ad a ut ab placeat vero blanditiis. Earum aspernatur quia beatae expedita voluptatem dignissimos provident. Quis minima id nemo ut aut est veritatis provident.\n\nRerum voluptatem quidem eius maiores magnam veniam. Voluptatem aperiam aut voluptate et nulla deserunt voluptas. Quaerat aut accusantium laborum est dolorem architecto reiciendis. Aliquam asperiores doloribus omnis maxime enim nesciunt. Eum aut rerum repellendus debitis et ut eius.\n\nQuaerat assumenda ea sit consequatur autem in. Cum eligendi voluptatem quo sed. Ut fuga iusto cupiditate autem sint.\n\nOfficia totam officiis architecto corporis molestiae amet ut. Tempora sed dolorum rerum omnis voluptatem accusantium sit eum. Quia debitis ipsum quidem aliquam inventore sunt consequatur qui.", + "created_at": "2016-03-22T15:20:35.846Z", + "updated_at": "2016-03-22T15:20:35.846Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 37, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 73, + "project_id": 5, + "status": "canceled", + "finished_at": null, + "trace": null, + "created_at": "2016-03-22T15:20:35.842Z", + "updated_at": "2016-03-22T15:20:35.842Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 37, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": null + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": null + }, + "erased_by_id": null, + "erased_at": null + } + ] + }, + { + "id": 38, + "project_id": 5, + "ref": "master", + "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849", + "before_sha": null, + "push_data": null, + "created_at": "2016-03-22T15:20:35.759Z", + "updated_at": "2016-03-22T15:20:35.759Z", + "tag": null, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 5, + "status": "failed", + "started_at": null, + "finished_at": null, + "duration": null, + "statuses": [ + { + "id": 76, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Et rerum quia ea cumque ut modi non. Libero eaque ipsam architecto maiores expedita deleniti. Ratione quia qui est id.\n\nQuod sit officiis sed unde inventore veniam quisquam velit. Ea harum cum quibusdam quisquam minima quo possimus non. Temporibus itaque aliquam aut rerum veritatis at.\n\nMagnam ipsum eius recusandae qui quis sit maiores eum. Et animi iusto aut itaque. Doloribus harum deleniti nobis accusantium et libero.\n\nRerum fuga perferendis magni commodi officiis id repudiandae. Consequatur ratione consequatur suscipit facilis sunt iure est dicta. Qui unde quasi facilis et quae nesciunt. Magnam iste et nobis officiis tenetur. Aspernatur quo et temporibus non in.\n\nNisi rerum velit est ad enim sint molestiae consequuntur. Quaerat nisi nesciunt quasi officiis. Possimus non blanditiis laborum quos.\n\nRerum laudantium facere animi qui. Ipsa est iusto magnam nihil. Enim omnis occaecati non dignissimos ut recusandae eum quasi. Qui maxime dolor et nemo voluptates incidunt quia.", + "created_at": "2016-03-22T15:20:35.882Z", + "updated_at": "2016-03-22T15:20:35.882Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 38, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 75, + "project_id": 5, + "status": "failed", + "finished_at": null, + "trace": "Sed et iste recusandae dicta corporis. Sunt alias porro fugit sunt. Fugiat omnis nihil dignissimos aperiam explicabo doloremque sit aut. Harum fugit expedita quia rerum ut consequatur laboriosam aliquam.\n\nNatus libero ut ut tenetur earum. Tempora omnis autem omnis et libero dolores illum autem. Deleniti eos sunt mollitia ipsam. Cum dolor repellendus dolorum sequi officia. Ullam sunt in aut pariatur excepturi.\n\nDolor nihil debitis et est eos. Cumque eos eum saepe ducimus autem. Alias architecto consequatur aut pariatur possimus. Aut quos aut incidunt quam velit et. Quas voluptatum ad dolorum dignissimos.\n\nUt voluptates consectetur illo et. Est commodi accusantium vel quo. Eos qui fugiat soluta porro.\n\nRatione possimus alias vel maxime sint totam est repellat. Ipsum corporis eos sint voluptatem eos odit. Temporibus libero nulla harum eligendi labore similique ratione magnam. Suscipit sequi in omnis neque.\n\nLaudantium dolor amet omnis placeat mollitia aut molestiae. Aut rerum similique ipsum quod illo quas unde. Sunt aut veritatis eos omnis porro. Rem veritatis mollitia praesentium dolorem. Consequatur sequi ad cumque earum omnis quia necessitatibus.", + "created_at": "2016-03-22T15:20:35.864Z", + "updated_at": "2016-03-22T15:20:35.864Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 38, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + } + ] + }, + { + "id": 39, + "project_id": 5, + "ref": "master", + "sha": "d2d430676773caa88cdaf7c55944073b2fd5561a", + "before_sha": null, + "push_data": null, + "created_at": "2016-03-22T15:20:35.761Z", + "updated_at": "2016-03-22T15:20:35.761Z", + "tag": null, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 5, + "status": "failed", + "started_at": null, + "finished_at": null, + "duration": null, + "statuses": [ + { + "id": 78, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Dolorem deserunt quas quia error hic quo cum vel. Natus voluptatem cumque expedita numquam odit. Eos expedita nostrum corporis consequatur est recusandae.\n\nCulpa blanditiis rerum repudiandae alias voluptatem. Velit iusto est ullam consequatur doloribus porro. Corporis voluptas consectetur est veniam et quia quae.\n\nEt aut magni fuga nesciunt officiis molestias. Quaerat et nam necessitatibus qui rerum. Architecto quia officiis voluptatem laborum est recusandae. Quasi ducimus soluta odit necessitatibus labore numquam dignissimos. Quia facere sint temporibus inventore sunt nihil saepe dolorum.\n\nFacere dolores quis dolores a. Est minus nostrum nihil harum. Earum laborum et ipsum unde neque sit nemo. Corrupti est consequatur minima fugit. Illum voluptatem illo error ducimus officia qui debitis.\n\nDignissimos porro a autem harum aut. Aut id reprehenderit et exercitationem. Est et quisquam ipsa temporibus molestiae. Architecto natus dolore qui fugiat incidunt. Autem odit veniam excepturi et voluptatibus culpa ipsum eos.\n\nAmet quo quisquam dignissimos soluta modi dolores. Sint omnis eius optio corporis dolor. Eligendi animi porro quia placeat ut.", + "created_at": "2016-03-22T15:20:35.927Z", + "updated_at": "2016-03-22T15:20:35.927Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 39, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 77, + "project_id": 5, + "status": "failed", + "finished_at": null, + "trace": "Rerum ut et suscipit est perspiciatis. Inventore debitis cum eius vitae. Ex incidunt id velit aut quo nisi. Laboriosam repellat deserunt eius reiciendis architecto et. Est harum quos nesciunt nisi consectetur.\n\nAlias esse omnis sint officia est consequatur in nobis. Dignissimos dolorum vel eligendi nesciunt dolores sit. Veniam mollitia ducimus et exercitationem molestiae libero sed. Atque omnis debitis laudantium voluptatibus qui. Repellendus tempore est commodi pariatur.\n\nExpedita voluptate illum est alias non. Modi nesciunt ab assumenda laborum nulla consequatur molestias doloremque. Magnam quod officia vel explicabo accusamus ut voluptatem incidunt. Rerum ut aliquid ullam saepe. Est eligendi debitis beatae blanditiis reiciendis.\n\nQui fuga sit dolores libero maiores et suscipit. Consectetur asperiores omnis minima impedit eos fugiat. Similique omnis nisi sed vero inventore ipsum aliquam exercitationem.\n\nBlanditiis magni iure dolorum omnis ratione delectus molestiae. Atque officia dolor voluptatem culpa quod. Incidunt suscipit quidem possimus veritatis non vel. Iusto aliquid et id quia quasi.\n\nVel facere velit blanditiis incidunt cupiditate sed maiores consequuntur. Quasi quia dicta consequuntur et quia voluptatem iste id. Incidunt et rerum fuga esse sint.", + "created_at": "2016-03-22T15:20:35.905Z", + "updated_at": "2016-03-22T15:20:35.905Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 39, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + } + ] + }, + { + "id": 40, + "project_id": 5, + "ref": "master", + "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73", + "before_sha": null, + "push_data": null, + "created_at": "2016-03-22T15:20:35.763Z", + "updated_at": "2016-03-22T15:20:35.763Z", + "tag": null, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 5, + "status": "failed", + "started_at": null, + "finished_at": null, + "duration": null, + "statuses": [ + { + "id": 79, + "project_id": 5, + "status": "failed", + "finished_at": "2016-03-29T06:28:12.695Z", + "trace": "Sed culpa est et facere saepe vel id ab. Quas temporibus aut similique dolorem consequatur corporis aut praesentium. Cum officia molestiae sit earum excepturi.\n\nSint possimus aut ratione quia. Quis nesciunt ratione itaque illo. Tenetur est dolor assumenda possimus voluptatem quia minima. Accusamus reprehenderit ut et itaque non reiciendis incidunt.\n\nRerum suscipit quibusdam dolore nam omnis. Consequatur ipsa nihil ut enim blanditiis delectus. Nulla quis hic occaecati mollitia qui placeat. Quo rerum sed perferendis a accusantium consequatur commodi ut. Sit quae et cumque vel eius tempora nostrum.\n\nUllam dolorem et itaque sint est. Ea molestias quia provident dolorem vitae error et et. Ea expedita officiis iste non. Qui vitae odit saepe illum. Dolores enim ratione deserunt tempore expedita amet non neque.\n\nEligendi asperiores voluptatibus omnis repudiandae expedita distinctio qui aliquid. Autem aut doloremque distinctio ab. Nostrum sapiente repudiandae aspernatur ea et quae voluptas. Officiis perspiciatis nisi laudantium asperiores error eligendi ab. Eius quia amet magni omnis exercitationem voluptatum et.\n\nVoluptatem ullam labore quas dicta est ex voluptas. Pariatur ea modi voluptas consequatur dolores perspiciatis similique. Numquam in distinctio perspiciatis ut qui earum. Quidem omnis mollitia facere aut beatae. Ea est iure et voluptatem.", + "created_at": "2016-03-22T15:20:35.950Z", + "updated_at": "2016-03-29T06:28:12.696Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 40, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": null + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": null + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 80, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Impedit et optio nemo ipsa. Non ad non quis ut sequi laudantium omnis velit. Corporis a enim illo eos. Quia totam tempore inventore ad est.\n\nNihil recusandae cupiditate eaque voluptatem molestias sint. Consequatur id voluptatem cupiditate harum. Consequuntur iusto quaerat reiciendis aut autem libero est. Quisquam dolores veritatis rerum et sint maxime ullam libero. Id quas porro ut perspiciatis rem amet vitae.\n\nNemo inventore minus blanditiis magnam. Modi consequuntur nostrum aut voluptatem ex. Sunt rerum rem optio mollitia qui aliquam officiis officia. Aliquid eos et id aut minus beatae reiciendis.\n\nDolores non in temporibus dicta. Fugiat voluptatem est aspernatur expedita voluptatum nam qui. Quia et eligendi sit quae sint tempore exercitationem eos. Est sapiente corrupti quidem at. Qui magni odio repudiandae saepe tenetur optio dolore.\n\nEos placeat soluta at dolorem adipisci provident. Quo commodi id reprehenderit possimus quo tenetur. Ipsum et quae eligendi laborum. Et qui nesciunt at quasi quidem voluptatem cum rerum. Excepturi non facilis aut sunt vero sed.\n\nQui explicabo ratione ut eligendi recusandae. Quis quasi quas molestiae consequatur voluptatem et voluptatem. Ex repellat saepe occaecati aperiam ea eveniet dignissimos facilis.", + "created_at": "2016-03-22T15:20:35.966Z", + "updated_at": "2016-03-22T15:20:35.966Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 40, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + } + ] + } + ] +} \ No newline at end of file diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb new file mode 100644 index 0000000000..7a40a43f8a --- /dev/null +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do + describe 'restore project tree' do + + let(:user) { create(:user) } + let(:namespace) { create(:namespace, owner: user) } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } + let(:project) { create(:empty_project, name: 'project', path: 'project') } + let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } + let(:restored_project_json) { project_tree_restorer.restore } + + before do + allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + end + + context 'JSON' do + 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_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb new file mode 100644 index 0000000000..8d29b2f8fd --- /dev/null +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -0,0 +1,149 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::ProjectTreeSaver, services: true do + describe 'saves the project tree into a json object' do + + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } + let(:project_tree_saver) { described_class.new(project: project, shared: shared) } + let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:user) { create(:user) } + let(:project) { setup_project } + + before do + project.team << [user, :master] + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + end + + after do + FileUtils.rm_rf(export_path) + end + + it 'saves project successfully' do + expect(project_tree_saver.save).to be true + end + + context 'JSON' do + + let(:saved_project_json) do + project_tree_saver.save + project_json(project_tree_saver.full_path) + end + + it 'saves the correct json' do + expect(saved_project_json).to include({ "visibility_level" => 20 }) + end + + it 'has events' do + expect(saved_project_json['events']).not_to be_empty + end + + it 'has milestones' do + expect(saved_project_json['milestones']).not_to be_empty + end + + it 'has merge requests' do + expect(saved_project_json['merge_requests']).not_to be_empty + end + + it 'has labels' do + expect(saved_project_json['labels']).not_to be_empty + end + + it 'has snippets' do + expect(saved_project_json['snippets']).not_to be_empty + end + + it 'has snippet notes' do + expect(saved_project_json['snippets'].first['notes']).not_to be_empty + end + + it 'has releases' do + expect(saved_project_json['releases']).not_to be_empty + end + + it 'has issues' do + expect(saved_project_json['issues']).not_to be_empty + end + + it 'has issue comments' do + expect(saved_project_json['issues'].first['notes']).not_to be_empty + end + + it 'has author on issue comments' do + expect(saved_project_json['issues'].first['notes'].first['author']).not_to be_empty + end + + it 'has project members' do + expect(saved_project_json['project_members']).not_to be_empty + end + + it 'has merge requests diffs' do + expect(saved_project_json['merge_requests'].first['merge_request_diff']).not_to be_empty + end + + it 'has merge requests comments' do + expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty + end + + it 'has author on merge requests comments' do + expect(saved_project_json['merge_requests'].first['notes'].first['author']).not_to be_empty + end + + it 'has pipeline statuses' do + expect(saved_project_json['pipelines'].first['statuses']).not_to be_empty + end + + it 'has pipeline builds' do + expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build'}).to eq(1) + end + + it 'has pipeline commits' do + expect(saved_project_json['pipelines']).not_to be_empty + end + + it 'has ci pipeline notes' do + expect(saved_project_json['pipelines'].first['notes']).not_to be_empty + end + end + end + + def setup_project + issue = create(:issue, assignee: user) + merge_request = create(:merge_request) + label = create(:label) + snippet = create(:project_snippet) + release = create(:release) + + project = create(:project, + :public, + issues: [issue], + merge_requests: [merge_request], + labels: [label], + snippets: [snippet], + releases: [release] + ) + + commit_status = create(:commit_status, project: project) + + ci_pipeline = create(:ci_pipeline, + project: project, + sha: merge_request.last_commit.id, + ref: merge_request.source_branch, + statuses: [commit_status]) + + create(:ci_build, pipeline: ci_pipeline, project: project) + create(:milestone, project: project) + create(:note, noteable: issue, project: project) + create(:note, noteable: merge_request, project: project) + create(:note, noteable: snippet, project: project) + create(:note_on_commit, + author: user, + project: project, + commit_id: ci_pipeline.sha) + project + end + + def project_json(filename) + JSON.parse(IO.read(filename)) + end +end diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb new file mode 100644 index 0000000000..109522fa62 --- /dev/null +++ b/spec/lib/gitlab/import_export/reader_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::Reader, lib: true do + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path:'') } + let(:test_config) { 'spec/support/import_export/import_export.yml' } + let(:project_tree_hash) do + { + only: [:name, :path], + include: [:issues, :labels, + { merge_requests: { + only: [:id], + except: [:iid], + include: [:merge_request_diff, :merge_request_test] + } }, + { commit_statuses: { include: :commit } }] + } + end + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config) + end + + it 'generates hash from project tree config' do + expect(described_class.new(shared: shared).project_tree).to match(project_tree_hash) + end + + context 'individual scenarios' do + + it 'generates the correct hash for a single project relation' do + setup_yaml(project_tree: [:issues]) + + expect(described_class.new(shared: shared).project_tree).to match(include: [:issues]) + end + + it 'generates the correct hash for a multiple project relation' do + setup_yaml(project_tree: [:issues, :snippets]) + + expect(described_class.new(shared: shared).project_tree).to match(include: [:issues, :snippets]) + end + + it 'generates the correct hash for a single sub-relation' do + setup_yaml(project_tree: [issues: [:notes]]) + + expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { include: :notes } }]) + end + + it 'generates the correct hash for a multiple sub-relation' do + setup_yaml(project_tree: [merge_requests: [:notes, :merge_request_diff]]) + + expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: [:notes, :merge_request_diff] } }]) + end + + it 'generates the correct hash for a sub-relation with another sub-relation' do + setup_yaml(project_tree: [merge_requests: [notes: :author]]) + + expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: { notes: { include: :author } } } }]) + end + + it 'generates the correct hash for a relation with included attributes' do + setup_yaml(project_tree: [:issues], included_attributes: { issues: [:name, :description] }) + + expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { only: [:name, :description] } }]) + end + + it 'generates the correct hash for a relation with excluded attributes' do + setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] }) + + expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name] } }]) + end + + it 'generates the correct hash for a relation with both excluded and included attributes' do + setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] }, included_attributes: { issues: [:description] }) + + expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name], only: [:description] } }]) + end + + it 'generates the correct hash for a relation with custom methods' do + setup_yaml(project_tree: [:issues], methods: { issues: [:name] }) + + expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }]) + end + + def setup_yaml(hash) + allow(YAML).to receive(:load_file).with(test_config).and_return(hash) + end + end +end diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_bundler_spec.rb new file mode 100644 index 0000000000..590a9a7e1a --- /dev/null +++ b/spec/lib/gitlab/import_export/repo_bundler_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::RepoSaver, services: true do + describe 'bundle a project Git repo' do + + let(:user) { create(:user) } + let!(:project) { create(:project, :public, name: 'searchable_project') } + let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } + let(:bundler) { described_class.new(project: project, shared: shared) } + + before do + project.team << [user, :master] + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + end + + after do + FileUtils.rm_rf(export_path) + end + + it 'bundles the repo successfully' do + expect(bundler.save).to be true + end + end +end diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb new file mode 100644 index 0000000000..b9ffc8694a --- /dev/null +++ b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::WikiRepoSaver, services: true do + describe 'bundle a wiki Git repo' do + + let(:user) { create(:user) } + let!(:project) { create(:project, :public, name: 'searchable_project') } + let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } + let(:wiki_bundler) { described_class.new(project: project, shared: shared) } + let!(:project_wiki) { ProjectWiki.new(project, user) } + + before do + project.team << [user, :master] + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + project_wiki.wiki + project_wiki.create_page("index", "test content") + end + + after do + FileUtils.rm_rf(export_path) + end + + it 'bundles the repo successfully' do + expect(wiki_bundler.save).to be true + end + end +end diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb index 88814bc474..659facd6c1 100644 --- a/spec/lib/gitlab/lfs/lfs_router_spec.rb +++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb @@ -17,12 +17,15 @@ describe Gitlab::Lfs::Router, lib: true do } end - let(:lfs_router_auth) { new_lfs_router(project, user) } - let(:lfs_router_noauth) { new_lfs_router(project, nil) } - let(:lfs_router_public_auth) { new_lfs_router(public_project, user) } - let(:lfs_router_public_noauth) { new_lfs_router(public_project, nil) } - let(:lfs_router_forked_noauth) { new_lfs_router(forked_project, nil) } - let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user_two) } + let(:lfs_router_auth) { new_lfs_router(project, user: user) } + let(:lfs_router_ci_auth) { new_lfs_router(project, ci: true) } + let(:lfs_router_noauth) { new_lfs_router(project) } + let(:lfs_router_public_auth) { new_lfs_router(public_project, user: user) } + let(:lfs_router_public_ci_auth) { new_lfs_router(public_project, ci: true) } + let(:lfs_router_public_noauth) { new_lfs_router(public_project) } + let(:lfs_router_forked_noauth) { new_lfs_router(forked_project) } + let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user: user_two) } + let(:lfs_router_forked_ci_auth) { new_lfs_router(forked_project, ci: true) } let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" } let(:sample_size) { 499013 } @@ -80,6 +83,7 @@ describe Gitlab::Lfs::Router, lib: true do context 'with required headers' do before do + project.lfs_objects << lfs_object env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile" end @@ -91,7 +95,6 @@ describe Gitlab::Lfs::Router, lib: true do context 'when user has project access' do before do - project.lfs_objects << lfs_object project.team << [user, :master] end @@ -104,6 +107,17 @@ describe Gitlab::Lfs::Router, lib: true do expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path) end end + + context 'when CI is authorized' do + it "responds with status 200" do + expect(lfs_router_ci_auth.try_call.first).to eq(200) + end + + it "responds with the file location" do + expect(lfs_router_ci_auth.try_call[1]['Content-Type']).to eq("application/octet-stream") + expect(lfs_router_ci_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path) + end + end end context 'without required headers' do @@ -134,143 +148,145 @@ describe Gitlab::Lfs::Router, lib: true do end describe 'download' do - describe 'when user is authenticated' do - before do - body = { 'operation' => 'download', - 'objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size - }] - }.to_json - env['rack.input'] = StringIO.new(body) + before do + body = { 'operation' => 'download', + 'objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size + }] + }.to_json + env['rack.input'] = StringIO.new(body) + end + + shared_examples 'an authorized requests' do + context 'when downloading an lfs object that is assigned to our project' do + before do + project.lfs_objects << lfs_object + end + + it 'responds with status 200 and href to download' do + response = router.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) + + expect(response_body).to eq('objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size, + 'actions' => { + 'download' => { + 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => { 'Authorization' => auth } + } + } + }]) + end end - describe 'when user has download access' do + context 'when downloading an lfs object that is assigned to other project' do before do - @auth = authorize(user) - env["HTTP_AUTHORIZATION"] = @auth - project.team << [user, :reporter] + public_project.lfs_objects << lfs_object end - context 'when downloading an lfs object that is assigned to our project' do - before do - project.lfs_objects << lfs_object - end + it 'responds with status 200 and error message' do + response = router.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) - it 'responds with status 200 and href to download' do - response = lfs_router_auth.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) + expect(response_body).to eq('objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }]) + end + end - expect(response_body).to eq('objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size, - 'actions' => { - 'download' => { - 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", - 'header' => { 'Authorization' => @auth } - } - } - }]) - end + context 'when downloading a lfs object that does not exist' do + before do + body = { 'operation' => 'download', + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }] + }.to_json + env['rack.input'] = StringIO.new(body) end - context 'when downloading an lfs object that is assigned to other project' do - before do - public_project.lfs_objects << lfs_object - end + it "responds with status 200 and error message" do + response = router.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) - it 'responds with status 200 and error message' do - response = lfs_router_auth.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) + expect(response_body).to eq('objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }]) + end + end - expect(response_body).to eq('objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size, - 'error' => { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", - } - }]) - end + context 'when downloading one new and one existing lfs object' do + before do + body = { 'operation' => 'download', + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }, + { 'oid' => sample_oid, + 'size' => sample_size + } + ] + }.to_json + env['rack.input'] = StringIO.new(body) + project.lfs_objects << lfs_object end - context 'when downloading a lfs object that does not exist' do - before do - body = { 'operation' => 'download', - 'objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }] - }.to_json - env['rack.input'] = StringIO.new(body) - end + it "responds with status 200 with upload hypermedia link for the new object" do + response = router.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) - it "responds with status 200 and error message" do - response = lfs_router_auth.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) - - expect(response_body).to eq('objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078, - 'error' => { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", + expect(response_body).to eq('objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }, + { 'oid' => sample_oid, + 'size' => sample_size, + 'actions' => { + 'download' => { + 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => { 'Authorization' => auth } } - }]) - end + } + }]) end + end + end - context 'when downloading one new and one existing lfs object' do - before do - body = { 'operation' => 'download', - 'objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }, - { 'oid' => sample_oid, - 'size' => sample_size - } - ] - }.to_json - env['rack.input'] = StringIO.new(body) - project.lfs_objects << lfs_object - end + context 'when user is authenticated' do + let(:auth) { authorize(user) } - it "responds with status 200 with upload hypermedia link for the new object" do - response = lfs_router_auth.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) + before do + env["HTTP_AUTHORIZATION"] = auth + project.team << [user, role] + end - expect(response_body).to eq('objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078, - 'error' => { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", - } - }, - { 'oid' => sample_oid, - 'size' => sample_size, - 'actions' => { - 'download' => { - 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", - 'header' => { 'Authorization' => @auth } - } - } - }]) - end - end + it_behaves_like 'an authorized requests' do + let(:role) { :reporter } + let(:router) { lfs_router_auth } end context 'when user does is not member of the project' do - before do - @auth = authorize(user) - env["HTTP_AUTHORIZATION"] = @auth - project.team << [user, :guest] - end + let(:role) { :guest } it 'responds with 403' do expect(lfs_router_auth.try_call.first).to eq(403) @@ -278,11 +294,7 @@ describe Gitlab::Lfs::Router, lib: true do end context 'when user does not have download access' do - before do - @auth = authorize(user) - env["HTTP_AUTHORIZATION"] = @auth - project.team << [user, :guest] - end + let(:role) { :guest } it 'responds with 403' do expect(lfs_router_auth.try_call.first).to eq(403) @@ -290,18 +302,19 @@ describe Gitlab::Lfs::Router, lib: true do end end - context 'when user is not authenticated' do - before do - body = { 'operation' => 'download', - 'objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size - }], + context 'when CI is authorized' do + let(:auth) { 'gitlab-ci-token:password' } - }.to_json - env['rack.input'] = StringIO.new(body) + before do + env["HTTP_AUTHORIZATION"] = auth end + it_behaves_like 'an authorized requests' do + let(:router) { lfs_router_ci_auth } + end + end + + context 'when user is not authenticated' do describe 'is accessing public project' do before do public_project.lfs_objects << lfs_object @@ -338,17 +351,17 @@ describe Gitlab::Lfs::Router, lib: true do end describe 'upload' do - describe 'when user is authenticated' do - before do - body = { 'operation' => 'upload', - 'objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size - }] - }.to_json - env['rack.input'] = StringIO.new(body) - end + before do + body = { 'operation' => 'upload', + 'objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size + }] + }.to_json + env['rack.input'] = StringIO.new(body) + end + describe 'when request is authenticated' do describe 'when user has project push access' do before do @auth = authorize(user) @@ -440,15 +453,15 @@ describe Gitlab::Lfs::Router, lib: true do expect(lfs_router_auth.try_call.first).to eq(403) end end + + context 'when CI is authorized' do + it 'responds with 401' do + expect(lfs_router_ci_auth.try_call.first).to eq(401) + end + end end context 'when user is not authenticated' do - before do - env['rack.input'] = StringIO.new( - { 'objects' => [], 'operation' => 'upload' }.to_json - ) - end - context 'when user has push access' do before do project.team << [user, :master] @@ -465,6 +478,18 @@ describe Gitlab::Lfs::Router, lib: true do end end end + + context 'when CI is authorized' do + let(:auth) { 'gitlab-ci-token:password' } + + before do + env["HTTP_AUTHORIZATION"] = auth + end + + it "responds with status 403" do + expect(lfs_router_public_ci_auth.try_call.first).to eq(401) + end + end end describe 'unsupported' do @@ -490,13 +515,68 @@ describe Gitlab::Lfs::Router, lib: true do env['REQUEST_METHOD'] = 'PUT' end - describe 'to one project' do - describe 'when user has push access to the project' do + shared_examples 'unauthorized' do + context 'and request is sent by gitlab-workhorse to authorize the request' do before do - project.team << [user, :master] + header_for_upload_authorize(router.project) end - describe 'when user is authenticated' do + it 'responds with status 401' do + expect(router.try_call.first).to eq(401) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + headers_for_upload_finalize(router.project) + end + + it 'responds with status 401' do + expect(router.try_call.first).to eq(401) + end + end + + context 'and request is sent with a malformed headers' do + before do + env["PATH_INFO"] = "#{router.project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}" + env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd" + end + + it 'does not recognize it as a valid lfs command' do + expect(router.try_call).to eq(nil) + end + end + end + + shared_examples 'forbidden' do + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + header_for_upload_authorize(router.project) + end + + it 'responds with 403' do + expect(router.try_call.first).to eq(403) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + headers_for_upload_finalize(router.project) + end + + it 'responds with 403' do + expect(router.try_call.first).to eq(403) + end + end + end + + describe 'to one project' do + describe 'when user is authenticated' do + describe 'when user has push access to the project' do + before do + project.team << [user, :developer] + end + context 'and request is sent by gitlab-workhorse to authorize the request' do before do header_for_upload_authorize(project) @@ -524,100 +604,35 @@ describe Gitlab::Lfs::Router, lib: true do end end - describe 'when user is unauthenticated' do - let(:lfs_router_noauth) { new_lfs_router(project, nil) } + describe 'and user does not have push access' do + let(:router) { lfs_router_auth } - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(project) - end - - it 'responds with status 401' do - expect(lfs_router_noauth.try_call.first).to eq(401) - end - end - - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(project) - end - - it 'responds with status 401' do - expect(lfs_router_noauth.try_call.first).to eq(401) - end - end - - context 'and request is sent with a malformed headers' do - before do - env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}" - env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd" - end - - it 'does not recognize it as a valid lfs command' do - expect(lfs_router_noauth.try_call).to eq(nil) - end - end + it_behaves_like 'forbidden' end end - describe 'and user does not have push access' do - describe 'when user is authenticated' do - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(project) - end + context 'when CI is authenticated' do + let(:router) { lfs_router_ci_auth } - it 'responds with 403' do - expect(lfs_router_auth.try_call.first).to eq(403) - end - end + it_behaves_like 'unauthorized' + end - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(project) - end + context 'for unauthenticated' do + let(:router) { new_lfs_router(project) } - it 'responds with 403' do - expect(lfs_router_auth.try_call.first).to eq(403) - end - end - end - - describe 'when user is unauthenticated' do - let(:lfs_router_noauth) { new_lfs_router(project, nil) } - - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(project) - end - - it 'responds with 401' do - expect(lfs_router_noauth.try_call.first).to eq(401) - end - end - - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(project) - end - - it 'responds with 401' do - expect(lfs_router_noauth.try_call.first).to eq(401) - end - end - end + it_behaves_like 'unauthorized' end end - describe "to a forked project" do + describe 'to a forked project' do let(:forked_project) { fork_project(public_project, user) } - describe 'when user has push access to the project' do - before do - forked_project.team << [user_two, :master] - end + describe 'when user is authenticated' do + describe 'when user has push access to the project' do + before do + forked_project.team << [user_two, :developer] + end - describe 'when user is authenticated' do context 'and request is sent by gitlab-workhorse to authorize the request' do before do header_for_upload_authorize(forked_project) @@ -645,78 +660,28 @@ describe Gitlab::Lfs::Router, lib: true do end end - describe 'when user is unauthenticated' do - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(forked_project) - end + describe 'and user does not have push access' do + let(:router) { lfs_router_forked_auth } - it 'responds with status 401' do - expect(lfs_router_forked_noauth.try_call.first).to eq(401) - end - end - - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(forked_project) - end - - it 'responds with status 401' do - expect(lfs_router_forked_noauth.try_call.first).to eq(401) - end - end + it_behaves_like 'forbidden' end end - describe 'and user does not have push access' do - describe 'when user is authenticated' do - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(forked_project) - end + context 'when CI is authenticated' do + let(:router) { lfs_router_forked_ci_auth } - it 'responds with 403' do - expect(lfs_router_forked_auth.try_call.first).to eq(403) - end - end + it_behaves_like 'unauthorized' + end - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(forked_project) - end + context 'for unauthenticated' do + let(:router) { lfs_router_forked_noauth } - it 'responds with 403' do - expect(lfs_router_forked_auth.try_call.first).to eq(403) - end - end - end - - describe 'when user is unauthenticated' do - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(forked_project) - end - - it 'responds with 401' do - expect(lfs_router_forked_noauth.try_call.first).to eq(401) - end - end - - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(forked_project) - end - - it 'responds with 401' do - expect(lfs_router_forked_noauth.try_call.first).to eq(401) - end - end - end + it_behaves_like 'unauthorized' end describe 'and second project not related to fork or a source project' do let(:second_project) { create(:project) } - let(:lfs_router_second_project) { new_lfs_router(second_project, user) } + let(:lfs_router_second_project) { new_lfs_router(second_project, user: user) } before do public_project.lfs_objects << lfs_object @@ -745,8 +710,8 @@ describe Gitlab::Lfs::Router, lib: true do ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) end - def new_lfs_router(project, user) - Gitlab::Lfs::Router.new(project, user, request) + def new_lfs_router(project, user: nil, ci: false) + Gitlab::Lfs::Router.new(project, user, ci, request) end def header_for_upload_authorize(project) diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index cdf641341c..8809b7e3f1 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -78,9 +78,8 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:add_metric). - with(described_class::SERIES, hash_including(:duration, :cpu_duration), - method: 'Dummy.foo') + expect(transaction).to receive(:measure_method). + with('Dummy.foo') @dummy.foo end @@ -158,9 +157,8 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:add_metric). - with(described_class::SERIES, hash_including(:duration, :cpu_duration), - method: 'Dummy#bar') + expect(transaction).to receive(:measure_method). + with('Dummy#bar') @dummy.new.bar end diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb new file mode 100644 index 0000000000..8d05081eec --- /dev/null +++ b/spec/lib/gitlab/metrics/method_call_spec.rb @@ -0,0 +1,91 @@ +require 'spec_helper' + +describe Gitlab::Metrics::MethodCall do + let(:method_call) { described_class.new('Foo#bar', 'foo') } + + describe '#measure' do + it 'measures the performance of the supplied block' do + method_call.measure { 'foo' } + + expect(method_call.real_time).to be_a_kind_of(Numeric) + expect(method_call.cpu_time).to be_a_kind_of(Numeric) + expect(method_call.call_count).to eq(1) + end + end + + describe '#to_metric' do + it 'returns a Metric instance' do + method_call.measure { 'foo' } + metric = method_call.to_metric + + expect(metric).to be_an_instance_of(Gitlab::Metrics::Metric) + expect(metric.series).to eq('foo') + + expect(metric.values[:duration]).to be_a_kind_of(Numeric) + expect(metric.values[:cpu_duration]).to be_a_kind_of(Numeric) + expect(metric.values[:call_count]).to an_instance_of(Fixnum) + + expect(metric.tags).to eq({ method: 'Foo#bar' }) + end + end + + describe '#above_threshold?' do + it 'returns false when the total call time is not above the threshold' do + expect(method_call.above_threshold?).to eq(false) + end + + it 'returns true when the total call time is above the threshold' do + expect(method_call).to receive(:real_time).and_return(9000) + + expect(method_call.above_threshold?).to eq(true) + end + end + + describe '#call_count' do + context 'without any method calls' do + it 'returns 0' do + expect(method_call.call_count).to eq(0) + end + end + + context 'with method calls' do + it 'returns the number of method calls' do + method_call.measure { 'foo' } + + expect(method_call.call_count).to eq(1) + end + end + end + + describe '#cpu_time' do + context 'without timings' do + it 'returns 0.0' do + expect(method_call.cpu_time).to eq(0.0) + end + end + + context 'with timings' do + it 'returns the total CPU time' do + method_call.measure { 'foo' } + + expect(method_call.cpu_time >= 0.0).to be(true) + end + end + end + + describe '#real_time' do + context 'without timings' do + it 'returns 0.0' do + expect(method_call.real_time).to eq(0.0) + end + end + + context 'with timings' do + it 'returns the total real time' do + method_call.measure { 'foo' } + + expect(method_call.real_time >= 0.0).to be(true) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index 40289f8b97..f264ed6402 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -58,6 +58,22 @@ describe Gitlab::Metrics::RackMiddleware do expect(transaction.values[:request_method]).to eq('GET') expect(transaction.values[:request_uri]).to eq('/foo') end + + context "when URI includes sensitive parameters" do + let(:env) do + { + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/foo?private_token=my-token', + 'PATH_INFO' => '/foo', + 'QUERY_STRING' => 'private_token=my_token', + 'action_dispatch.parameter_filter' => [:private_token] + } + end + + it 'stores the request URI with the sensitive parameters filtered' do + expect(transaction.values[:request_uri]).to eq('/foo?private_token=[FILTERED]') + end + end end describe '#tag_controller' do diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 1d5a51a157..3b1c67a214 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -46,6 +46,22 @@ describe Gitlab::Metrics::Transaction do end end + describe '#measure_method' do + it 'adds a new method if it does not exist already' do + transaction.measure_method('Foo#bar') { 'foo' } + + expect(transaction.methods['Foo#bar']). + to be_an_instance_of(Gitlab::Metrics::MethodCall) + end + + it 'adds timings to an existing method call' do + transaction.measure_method('Foo#bar') { 'foo' } + transaction.measure_method('Foo#bar') { 'foo' } + + expect(transaction.methods['Foo#bar'].call_count).to eq(2) + end + end + describe '#increment' do it 'increments a counter' do transaction.increment(:time, 1) diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 7c617723e6..7b4ccc8391 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -105,7 +105,8 @@ describe Gitlab::ReferenceExtractor, lib: true do it 'returns JIRA issues for a JIRA-integrated project' do subject.analyze('JIRA-123 and FOOBAR-4567') - expect(subject.issues).to eq [JiraIssue.new('JIRA-123', project), JiraIssue.new('FOOBAR-4567', project)] + expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project), + ExternalIssue.new('FOOBAR-4567', project)] end end diff --git a/spec/lib/gitlab/gitignore_spec.rb b/spec/lib/gitlab/template/gitignore_spec.rb similarity index 87% rename from spec/lib/gitlab/gitignore_spec.rb rename to spec/lib/gitlab/template/gitignore_spec.rb index 72baa516cc..bc0ec9325c 100644 --- a/spec/lib/gitlab/gitignore_spec.rb +++ b/spec/lib/gitlab/template/gitignore_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe Gitlab::Gitignore do - subject { Gitlab::Gitignore } +describe Gitlab::Template::Gitignore do + subject { described_class } describe '.all' do it 'strips the gitignore suffix' do @@ -24,7 +24,7 @@ describe Gitlab::Gitignore do it 'returns the Gitignore object of a valid file' do ruby = subject.find('Ruby') - expect(ruby).to be_a Gitlab::Gitignore + expect(ruby).to be_a Gitlab::Template::Gitignore expect(ruby.name).to eq('Ruby') end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 1e6eb20ab3..ae55a01ebe 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -401,23 +401,56 @@ describe Notify do end describe 'project access requested' do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:project_member) do - project.request_access(user) - project.members.request.find_by(user_id: user.id) + context 'for a project in a user namespace' do + let(:project) { create(:project).tap { |p| p.team << [p.owner, :master, p.owner] } } + let(:user) { create(:user) } + let(:project_member) do + project.request_access(user) + project.members.request.find_by(user_id: user.id) + end + subject { Notify.member_access_requested_email('project', project_member.id) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + + it 'contains all the useful information' do + to_emails = subject.header[:to].addrs + expect(to_emails.size).to eq(1) + expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email) + + is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" + is_expected.to have_body_text /#{project.name_with_namespace}/ + is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/ + is_expected.to have_body_text /#{project_member.human_access}/ + end end - subject { Notify.member_access_requested_email('project', project_member.id) } - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like "a user cannot unsubscribe through footer link" + context 'for a project in a group' do + let(:group_owner) { create(:user) } + let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } } + let(:project) { create(:project, namespace: group) } + let(:user) { create(:user) } + let(:project_member) do + project.request_access(user) + project.members.request.find_by(user_id: user.id) + end + subject { Notify.member_access_requested_email('project', project_member.id) } - it 'contains all the useful information' do - is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/ - is_expected.to have_body_text /#{project_member.human_access}/ + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + + it 'contains all the useful information' do + to_emails = subject.header[:to].addrs + expect(to_emails.size).to eq(1) + expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email) + + is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" + is_expected.to have_body_text /#{project.name_with_namespace}/ + is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/ + is_expected.to have_body_text /#{project_member.human_access}/ + end end end diff --git a/spec/mailers/previews/devise_mailer_preview.rb b/spec/mailers/previews/devise_mailer_preview.rb index dc3062a433..d6588efc48 100644 --- a/spec/mailers/previews/devise_mailer_preview.rb +++ b/spec/mailers/previews/devise_mailer_preview.rb @@ -1,11 +1,30 @@ class DeviseMailerPreview < ActionMailer::Preview def confirmation_instructions_for_signup - user = User.new(name: 'Jane Doe', email: 'signup@example.com') - DeviseMailer.confirmation_instructions(user, 'faketoken', {}) + DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {}) end def confirmation_instructions_for_new_email user = User.last + user.unconfirmed_email = 'unconfirmed@example.com' + DeviseMailer.confirmation_instructions(user, 'faketoken', {}) end + + def reset_password_instructions + DeviseMailer.reset_password_instructions(unsaved_user, 'faketoken', {}) + end + + def unlock_instructions + DeviseMailer.unlock_instructions(unsaved_user, 'faketoken', {}) + end + + def password_change + DeviseMailer.password_change(unsaved_user, {}) + end + + private + + def unsaved_user + User.new(name: 'Jane Doe', email: 'jdoe@example.com') + end end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 5d1fa8226e..8154001cf4 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -2,7 +2,12 @@ require 'spec_helper' describe Ci::Build, models: true do let(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project) } + + let(:pipeline) do + create(:ci_pipeline, project: project, + sha: project.commit.id) + end + let(:build) { create(:ci_build, pipeline: pipeline) } it { is_expected.to validate_presence_of :ref } @@ -36,32 +41,44 @@ describe Ci::Build, models: true do subject { build.ignored? } context 'if build is not allowed to fail' do - before { build.allow_failure = false } + before do + build.allow_failure = false + end context 'and build.status is success' do - before { build.status = 'success' } + before do + build.status = 'success' + end it { is_expected.to be_falsey } end context 'and build.status is failed' do - before { build.status = 'failed' } + before do + build.status = 'failed' + end it { is_expected.to be_falsey } end end context 'if build is allowed to fail' do - before { build.allow_failure = true } + before do + build.allow_failure = true + end context 'and build.status is success' do - before { build.status = 'success' } + before do + build.status = 'success' + end it { is_expected.to be_falsey } end context 'and build.status is failed' do - before { build.status = 'failed' } + before do + build.status = 'failed' + end it { is_expected.to be_truthy } end @@ -75,7 +92,9 @@ describe Ci::Build, models: true do context 'if build.trace contains text' do let(:text) { 'example output' } - before { build.trace = text } + before do + build.trace = text + end it { is_expected.to include(text) } it { expect(subject.length).to be >= text.length } @@ -188,7 +207,9 @@ describe Ci::Build, models: true do ] end - before { build.update_attributes(stage: 'stage') } + before do + build.update_attributes(stage: 'stage') + end it { is_expected.to eq(predefined_variables + yaml_variables) } @@ -199,7 +220,9 @@ describe Ci::Build, models: true do ] end - before { build.update_attributes(tag: true) } + before do + build.update_attributes(tag: true) + end it { is_expected.to eq(tag_variable + predefined_variables + yaml_variables) } end @@ -257,57 +280,6 @@ describe Ci::Build, models: true do end end - describe '#can_be_served?' do - let(:runner) { create(:ci_runner) } - - before { build.project.runners << runner } - - context 'when runner does not have tags' do - it 'can handle builds without tags' do - expect(build.can_be_served?(runner)).to be_truthy - end - - it 'cannot handle build with tags' do - build.tag_list = ['aa'] - expect(build.can_be_served?(runner)).to be_falsey - end - end - - context 'when runner has tags' do - before { runner.tag_list = ['bb', 'cc'] } - - shared_examples 'tagged build picker' do - it 'can handle build with matching tags' do - build.tag_list = ['bb'] - expect(build.can_be_served?(runner)).to be_truthy - end - - it 'cannot handle build without matching tags' do - build.tag_list = ['aa'] - expect(build.can_be_served?(runner)).to be_falsey - end - end - - context 'when runner can pick untagged jobs' do - it 'can handle builds without tags' do - expect(build.can_be_served?(runner)).to be_truthy - end - - it_behaves_like 'tagged build picker' - end - - context 'when runner can not pick untagged jobs' do - before { runner.run_untagged = false } - - it 'can not handle builds without tags' do - expect(build.can_be_served?(runner)).to be_falsey - end - - it_behaves_like 'tagged build picker' - end - end - end - describe '#has_tags?' do context 'when build has tags' do subject { create(:ci_build, tag_list: ['tag']) } @@ -348,7 +320,7 @@ describe Ci::Build, models: true do end it 'that cannot handle build' do - expect_any_instance_of(Ci::Build).to receive(:can_be_served?).and_return(false) + expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false) is_expected.to be_falsey end @@ -360,7 +332,9 @@ describe Ci::Build, models: true do %w(pending).each do |state| context "if commit_status.status is #{state}" do - before { build.status = state } + before do + build.status = state + end it { is_expected.to be_truthy } @@ -379,7 +353,9 @@ describe Ci::Build, models: true do %w(success failed canceled running).each do |state| context "if commit_status.status is #{state}" do - before { build.status = state } + before do + build.status = state + end it { is_expected.to be_falsey } end @@ -390,7 +366,10 @@ describe Ci::Build, models: true do subject { build.artifacts? } context 'artifacts archive does not exist' do - before { build.update_attributes(artifacts_file: nil) } + before do + build.update_attributes(artifacts_file: nil) + end + it { is_expected.to be_falsy } end @@ -623,7 +602,9 @@ describe Ci::Build, models: true do let!(:build) { create(:ci_build, :trace, :success, :artifacts) } describe '#erase' do - before { build.erase(erased_by: user) } + before do + build.erase(erased_by: user) + end context 'erased by user' do let!(:user) { create(:user, username: 'eraser') } @@ -660,7 +641,9 @@ describe Ci::Build, models: true do end context 'build has been erased' do - before { build.erase } + before do + build.erase + end it { is_expected.to be true } end @@ -668,7 +651,9 @@ describe Ci::Build, models: true do context 'metadata and build trace are not available' do let!(:build) { create(:ci_build, :success, :artifacts) } - before { build.remove_artifacts_metadata! } + before do + build.remove_artifacts_metadata! + end describe '#erase' do it 'should not raise error' do @@ -678,4 +663,10 @@ describe Ci::Build, models: true do end end end + + describe '#commit' do + it 'returns commit pipeline has been created for' do + expect(build.commit).to eq project.commit + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 0d769ed732..34507cf508 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -258,6 +258,19 @@ describe Ci::Pipeline, models: true do end end end + + context 'when no builds created' do + let(:pipeline) { build(:ci_pipeline) } + + before do + stub_ci_pipeline_yaml_file(YAML.dump(before_script: ['ls'])) + end + + it 'returns false' do + expect(pipeline.create_builds(nil)).to be_falsey + expect(pipeline).not_to be_persisted + end + end end describe "#finished_at" do diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 5d04d8ffcf..ef65eb9932 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -20,34 +20,36 @@ describe Ci::Runner, models: true do end describe '#display_name' do - it 'should return the description if it has a value' do + it 'returns the description if it has a value' do runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448') expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448' end - it 'should return the token if it does not have a description' do + it 'returns the token if it does not have a description' do runner = FactoryGirl.create(:ci_runner) expect(runner.display_name).to eq runner.description end - it 'should return the token if the description is an empty string' do + it 'returns the token if the description is an empty string' do runner = FactoryGirl.build(:ci_runner, description: '', token: 'token') expect(runner.display_name).to eq runner.token end end - describe :assign_to do + describe '#assign_to' do let!(:project) { FactoryGirl.create :empty_project } let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) } - before { shared_runner.assign_to(project) } + before do + shared_runner.assign_to(project) + end it { expect(shared_runner).to be_specific } it { expect(shared_runner.projects).to eq([project]) } it { expect(shared_runner.only_for?(project)).to be_truthy } end - describe :online do + describe '.online' do subject { Ci::Runner.online } before do @@ -58,60 +60,269 @@ describe Ci::Runner, models: true do it { is_expected.to eq([@runner2])} end - describe :online? do + describe '#online?' do let(:runner) { FactoryGirl.create(:ci_runner, :shared) } subject { runner.online? } context 'never contacted' do - before { runner.contacted_at = nil } + before do + runner.contacted_at = nil + end it { is_expected.to be_falsey } end context 'contacted long time ago time' do - before { runner.contacted_at = 1.year.ago } + before do + runner.contacted_at = 1.year.ago + end it { is_expected.to be_falsey } end context 'contacted 1s ago' do - before { runner.contacted_at = 1.second.ago } + before do + runner.contacted_at = 1.second.ago + end it { is_expected.to be_truthy } end end - describe :status do + describe '#can_pick?' do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + let(:runner) { create(:ci_runner) } + + before do + build.project.runners << runner + end + + context 'when runner does not have tags' do + it 'can handle builds without tags' do + expect(runner.can_pick?(build)).to be_truthy + end + + it 'cannot handle build with tags' do + build.tag_list = ['aa'] + + expect(runner.can_pick?(build)).to be_falsey + end + end + + context 'when runner has tags' do + before do + runner.tag_list = ['bb', 'cc'] + end + + shared_examples 'tagged build picker' do + it 'can handle build with matching tags' do + build.tag_list = ['bb'] + + expect(runner.can_pick?(build)).to be_truthy + end + + it 'cannot handle build without matching tags' do + build.tag_list = ['aa'] + + expect(runner.can_pick?(build)).to be_falsey + end + end + + context 'when runner can pick untagged jobs' do + it 'can handle builds without tags' do + expect(runner.can_pick?(build)).to be_truthy + end + + it_behaves_like 'tagged build picker' + end + + context 'when runner cannot pick untagged jobs' do + before do + runner.run_untagged = false + end + + it 'cannot handle builds without tags' do + expect(runner.can_pick?(build)).to be_falsey + end + + it_behaves_like 'tagged build picker' + end + end + + context 'when runner is locked' do + before do + runner.locked = true + end + + shared_examples 'locked build picker' do + context 'when runner cannot pick untagged jobs' do + before do + runner.run_untagged = false + end + + it 'cannot handle builds without tags' do + expect(runner.can_pick?(build)).to be_falsey + end + end + + context 'when having runner tags' do + before do + runner.tag_list = ['bb', 'cc'] + end + + it 'cannot handle it for builds without matching tags' do + build.tag_list = ['aa'] + + expect(runner.can_pick?(build)).to be_falsey + end + end + end + + context 'when serving the same project' do + it 'can handle it' do + expect(runner.can_pick?(build)).to be_truthy + end + + it_behaves_like 'locked build picker' + + context 'when having runner tags' do + before do + runner.tag_list = ['bb', 'cc'] + build.tag_list = ['bb'] + end + + it 'can handle it for matching tags' do + expect(runner.can_pick?(build)).to be_truthy + end + end + end + + context 'serving a different project' do + before do + runner.runner_projects.destroy_all + end + + it 'cannot handle it' do + expect(runner.can_pick?(build)).to be_falsey + end + + it_behaves_like 'locked build picker' + + context 'when having runner tags' do + before do + runner.tag_list = ['bb', 'cc'] + build.tag_list = ['bb'] + end + + it 'cannot handle it for matching tags' do + expect(runner.can_pick?(build)).to be_falsey + end + end + end + end + end + + describe '#status' do let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) } subject { runner.status } context 'never connected' do - before { runner.contacted_at = nil } + before do + runner.contacted_at = nil + end it { is_expected.to eq(:not_connected) } end context 'contacted 1s ago' do - before { runner.contacted_at = 1.second.ago } + before do + runner.contacted_at = 1.second.ago + end it { is_expected.to eq(:online) } end context 'contacted long time ago' do - before { runner.contacted_at = 1.year.ago } + before do + runner.contacted_at = 1.year.ago + end it { is_expected.to eq(:offline) } end context 'inactive' do - before { runner.active = false } + before do + runner.active = false + end it { is_expected.to eq(:paused) } end end + describe '.assignable_for' do + let(:runner) { create(:ci_runner) } + let(:project) { create(:project) } + let(:another_project) { create(:project) } + + before do + project.runners << runner + end + + context 'with shared runners' do + before do + runner.update(is_shared: true) + end + + context 'does not give owned runner' do + subject { Ci::Runner.assignable_for(project) } + + it { is_expected.to be_empty } + end + + context 'does not give shared runner' do + subject { Ci::Runner.assignable_for(another_project) } + + it { is_expected.to be_empty } + end + end + + context 'with unlocked runner' do + context 'does not give owned runner' do + subject { Ci::Runner.assignable_for(project) } + + it { is_expected.to be_empty } + end + + context 'does give a specific runner' do + subject { Ci::Runner.assignable_for(another_project) } + + it { is_expected.to contain_exactly(runner) } + end + end + + context 'with locked runner' do + before do + runner.update(locked: true) + end + + context 'does not give owned runner' do + subject { Ci::Runner.assignable_for(project) } + + it { is_expected.to be_empty } + end + + context 'does not give a locked runner' do + subject { Ci::Runner.assignable_for(another_project) } + + it { is_expected.to be_empty } + end + end + end + describe "belongs_to_one_project?" do it "returns false if there are two projects runner assigned to" do runner = FactoryGirl.create(:ci_runner) diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index beca8708c9..ba02d5fe97 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -207,4 +207,16 @@ eos expect(commit.participants).to include(note1.author, note2.author) end end + + describe '#uri_type' do + it 'returns the URI type at the given path' do + expect(commit.uri_type('files/html')).to be(:tree) + expect(commit.uri_type('files/images/logo-black.png')).to be(:raw) + expect(commit.uri_type('files/js/application.js')).to be(:blob) + end + + it "returns nil if the path doesn't exists" do + expect(commit.uri_type('this/path/doesnt/exist')).to be_nil + end + end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 8fb605fff8..96397d7c8a 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -1,8 +1,13 @@ require 'spec_helper' describe CommitStatus, models: true do - let(:pipeline) { FactoryGirl.create :ci_pipeline } - let(:commit_status) { FactoryGirl.create :commit_status, pipeline: pipeline } + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, project: project, sha: project.commit.id) + end + + let(:commit_status) { create(:commit_status, pipeline: pipeline) } it { is_expected.to belong_to(:pipeline) } it { is_expected.to belong_to(:user) } @@ -13,7 +18,7 @@ describe CommitStatus, models: true do it { is_expected.to delegate_method(:sha).to(:pipeline) } it { is_expected.to delegate_method(:short_sha).to(:pipeline) } - + it { is_expected.to respond_to :success? } it { is_expected.to respond_to :failed? } it { is_expected.to respond_to :running? } @@ -116,7 +121,7 @@ describe CommitStatus, models: true do it { is_expected.to be > 0.0 } end end - + describe :latest do subject { CommitStatus.latest.order(:id) } @@ -198,4 +203,10 @@ describe CommitStatus, models: true do end end end + + describe '#commit' do + it 'returns commit pipeline has been created for' do + expect(commit_status.commit).to eq project.commit + end + end end diff --git a/spec/models/concerns/participable_spec.rb b/spec/models/concerns/participable_spec.rb index 7e4ea0f2d6..a9f4ef9ee5 100644 --- a/spec/models/concerns/participable_spec.rb +++ b/spec/models/concerns/participable_spec.rb @@ -37,6 +37,16 @@ describe Participable, models: true do expect(participants).to include(user3) end + it 'caches the raw list of participants' do + instance = model.new + user1 = build(:user) + + expect(instance).to receive(:raw_participants).once + + instance.participants(user1) + instance.participants(user1) + end + it 'supports attributes returning another Participable' do other_model = Class.new { include Participable } diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index ccdcb29f77..2c19aa3f67 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -158,6 +158,18 @@ describe Group, models: true do it { expect(group.has_master?(@members[:requester])).to be_falsey } end + describe '#owners' do + let(:owner) { create(:user) } + let(:developer) { create(:user) } + + it 'returns the owners of a Group' do + group.add_owner(owner) + group.add_developer(developer) + + expect(group.owners).to eq([owner]) + end + end + def setup_group_members(group) members = { owner: create(:user), diff --git a/spec/models/jira_issue_spec.rb b/spec/models/jira_issue_spec.rb deleted file mode 100644 index 1634265b43..0000000000 --- a/spec/models/jira_issue_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' - -describe JiraIssue do - let(:project) { create(:project) } - subject { JiraIssue.new('JIRA-123', project) } - - describe 'id' do - subject { super().id } - it { is_expected.to eq('JIRA-123') } - end - - describe 'iid' do - subject { super().iid } - it { is_expected.to eq('JIRA-123') } - end - - describe 'to_s' do - subject { super().to_s } - it { is_expected.to eq('JIRA-123') } - end - - describe :== do - specify { expect(subject).to eq(JiraIssue.new('JIRA-123', project)) } - specify { expect(subject).not_to eq(JiraIssue.new('JIRA-124', project)) } - - it 'only compares with JiraIssues' do - expect(subject).not_to eq('JIRA-123') - end - end -end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 26fbedbef2..49cf3d8633 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -26,7 +26,7 @@ describe Key, models: true do end end - context "validation of uniqueness" do + context "validation of uniqueness (based on fingerprint uniqueness)" do let(:user) { create(:user) } it "accepts the key once" do diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb index 4e24e89b00..df336a6eff 100644 --- a/spec/models/notification_setting_spec.rb +++ b/spec/models/notification_setting_spec.rb @@ -12,5 +12,30 @@ RSpec.describe NotificationSetting, type: :model do it { is_expected.to validate_presence_of(:user) } 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/) } + + context "events" do + let(:user) { create(:user) } + let(:notification_setting) { NotificationSetting.new(source_id: 1, source_type: 'Project', user_id: user.id) } + + before do + notification_setting.level = "custom" + notification_setting.new_note = "true" + notification_setting.new_issue = 1 + notification_setting.close_issue = "1" + notification_setting.merge_merge_request = "t" + notification_setting.close_merge_request = "nil" + notification_setting.reopen_merge_request = "false" + notification_setting.save + end + + it "parses boolean before saving" do + expect(notification_setting.new_note).to eq(true) + expect(notification_setting.new_issue).to eq(true) + expect(notification_setting.close_issue).to eq(true) + expect(notification_setting.merge_merge_request).to eq(true) + expect(notification_setting.close_merge_request).to eq(false) + expect(notification_setting.reopen_merge_request).to eq(false) + end + end end end diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb new file mode 100644 index 0000000000..46eb71cef1 --- /dev/null +++ b/spec/models/personal_access_token_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe PersonalAccessToken, models: true do + describe ".generate" do + it "generates a random token" do + personal_access_token = PersonalAccessToken.generate({}) + expect(personal_access_token.token).to be_present + end + + it "doesn't save the record" do + personal_access_token = PersonalAccessToken.generate({}) + expect(personal_access_token).not_to be_persisted + end + end +end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 5309cfb99f..c951732454 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -76,7 +76,8 @@ describe JiraService, models: true do end it "should call JIRA API" do - @jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project)) + @jira_service.execute(merge_request, + ExternalIssue.new("JIRA-123", project)) expect(WebMock).to have_requested(:post, @comment_url).with( body: /Issue solved with/ ).once @@ -84,7 +85,8 @@ describe JiraService, models: true do it "calls the api with jira_issue_transition_id" do @jira_service.jira_issue_transition_id = 'this-is-a-custom-id' - @jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project)) + @jira_service.execute(merge_request, + ExternalIssue.new("JIRA-123", project)) expect(WebMock).to have_requested(:post, @api_url).with( body: /this-is-a-custom-id/ ).once diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index fedab1f913..53c8408633 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -220,7 +220,7 @@ describe Project, models: true do end end - describe :find_with_namespace do + describe '.find_with_namespace' do context 'with namespace' do before do @group = create :group, name: 'gitlab' @@ -231,6 +231,22 @@ describe Project, models: true do it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) } it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil } end + + context 'when multiple projects using a similar name exist' do + let(:group) { create(:group, name: 'gitlab') } + + let!(:project1) do + create(:empty_project, name: 'gitlab1', path: 'gitlab', namespace: group) + end + + let!(:project2) do + create(:empty_project, name: 'gitlab2', path: 'GITLAB', namespace: group) + end + + it 'returns the row where the path matches literally' do + expect(Project.find_with_namespace('gitlab/GITLAB')).to eq(project2) + end + end end describe :to_param do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 8c2347992f..d8350000bf 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -31,6 +31,47 @@ describe Repository, models: true do it { is_expected.not_to include('v1.0.0') } end + describe 'tags_sorted_by' do + context 'name' do + subject { repository.tags_sorted_by('name').map(&:name) } + + it { is_expected.to eq(['v1.1.0', 'v1.0.0']) } + end + + context 'updated' do + let(:tag_a) { repository.find_tag('v1.0.0') } + let(:tag_b) { repository.find_tag('v1.1.0') } + + context 'desc' do + subject { repository.tags_sorted_by('updated_desc').map(&:name) } + + before do + double_first = double(committed_date: Time.now) + double_last = double(committed_date: Time.now - 1.second) + + allow(repository).to receive(:commit).with(tag_a.target).and_return(double_first) + allow(repository).to receive(:commit).with(tag_b.target).and_return(double_last) + end + + it { is_expected.to eq(['v1.0.0', 'v1.1.0']) } + end + + context 'asc' do + subject { repository.tags_sorted_by('updated_asc').map(&:name) } + + before do + double_first = double(committed_date: Time.now - 1.second) + double_last = double(committed_date: Time.now) + + allow(repository).to receive(:commit).with(tag_a.target).and_return(double_last) + allow(repository).to receive(:commit).with(tag_b.target).and_return(double_first) + end + + it { is_expected.to eq(['v1.1.0', 'v1.0.0']) } + end + end + end + describe :last_commit_for_path do subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index 0c19094ec5..f22db61e74 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' -describe API, api: true do +describe API::Helpers, api: true do + include API::Helpers include ApiHelpers + let(:user) { create(:user) } let(:admin) { create(:admin) } let(:key) { create(:key, user: user) } @@ -39,24 +41,64 @@ describe API, api: true do end describe ".current_user" do - it "should return nil for an invalid token" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } - expect(current_user).to be_nil + describe "when authenticating using a user's private token" do + it "should return nil for an invalid token" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil + end + + it "should return nil for a user without access" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + expect(current_user).to be_nil + end + + it "should leave user as is when sudo not specified" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + expect(current_user).to eq(user) + clear_env + params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token + expect(current_user).to eq(user) + end end - it "should return nil for a user without access" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token - allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false) - expect(current_user).to be_nil - end + describe "when authenticating using a user's personal access tokens" do + let(:personal_access_token) { create(:personal_access_token, user: user) } - it "should leave user as is when sudo not specified" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token - expect(current_user).to eq(user) - clear_env - params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token - expect(current_user).to eq(user) + it "should return nil for an invalid token" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil + end + + it "should return nil for a user without access" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + expect(current_user).to be_nil + end + + it "should leave user as is when sudo not specified" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + expect(current_user).to eq(user) + clear_env + params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token + expect(current_user).to eq(user) + end + + it 'does not allow revoked tokens' do + personal_access_token.revoke! + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil + end + + it 'does not allow expired tokens' do + personal_access_token.update_attributes!(expires_at: 1.day.ago) + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil + end end it "should change current user to sudo when admin" do diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb new file mode 100644 index 0000000000..2e65e7f192 --- /dev/null +++ b/spec/requests/api/award_emoji_spec.rb @@ -0,0 +1,198 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + let(:user) { create(:user) } + let!(:project) { create(:project) } + let(:issue) { create(:issue, project: project, author: user) } + let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } + let!(:note) { create(:note, project: project, noteable: issue) } + + before { project.team << [user, :master] } + + describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do + context 'on an issue' do + it "returns an array of award_emoji" do + get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(award_emoji.name) + end + + it "should return a 404 error when issue id not found" do + get api("/projects/#{project.id}/issues/12345/award_emoji", user) + + expect(response.status).to eq(404) + end + end + + context 'on a merge request' do + it "returns an array of award_emoji" do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(downvote.name) + end + end + + context 'when the user has no access' do + it 'returns a status code 404' do + user1 = create(:user) + + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1) + + expect(response.status).to eq(404) + end + end + end + + describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do + let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } + + it 'returns an array of award emoji' do + get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(rocket.name) + end + end + + + describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do + context 'on an issue' do + it "returns the award emoji" do + get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(award_emoji.name) + expect(json_response['awardable_id']).to eq(issue.id) + expect(json_response['awardable_type']).to eq("Issue") + end + + it "returns a 404 error if the award is not found" do + get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) + + expect(response.status).to eq(404) + end + end + + context 'on a merge request' do + it 'returns the award emoji' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(downvote.name) + expect(json_response['awardable_id']).to eq(merge_request.id) + expect(json_response['awardable_type']).to eq("MergeRequest") + end + end + + context 'when the user has no access' do + it 'returns a status code 404' do + user1 = create(:user) + + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1) + + expect(response.status).to eq(404) + end + end + end + + describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do + let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } + + it 'returns an award emoji' do + get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) + + expect(response.status).to eq(200) + expect(json_response).not_to be_an Array + expect(json_response['name']).to eq(rocket.name) + end + end + + describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do + context "on an issue" do + it "creates a new award emoji" do + post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish' + + expect(response.status).to eq(201) + expect(json_response['name']).to eq('blowfish') + expect(json_response['user']['username']).to eq(user.username) + end + + it "should return a 400 bad request error if the name is not given" do + post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) + + expect(response.status).to eq(400) + end + + it "should return a 401 unauthorized error if the user is not authenticated" do + post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup' + + expect(response.status).to eq(401) + end + end + end + + describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do + it 'creates a new award emoji' do + expect do + post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + end.to change { note.award_emoji.count }.from(0).to(1) + + expect(response.status).to eq(201) + expect(json_response['user']['username']).to eq(user.username) + end + end + + describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do + context 'when the awardable is an Issue' do + it 'deletes the award' do + expect do + delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) + end.to change { issue.award_emoji.count }.from(1).to(0) + + expect(response.status).to eq(200) + end + + it 'returns a 404 error when the award emoji can not be found' do + delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) + + expect(response.status).to eq(404) + end + end + + context 'when the awardable is a Merge Request' do + it 'deletes the award' do + expect do + delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) + end.to change { merge_request.award_emoji.count }.from(1).to(0) + + expect(response.status).to eq(200) + end + + it 'returns a 404 error when note id not found' do + delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user) + + expect(response.status).to eq(404) + end + end + end + + describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do + let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket', user: user) } + + it 'deletes the award' do + expect do + delete api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) + end.to change { note.award_emoji.count }.from(1).to(0) + + expect(response.status).to eq(200) + end + end +end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index ac85f34092..47e9253a10 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -9,8 +9,8 @@ describe API::API, api: true do let!(:project) { create(:project, creator_id: user.id) } let!(:developer) { create(:project_member, :developer, user: user, project: project) } let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) } - let(:pipeline) { create(:ci_pipeline, project: project)} - let(:build) { create(:ci_build, pipeline: pipeline) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) } + let!(:build) { create(:ci_build, pipeline: pipeline) } describe 'GET /projects/:id/builds ' do let(:query) { '' } @@ -23,6 +23,11 @@ describe API::API, api: true do expect(json_response).to be_an Array end + it 'returns correct values' do + expect(json_response).not_to be_empty + expect(json_response.first['commit']['id']).to eq project.commit.id + end + context 'filter project with one scope element' do let(:query) { 'scope=pending' } @@ -132,7 +137,7 @@ describe API::API, api: true do describe 'GET /projects/:id/builds/:build_id/trace' do let(:build) { create(:ci_build, :trace, pipeline: pipeline) } - + before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) } context 'authorized user' do diff --git a/spec/requests/api/gitignores_spec.rb b/spec/requests/api/gitignores_spec.rb deleted file mode 100644 index aab2d8c81b..0000000000 --- a/spec/requests/api/gitignores_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'spec_helper' - -describe API::Gitignores, api: true do - include ApiHelpers - - describe 'Entity Gitignore' do - before { get api('/gitignores/Ruby') } - - it { expect(json_response['name']).to eq('Ruby') } - it { expect(json_response['content']).to include('*.gem') } - end - - describe 'Entity GitignoresList' do - before { get api('/gitignores') } - - it { expect(json_response.first['name']).not_to be_nil } - it { expect(json_response.first['content']).to be_nil } - end - - describe 'GET /gitignores' do - it 'returns a list of available license templates' do - get api('/gitignores') - - expect(response.status).to eq(200) - expect(json_response).to be_an Array - expect(json_response.size).to be > 15 - end - end -end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index f167813e07..01eb4b44b8 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -428,8 +428,9 @@ describe API::API, api: true do describe 'permissions' do context 'all projects' do - it 'Contains permission information' do - project.team << [user, :master] + before { project.team << [user, :master] } + + it 'contains permission information' do get api("/projects", user) expect(response.status).to eq(200) @@ -440,7 +441,7 @@ describe API::API, api: true do end context 'personal project' do - it 'Sets project access and returns 200' do + it 'sets project access and returns 200' do project.team << [user, :master] get api("/projects/#{project.id}", user) @@ -452,9 +453,11 @@ describe API::API, api: true do end context 'group project' do + let(:project2) { create(:project, group: create(:group)) } + + before { project2.group.add_owner(user) } + it 'should set the owner and return 200' do - project2 = create(:project, group: create(:group)) - project2.group.add_owner(user) get api("/projects/#{project2.id}", user) expect(response.status).to eq(200) diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 73ae8ef631..b4c826522a 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -187,14 +187,16 @@ describe API::Runners, api: true do update_runner(shared_runner.id, admin, description: "#{description}_updated", active: !active, tag_list: ['ruby2.1', 'pgsql', 'mysql'], - run_untagged: 'false') + run_untagged: 'false', + locked: 'true') shared_runner.reload expect(response.status).to eq(200) expect(shared_runner.description).to eq("#{description}_updated") expect(shared_runner.active).to eq(!active) expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') - expect(shared_runner.run_untagged?).to be false + expect(shared_runner.run_untagged?).to be(false) + expect(shared_runner.locked?).to be(true) end end @@ -360,11 +362,13 @@ describe API::Runners, api: true do describe 'POST /projects/:id/runners' do context 'authorized user' do - it 'should enable specific runner' do - specific_runner2 = create(:ci_runner).tap do |runner| + let(:specific_runner2) do + create(:ci_runner).tap do |runner| create(:ci_runner_project, runner: runner, project: project2) end + end + it 'should enable specific runner' do expect do post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id end.to change{ project.runners.count }.by(+1) @@ -375,7 +379,17 @@ describe API::Runners, api: true do expect do post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id end.to change{ project.runners.count }.by(0) - expect(response.status).to eq(201) + expect(response.status).to eq(409) + end + + it 'should not enable locked runner' do + specific_runner2.update(locked: true) + + expect do + post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id + end.to change{ project.runners.count }.by(0) + + expect(response.status).to eq(403) end it 'should not enable shared runner' do diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb new file mode 100644 index 0000000000..41cbf0c666 --- /dev/null +++ b/spec/requests/api/sidekiq_metrics_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe API::SidekiqMetrics, api: true do + include ApiHelpers + + let(:admin) { create(:user, :admin) } + + describe 'GET sidekiq/*' do + it 'defines the `queue_metrics` endpoint' do + get api('/sidekiq/queue_metrics', admin) + + expect(response.status).to eq(200) + expect(json_response).to be_a Hash + end + + it 'defines the `process_metrics` endpoint' do + get api('/sidekiq/process_metrics', admin) + + expect(response.status).to eq(200) + expect(json_response['processes']).to be_an Array + end + + it 'defines the `job_stats` endpoint' do + get api('/sidekiq/job_stats', admin) + + expect(response.status).to eq(200) + expect(json_response).to be_a Hash + end + + it 'defines the `compound_metrics` endpoint' do + get api('/sidekiq/compound_metrics', admin) + + expect(response.status).to eq(200) + expect(json_response).to be_a Hash + expect(json_response['queues']).to be_a Hash + expect(json_response['processes']).to be_an Array + expect(json_response['jobs']).to be_a Hash + end + end +end diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb new file mode 100644 index 0000000000..a6d5ade301 --- /dev/null +++ b/spec/requests/api/templates_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe API::Templates, api: true do + include ApiHelpers + + describe 'the Template Entity' do + before { get api('/gitignores/Ruby') } + + it { expect(json_response['name']).to eq('Ruby') } + it { expect(json_response['content']).to include('*.gem') } + end + + describe 'the TemplateList Entity' do + before { get api('/gitignores') } + + it { expect(json_response.first['name']).not_to be_nil } + it { expect(json_response.first['content']).to be_nil } + end + + context 'requesting gitignores' do + describe 'GET /gitignores' do + it 'returns a list of available gitignore templates' do + get api('/gitignores') + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to be > 15 + end + end + end + + context 'requesting gitlab-ci-ymls' do + describe 'GET /gitlab_ci_ymls' do + it 'returns a list of available gitlab_ci_ymls' do + get api('/gitlab_ci_ymls') + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).not_to be_nil + end + end + end + + describe 'GET /gitlab_ci_ymls/Ruby' do + it 'adds a disclaimer on the top' do + get api('/gitlab_ci_ymls/Ruby') + + expect(response.status).to eq(200) + expect(json_response['content']).to start_with("# This file is a template,") + end + end +end diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb index 984b78487d..8b0becd83d 100644 --- a/spec/services/ci/create_builds_service_spec.rb +++ b/spec/services/ci/create_builds_service_spec.rb @@ -9,7 +9,7 @@ describe Ci::CreateBuildsService, services: true do # subject do - described_class.new(pipeline).execute('test', nil, user, status) + described_class.new(pipeline).execute('test', user, status, nil) end context 'next builds available' do @@ -17,6 +17,10 @@ describe Ci::CreateBuildsService, services: true do it { is_expected.to be_an_instance_of Array } it { is_expected.to all(be_an_instance_of Ci::Build) } + + it 'does not persist created builds' do + expect(subject.first).not_to be_persisted + end end context 'builds skipped' do diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index d91fc57429..f28f2f1438 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -45,11 +45,73 @@ module Ci end end + context 'deleted projects' do + before do + project.update(pending_delete: true) + end + + context 'for shared runners' do + before do + project.update(shared_runners_enabled: true) + end + + it 'does not pick a build' do + expect(service.execute(shared_runner)).to be_nil + end + end + + context 'for specific runner' do + it 'does not pick a build' do + expect(service.execute(specific_runner)).to be_nil + end + end + end + context 'allow shared runners' do before do project.update(shared_runners_enabled: true) end + context 'for multiple builds' do + let!(:project2) { create :empty_project, shared_runners_enabled: true } + let!(:pipeline2) { create :ci_pipeline, project: project2 } + let!(:project3) { create :empty_project, shared_runners_enabled: true } + let!(:pipeline3) { create :ci_pipeline, project: project3 } + let!(:build1_project1) { pending_build } + let!(:build2_project1) { FactoryGirl.create :ci_build, pipeline: pipeline } + let!(:build3_project1) { FactoryGirl.create :ci_build, pipeline: pipeline } + let!(:build1_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 } + let!(:build2_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 } + let!(:build1_project3) { FactoryGirl.create :ci_build, pipeline: pipeline3 } + + it 'prefers projects without builds first' do + # it gets for one build from each of the projects + expect(service.execute(shared_runner)).to eq(build1_project1) + expect(service.execute(shared_runner)).to eq(build1_project2) + expect(service.execute(shared_runner)).to eq(build1_project3) + + # then it gets a second build from each of the projects + expect(service.execute(shared_runner)).to eq(build2_project1) + expect(service.execute(shared_runner)).to eq(build2_project2) + + # in the end the third build + expect(service.execute(shared_runner)).to eq(build3_project1) + end + + it 'equalises number of running builds' do + # after finishing the first build for project 1, get a second build from the same project + expect(service.execute(shared_runner)).to eq(build1_project1) + build1_project1.success + expect(service.execute(shared_runner)).to eq(build2_project1) + + expect(service.execute(shared_runner)).to eq(build1_project2) + build1_project2.success + expect(service.execute(shared_runner)).to eq(build2_project2) + expect(service.execute(shared_runner)).to eq(build1_project3) + expect(service.execute(shared_runner)).to eq(build3_project1) + end + end + context 'shared runner' do let(:build) { service.execute(shared_runner) } diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index a5b4d9f05d..deab242f45 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -39,7 +39,7 @@ describe CreateCommitBuildsService, services: true do end it "creates commit if there is no appropriate job but deploy job has right ref setting" do - config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } }) + config = YAML.dump({ deploy: { script: "ls", only: ["0_1"] } }) stub_ci_pipeline_yaml_file(config) result = service.execute(project, user, @@ -81,7 +81,7 @@ describe CreateCommitBuildsService, services: true do expect(pipeline.yaml_errors).not_to be_nil end - describe :ci_skip? do + context 'when commit contains a [ci skip] directive' do let(:message) { "some message[ci skip]" } before do @@ -171,5 +171,24 @@ describe CreateCommitBuildsService, services: true do expect(pipeline.status).to eq("failed") expect(pipeline.builds.any?).to be false end + + context 'when there are no jobs for this pipeline' do + before do + config = YAML.dump({ test: { script: 'ls', only: ['feature'] } }) + stub_ci_pipeline_yaml_file(config) + end + + it 'does not create a new pipeline' do + result = service.execute(project, user, + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: [{ message: 'some msg' }]) + + expect(result).to be_falsey + expect(Ci::Build.all).to be_empty + expect(Ci::Pipeline.count).to eq(0) + end + end end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 18692f1279..f99ad046f0 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -312,7 +312,8 @@ describe GitPushService, services: true do end it "doesn't close issues when external issue tracker is in use" do - allow(project).to receive(:default_issues_tracker?).and_return(false) + allow_any_instance_of(Project).to receive(:default_issues_tracker?). + and_return(false) # The push still shouldn't create cross-reference notes. expect do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index e871a103d4..776a6ab5ed 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -46,6 +46,8 @@ describe NotificationService, services: true do project.team << [issue.assignee, :master] project.team << [note.author, :master] create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy') + update_custom_notification(:new_note, @u_guest_custom, project) + update_custom_notification(:new_note, @u_custom_global) end describe :new_note do @@ -53,7 +55,7 @@ describe NotificationService, services: true do add_users_with_subscription(note.project, issue) # Ensure create SentNotification by noteable = issue 6 times, not noteable = note - expect(SentNotification).to receive(:record).with(issue, any_args).exactly(7).times + expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times ActionMailer::Base.deliveries.clear @@ -62,10 +64,12 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(note.noteable.author) should_email(note.noteable.assignee) + should_email(@u_custom_global) should_email(@u_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) should_email(@subscribed_participant) + should_not_email(@u_guest_custom) should_not_email(@u_guest_watcher) should_not_email(note.author) should_not_email(@u_participating) @@ -103,10 +107,12 @@ describe NotificationService, services: true do before do note.project.namespace_id = group.id note.project.group.add_user(@u_watcher, GroupMember::MASTER) + note.project.group.add_user(@u_custom_global, GroupMember::MASTER) note.project.save @u_watcher.notification_settings_for(note.project).participating! @u_watcher.notification_settings_for(note.project.group).global! + update_custom_notification(:new_note, @u_custom_global) ActionMailer::Base.deliveries.clear end @@ -116,6 +122,8 @@ describe NotificationService, services: true do should_email(note.noteable.author) should_email(note.noteable.assignee) should_email(@u_mentioned) + should_email(@u_custom_global) + should_not_email(@u_guest_custom) should_not_email(@u_guest_watcher) should_not_email(@u_watcher) should_not_email(note.author) @@ -136,6 +144,7 @@ describe NotificationService, services: true do let(:admin) { create(:admin) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) } let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") } + let(:guest_watcher) { create_user_with_notification(:watch, "guest-watcher-confidential") } it 'filters out users that can not read the issue' do project.team << [member, :developer] @@ -149,6 +158,7 @@ describe NotificationService, services: true do should_not_email(non_member) should_not_email(guest) + should_not_email(guest_watcher) should_email(author) should_email(assignee) should_email(member) @@ -223,6 +233,9 @@ describe NotificationService, services: true do should_email(member) end + # it emails custom global users on mention + should_email(@u_custom_global) + should_email(@u_guest_watcher) should_email(note.noteable.author) should_not_email(note.author) @@ -241,13 +254,16 @@ describe NotificationService, services: true do build_team(note.project) ActionMailer::Base.deliveries.clear allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer) + update_custom_notification(:new_note, @u_guest_custom, project) + update_custom_notification(:new_note, @u_custom_global) end describe '#new_note, #perform_enqueued_jobs' do it do notification.new_note(note) - should_email(@u_guest_watcher) + should_email(@u_custom_global) + should_email(@u_guest_custom) should_email(@u_committer) should_email(@u_watcher) should_not_email(@u_mentioned) @@ -288,6 +304,8 @@ describe NotificationService, services: true do build_team(issue.project) add_users_with_subscription(issue.project, issue) ActionMailer::Base.deliveries.clear + update_custom_notification(:new_issue, @u_guest_custom, project) + update_custom_notification(:new_issue, @u_custom_global) end describe '#new_issue' do @@ -297,6 +315,8 @@ describe NotificationService, services: true do should_email(issue.assignee) should_email(@u_watcher) should_email(@u_guest_watcher) + should_email(@u_guest_custom) + should_email(@u_custom_global) should_email(@u_participant_mentioned) should_not_email(@u_mentioned) should_not_email(@u_participating) @@ -345,6 +365,7 @@ describe NotificationService, services: true do notification.new_issue(confidential_issue, @u_disabled) + should_not_email(@u_guest_watcher) should_not_email(non_member) should_not_email(author) should_not_email(guest) @@ -356,12 +377,20 @@ describe NotificationService, services: true do end describe '#reassigned_issue' do + + before do + update_custom_notification(:reassign_issue, @u_guest_custom, project) + update_custom_notification(:reassign_issue, @u_custom_global) + end + it 'emails new assignee' do notification.reassigned_issue(issue, @u_disabled) should_email(issue.assignee) should_email(@u_watcher) should_email(@u_guest_watcher) + should_email(@u_guest_custom) + should_email(@u_custom_global) should_email(@u_participant_mentioned) should_email(@subscriber) should_not_email(@unsubscriber) @@ -378,8 +407,10 @@ describe NotificationService, services: true do should_email(@u_mentioned) should_email(@u_watcher) should_email(@u_guest_watcher) + should_email(@u_guest_custom) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@u_custom_global) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -394,8 +425,10 @@ describe NotificationService, services: true do should_email(issue.assignee) should_email(@u_watcher) should_email(@u_guest_watcher) + should_email(@u_guest_custom) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@u_custom_global) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -410,8 +443,10 @@ describe NotificationService, services: true do should_email(issue.assignee) should_email(@u_watcher) should_email(@u_guest_watcher) + should_email(@u_guest_custom) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@u_custom_global) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -425,8 +460,10 @@ describe NotificationService, services: true do expect(issue.assignee).to be @u_mentioned should_email(@u_watcher) should_email(@u_guest_watcher) + should_email(@u_guest_custom) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@u_custom_global) should_not_email(issue.assignee) should_not_email(@unsubscriber) should_not_email(@u_participating) @@ -529,6 +566,12 @@ describe NotificationService, services: true do end describe '#close_issue' do + + before do + update_custom_notification(:close_issue, @u_guest_custom, project) + update_custom_notification(:close_issue, @u_custom_global) + end + it 'should sent email to issue assignee and issue author' do notification.close_issue(issue, @u_disabled) @@ -536,6 +579,8 @@ describe NotificationService, services: true do should_email(issue.author) should_email(@u_watcher) should_email(@u_guest_watcher) + should_email(@u_guest_custom) + should_email(@u_custom_global) should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) @@ -575,6 +620,11 @@ describe NotificationService, services: true do end describe '#reopen_issue' do + before do + update_custom_notification(:reopen_issue, @u_guest_custom, project) + update_custom_notification(:reopen_issue, @u_custom_global) + end + it 'should send email to issue assignee and issue author' do notification.reopen_issue(issue, @u_disabled) @@ -582,6 +632,8 @@ describe NotificationService, services: true do should_email(issue.author) should_email(@u_watcher) should_email(@u_guest_watcher) + should_email(@u_guest_custom) + should_email(@u_custom_global) should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) @@ -631,6 +683,11 @@ describe NotificationService, services: true do end describe '#new_merge_request' do + before do + update_custom_notification(:new_merge_request, @u_guest_custom, project) + update_custom_notification(:new_merge_request, @u_custom_global) + end + it do notification.new_merge_request(merge_request, @u_disabled) @@ -639,6 +696,8 @@ describe NotificationService, services: true do should_email(@watcher_and_subscriber) should_email(@u_participant_mentioned) should_email(@u_guest_watcher) + should_email(@u_guest_custom) + should_email(@u_custom_global) should_not_email(@u_participating) should_not_email(@u_disabled) should_not_email(@u_lazy_participant) @@ -685,6 +744,11 @@ describe NotificationService, services: true do end describe '#reassigned_merge_request' do + before do + update_custom_notification(:reassign_merge_request, @u_guest_custom, project) + update_custom_notification(:reassign_merge_request, @u_custom_global) + end + it do notification.reassigned_merge_request(merge_request, merge_request.author) @@ -694,6 +758,8 @@ describe NotificationService, services: true do should_email(@subscriber) should_email(@watcher_and_subscriber) should_email(@u_guest_watcher) + should_email(@u_guest_custom) + should_email(@u_custom_global) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -761,12 +827,19 @@ describe NotificationService, services: true do end describe '#closed_merge_request' do + before do + update_custom_notification(:close_merge_request, @u_guest_custom, project) + update_custom_notification(:close_merge_request, @u_custom_global) + end + it do notification.close_mr(merge_request, @u_disabled) should_email(merge_request.assignee) should_email(@u_watcher) should_email(@u_guest_watcher) + should_email(@u_guest_custom) + should_email(@u_custom_global) should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) @@ -807,6 +880,12 @@ describe NotificationService, services: true do end describe '#merged_merge_request' do + + before do + update_custom_notification(:merge_merge_request, @u_guest_custom, project) + update_custom_notification(:merge_merge_request, @u_custom_global) + end + it do notification.merge_mr(merge_request, @u_disabled) @@ -816,6 +895,8 @@ describe NotificationService, services: true do should_email(@subscriber) should_email(@watcher_and_subscriber) should_email(@u_guest_watcher) + should_email(@u_custom_global) + should_email(@u_guest_custom) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -853,6 +934,11 @@ describe NotificationService, services: true do end describe '#reopen_merge_request' do + before do + update_custom_notification(:reopen_merge_request, @u_guest_custom, project) + update_custom_notification(:reopen_merge_request, @u_custom_global) + end + it do notification.reopen_mr(merge_request, @u_disabled) @@ -862,6 +948,8 @@ describe NotificationService, services: true do should_email(@subscriber) should_email(@watcher_and_subscriber) should_email(@u_guest_watcher) + should_email(@u_guest_custom) + should_email(@u_custom_global) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -914,7 +1002,9 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participating) should_email(@u_lazy_participant) + should_email(@u_custom_global) should_not_email(@u_guest_watcher) + should_not_email(@u_guest_custom) should_not_email(@u_disabled) end end @@ -929,13 +1019,15 @@ describe NotificationService, services: true do @u_committer = create(:user, username: 'committer') @u_not_mentioned = create_global_setting_for(create(:user, username: 'regular'), :participating) @u_outsider_mentioned = create(:user, username: 'outsider') + @u_custom_global = create_global_setting_for(create(:user, username: 'custom_global'), :custom) # User to be participant by default # This user does not contain any record in notification settings table # It should be treated with a :participating notification_level @u_lazy_participant = create(:user, username: 'lazy-participant') - create_guest_watcher + @u_guest_watcher = create_user_with_notification(:watch, 'guest_watching') + @u_guest_custom = create_user_with_notification(:custom, 'guest_custom') project.team << [@u_watcher, :master] project.team << [@u_participating, :master] @@ -945,6 +1037,7 @@ describe NotificationService, services: true do project.team << [@u_committer, :master] project.team << [@u_not_mentioned, :master] project.team << [@u_lazy_participant, :master] + project.team << [@u_custom_global, :master] end def create_global_setting_for(user, level) @@ -955,10 +1048,20 @@ describe NotificationService, services: true do user end - def create_guest_watcher - @u_guest_watcher = create(:user, username: 'guest_watching') - setting = @u_guest_watcher.notification_settings_for(project) - setting.level = :watch + def create_user_with_notification(level, username) + user = create(:user, username: username) + setting = user.notification_settings_for(project) + setting.level = level + setting.save + + user + end + + # Create custom notifications + # When resource is nil it means global notification + def update_custom_notification(event, user, resource = nil) + setting = user.notification_settings_for(resource) + setting.events[event] = true setting.save end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 09f0ee3871..85dd30bf48 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -529,7 +529,7 @@ describe SystemNoteService, services: true do let(:author) { create(:user) } let(:issue) { create(:issue, project: project) } let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) } - let(:jira_issue) { JiraIssue.new("JIRA-1", project)} + let(:jira_issue) { ExternalIssue.new("JIRA-1", project)} let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? } let(:commit) { project.commit } diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 26f09cdbaf..b452253672 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -108,17 +108,25 @@ describe TodoService, services: true do should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end - it 'does not create todo when when tasks are marked as completed' do - issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") + context 'issues with a task list' do + it 'does not create todo when tasks are marked as completed' do + issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") - service.update_issue(issue, author) + service.update_issue(issue, author) - should_not_create_todo(user: admin, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: assignee, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: member, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: admin, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: assignee, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: member, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) + end + + it 'does not raise an error when description not change' do + issue.update(title: 'Sample') + + expect { service.update_issue(issue, author) }.not_to raise_error + end end end @@ -165,6 +173,48 @@ describe TodoService, services: true do expect(first_todo.reload).to be_done expect(second_todo.reload).to be_done end + + describe 'cached counts' do + it 'updates when todos change' do + create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + + expect(john_doe.todos_done_count).to eq(0) + expect(john_doe.todos_pending_count).to eq(1) + expect(john_doe).to receive(:update_todos_count_cache).and_call_original + + service.mark_pending_todos_as_done(issue, john_doe) + + expect(john_doe.todos_done_count).to eq(1) + expect(john_doe.todos_pending_count).to eq(0) + end + end + end + + describe '#mark_todos_as_done' do + it 'marks related todos for the user as done' do + first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + + service.mark_todos_as_done([first_todo, second_todo], john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + + describe 'cached counts' do + it 'updates when todos change' do + todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + + expect(john_doe.todos_done_count).to eq(0) + expect(john_doe.todos_pending_count).to eq(1) + expect(john_doe).to receive(:update_todos_count_cache).and_call_original + + service.mark_todos_as_done([todo], john_doe) + + expect(john_doe.todos_done_count).to eq(1) + expect(john_doe.todos_pending_count).to eq(0) + end + end end describe '#new_note' do @@ -285,17 +335,25 @@ describe TodoService, services: true do expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count) end - it 'does not create todo when when tasks are marked as completed' do - mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") + context 'with a task list' do + it 'does not create todo when tasks are marked as completed' do + mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") - service.update_merge_request(mr_assigned, author) + service.update_merge_request(mr_assigned, author) - should_not_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: assignee, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: assignee, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) + end + + it 'does not raise an error when description not change' do + mr_assigned.update(title: 'Sample') + + expect { service.update_merge_request(mr_assigned, author) }.not_to raise_error + end end end @@ -379,6 +437,18 @@ describe TodoService, services: true do end end + it 'updates cached counts when a todo is created' do + issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions) + + expect(john_doe.todos_pending_count).to eq(0) + expect(john_doe).to receive(:update_todos_count_cache) + + service.new_issue(issue, author) + + expect(Todo.where(user_id: john_doe.id, state: :pending).count).to eq 1 + expect(john_doe.todos_pending_count).to eq(1) + end + def should_create_todo(attributes = {}) attributes.reverse_merge!( project: project, diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml new file mode 100644 index 0000000000..3ceec50640 --- /dev/null +++ b/spec/support/import_export/import_export.yml @@ -0,0 +1,20 @@ +# Class relationships to be included in the project import/export +project_tree: + - :issues + - :labels + - merge_requests: + - :merge_request_diff + - :merge_request_test + - commit_statuses: + - :commit + +included_attributes: + project: + - :name + - :path + merge_requests: + - :id + +excluded_attributes: + merge_requests: + - :iid \ No newline at end of file diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb index e3827cae9a..7d6668920c 100644 --- a/spec/workers/expire_build_artifacts_worker_spec.rb +++ b/spec/workers/expire_build_artifacts_worker_spec.rb @@ -20,6 +20,10 @@ describe ExpireBuildArtifactsWorker do it 'does remove files' do expect(build.reload.artifacts_file.exists?).to be_falsey end + + it 'does nullify artifacts_file column' do + expect(build.reload.artifacts_file_identifier).to be_nil + end end context 'with not yet expired artifacts' do @@ -32,6 +36,10 @@ describe ExpireBuildArtifactsWorker do it 'does not remove files' do expect(build.reload.artifacts_file.exists?).to be_truthy end + + it 'does not nullify artifacts_file column' do + expect(build.reload.artifacts_file_identifier).not_to be_nil + end end context 'without expire date' do @@ -44,6 +52,10 @@ describe ExpireBuildArtifactsWorker do it 'does not remove files' do expect(build.reload.artifacts_file.exists?).to be_truthy end + + it 'does not nullify artifacts_file column' do + expect(build.reload.artifacts_file_identifier).not_to be_nil + end end context 'for expired artifacts' do diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb index 1abd87d7d3..b5e1fdb8de 100644 --- a/spec/workers/merge_worker_spec.rb +++ b/spec/workers/merge_worker_spec.rb @@ -9,7 +9,7 @@ describe MergeWorker do before do source_project.team << [author, :master] - source_project.repository.expire_branch_names + source_project.repository.expire_branches_cache end it 'clears cache of source repo after removing source branch' do diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb index 5a03bb77eb..05e07789da 100644 --- a/spec/workers/repository_check/single_repository_worker_spec.rb +++ b/spec/workers/repository_check/single_repository_worker_spec.rb @@ -4,6 +4,26 @@ require 'fileutils' describe RepositoryCheck::SingleRepositoryWorker do subject { described_class.new } + it 'passes when the project has no push events' do + project = create(:project_empty_repo, wiki_enabled: false) + project.events.destroy_all + break_repo(project) + + subject.perform(project.id) + + expect(project.reload.last_repository_check_failed).to eq(false) + end + + it 'fails when the project has push events and a broken repository' do + project = create(:project_empty_repo) + create_push_event(project) + break_repo(project) + + subject.perform(project.id) + + expect(project.reload.last_repository_check_failed).to eq(true) + end + it 'fails if the wiki repository is broken' do project = create(:project_empty_repo, wiki_enabled: true) project.create_wiki @@ -39,6 +59,7 @@ describe RepositoryCheck::SingleRepositoryWorker do it 'does not create a wiki if the main repo does not exist at all' do project = create(:project_empty_repo) + create_push_event(project) FileUtils.rm_rf(project.repository.path_to_repo) FileUtils.rm_rf(wiki_path(project)) @@ -54,4 +75,12 @@ describe RepositoryCheck::SingleRepositoryWorker do def wiki_path(project) project.wiki.repository.path_to_repo end + + def create_push_event(project) + project.events.create(action: Event::PUSHED, author_id: create(:user).id) + end + + def break_repo(project) + FileUtils.rm_rf(File.join(project.repository.path_to_repo, 'objects')) + end end diff --git a/vendor/assets/javascripts/raphael.js b/vendor/assets/javascripts/raphael.js new file mode 100644 index 0000000000..3f3f8a0b7f --- /dev/null +++ b/vendor/assets/javascripts/raphael.js @@ -0,0 +1,8239 @@ +// ┌────────────────────────────────────────────────────────────────────┐ \\ +// │ Raphaël 2.1.4 - JavaScript Vector Library │ \\ +// ├────────────────────────────────────────────────────────────────────┤ \\ +// │ Copyright © 2008-2012 Dmitry Baranovskiy (http://raphaeljs.com) │ \\ +// │ Copyright © 2008-2012 Sencha Labs (http://sencha.com) │ \\ +// ├────────────────────────────────────────────────────────────────────┤ \\ +// │ Licensed under the MIT (http://raphaeljs.com/license.html) license.│ \\ +// └────────────────────────────────────────────────────────────────────┘ \\ +// Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ┌────────────────────────────────────────────────────────────┐ \\ +// │ Eve 0.4.2 - JavaScript Events Library │ \\ +// ├────────────────────────────────────────────────────────────┤ \\ +// │ Author Dmitry Baranovskiy (http://dmitry.baranovskiy.com/) │ \\ +// └────────────────────────────────────────────────────────────┘ \\ + +(function (glob) { + var version = "0.4.2", + has = "hasOwnProperty", + separator = /[\.\/]/, + wildcard = "*", + fun = function () {}, + numsort = function (a, b) { + return a - b; + }, + current_event, + stop, + events = {n: {}}, + /*\ + * eve + [ method ] + + * Fires event with given `name`, given scope and other parameters. + + > Arguments + + - name (string) name of the *event*, dot (`.`) or slash (`/`) separated + - scope (object) context for the event handlers + - varargs (...) the rest of arguments will be sent to event handlers + + = (object) array of returned values from the listeners + \*/ + eve = function (name, scope) { + name = String(name); + var e = events, + oldstop = stop, + args = Array.prototype.slice.call(arguments, 2), + listeners = eve.listeners(name), + z = 0, + f = false, + l, + indexed = [], + queue = {}, + out = [], + ce = current_event, + errors = []; + current_event = name; + stop = 0; + for (var i = 0, ii = listeners.length; i < ii; i++) if ("zIndex" in listeners[i]) { + indexed.push(listeners[i].zIndex); + if (listeners[i].zIndex < 0) { + queue[listeners[i].zIndex] = listeners[i]; + } + } + indexed.sort(numsort); + while (indexed[z] < 0) { + l = queue[indexed[z++]]; + out.push(l.apply(scope, args)); + if (stop) { + stop = oldstop; + return out; + } + } + for (i = 0; i < ii; i++) { + l = listeners[i]; + if ("zIndex" in l) { + if (l.zIndex == indexed[z]) { + out.push(l.apply(scope, args)); + if (stop) { + break; + } + do { + z++; + l = queue[indexed[z]]; + l && out.push(l.apply(scope, args)); + if (stop) { + break; + } + } while (l) + } else { + queue[l.zIndex] = l; + } + } else { + out.push(l.apply(scope, args)); + if (stop) { + break; + } + } + } + stop = oldstop; + current_event = ce; + return out.length ? out : null; + }; + // Undocumented. Debug only. + eve._events = events; + /*\ + * eve.listeners + [ method ] + + * Internal method which gives you array of all event handlers that will be triggered by the given `name`. + + > Arguments + + - name (string) name of the event, dot (`.`) or slash (`/`) separated + + = (array) array of event handlers + \*/ + eve.listeners = function (name) { + var names = name.split(separator), + e = events, + item, + items, + k, + i, + ii, + j, + jj, + nes, + es = [e], + out = []; + for (i = 0, ii = names.length; i < ii; i++) { + nes = []; + for (j = 0, jj = es.length; j < jj; j++) { + e = es[j].n; + items = [e[names[i]], e[wildcard]]; + k = 2; + while (k--) { + item = items[k]; + if (item) { + nes.push(item); + out = out.concat(item.f || []); + } + } + } + es = nes; + } + return out; + }; + + /*\ + * eve.on + [ method ] + ** + * Binds given event handler with a given name. You can use wildcards “`*`” for the names: + | eve.on("*.under.*", f); + | eve("mouse.under.floor"); // triggers f + * Use @eve to trigger the listener. + ** + > Arguments + ** + - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards + - f (function) event handler function + ** + = (function) returned function accepts a single numeric parameter that represents z-index of the handler. It is an optional feature and only used when you need to ensure that some subset of handlers will be invoked in a given order, despite of the order of assignment. + > Example: + | eve.on("mouse", eatIt)(2); + | eve.on("mouse", scream); + | eve.on("mouse", catchIt)(1); + * This will ensure that `catchIt()` function will be called before `eatIt()`. + * + * If you want to put your handler before non-indexed handlers, specify a negative value. + * Note: I assume most of the time you don’t need to worry about z-index, but it’s nice to have this feature “just in case”. + \*/ + eve.on = function (name, f) { + name = String(name); + if (typeof f != "function") { + return function () {}; + } + var names = name.split(separator), + e = events; + for (var i = 0, ii = names.length; i < ii; i++) { + e = e.n; + e = e.hasOwnProperty(names[i]) && e[names[i]] || (e[names[i]] = {n: {}}); + } + e.f = e.f || []; + for (i = 0, ii = e.f.length; i < ii; i++) if (e.f[i] == f) { + return fun; + } + e.f.push(f); + return function (zIndex) { + if (+zIndex == +zIndex) { + f.zIndex = +zIndex; + } + }; + }; + /*\ + * eve.f + [ method ] + ** + * Returns function that will fire given event with optional arguments. + * Arguments that will be passed to the result function will be also + * concated to the list of final arguments. + | el.onclick = eve.f("click", 1, 2); + | eve.on("click", function (a, b, c) { + | console.log(a, b, c); // 1, 2, [event object] + | }); + > Arguments + - event (string) event name + - varargs (…) and any other arguments + = (function) possible event handler function + \*/ + eve.f = function (event) { + var attrs = [].slice.call(arguments, 1); + return function () { + eve.apply(null, [event, null].concat(attrs).concat([].slice.call(arguments, 0))); + }; + }; + /*\ + * eve.stop + [ method ] + ** + * Is used inside an event handler to stop the event, preventing any subsequent listeners from firing. + \*/ + eve.stop = function () { + stop = 1; + }; + /*\ + * eve.nt + [ method ] + ** + * Could be used inside event handler to figure out actual name of the event. + ** + > Arguments + ** + - subname (string) #optional subname of the event + ** + = (string) name of the event, if `subname` is not specified + * or + = (boolean) `true`, if current event’s name contains `subname` + \*/ + eve.nt = function (subname) { + if (subname) { + return new RegExp("(?:\\.|\\/|^)" + subname + "(?:\\.|\\/|$)").test(current_event); + } + return current_event; + }; + /*\ + * eve.nts + [ method ] + ** + * Could be used inside event handler to figure out actual name of the event. + ** + ** + = (array) names of the event + \*/ + eve.nts = function () { + return current_event.split(separator); + }; + /*\ + * eve.off + [ method ] + ** + * Removes given function from the list of event listeners assigned to given name. + * If no arguments specified all the events will be cleared. + ** + > Arguments + ** + - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards + - f (function) event handler function + \*/ + /*\ + * eve.unbind + [ method ] + ** + * See @eve.off + \*/ + eve.off = eve.unbind = function (name, f) { + if (!name) { + eve._events = events = {n: {}}; + return; + } + var names = name.split(separator), + e, + key, + splice, + i, ii, j, jj, + cur = [events]; + for (i = 0, ii = names.length; i < ii; i++) { + for (j = 0; j < cur.length; j += splice.length - 2) { + splice = [j, 1]; + e = cur[j].n; + if (names[i] != wildcard) { + if (e[names[i]]) { + splice.push(e[names[i]]); + } + } else { + for (key in e) if (e[has](key)) { + splice.push(e[key]); + } + } + cur.splice.apply(cur, splice); + } + } + for (i = 0, ii = cur.length; i < ii; i++) { + e = cur[i]; + while (e.n) { + if (f) { + if (e.f) { + for (j = 0, jj = e.f.length; j < jj; j++) if (e.f[j] == f) { + e.f.splice(j, 1); + break; + } + !e.f.length && delete e.f; + } + for (key in e.n) if (e.n[has](key) && e.n[key].f) { + var funcs = e.n[key].f; + for (j = 0, jj = funcs.length; j < jj; j++) if (funcs[j] == f) { + funcs.splice(j, 1); + break; + } + !funcs.length && delete e.n[key].f; + } + } else { + delete e.f; + for (key in e.n) if (e.n[has](key) && e.n[key].f) { + delete e.n[key].f; + } + } + e = e.n; + } + } + }; + /*\ + * eve.once + [ method ] + ** + * Binds given event handler with a given name to only run once then unbind itself. + | eve.once("login", f); + | eve("login"); // triggers f + | eve("login"); // no listeners + * Use @eve to trigger the listener. + ** + > Arguments + ** + - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards + - f (function) event handler function + ** + = (function) same return function as @eve.on + \*/ + eve.once = function (name, f) { + var f2 = function () { + eve.unbind(name, f2); + return f.apply(this, arguments); + }; + return eve.on(name, f2); + }; + /*\ + * eve.version + [ property (string) ] + ** + * Current version of the library. + \*/ + eve.version = version; + eve.toString = function () { + return "You are running Eve " + version; + }; + (typeof module != "undefined" && module.exports) ? (module.exports = eve) : (typeof define != "undefined" ? (define("eve", [], function() { return eve; })) : (glob.eve = eve)); +})(window || this); +// ┌─────────────────────────────────────────────────────────────────────┐ \\ +// │ "Raphaël 2.1.2" - JavaScript Vector Library │ \\ +// ├─────────────────────────────────────────────────────────────────────┤ \\ +// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\ +// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\ +// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\ +// └─────────────────────────────────────────────────────────────────────┘ \\ + +(function (glob, factory) { + // AMD support + if (typeof define === "function" && define.amd) { + // Define as an anonymous module + define(["eve"], function( eve ) { + return factory(glob, eve); + }); + } else { + // Browser globals (glob is window) + // Raphael adds itself to window + factory(glob, glob.eve || (typeof require == "function" && require('eve')) ); + } +}(this, function (window, eve) { + /*\ + * Raphael + [ method ] + ** + * Creates a canvas object on which to draw. + * You must do this first, as all future calls to drawing methods + * from this instance will be bound to this canvas. + > Parameters + ** + - container (HTMLElement|string) DOM element or its ID which is going to be a parent for drawing surface + - width (number) + - height (number) + - callback (function) #optional callback function which is going to be executed in the context of newly created paper + * or + - x (number) + - y (number) + - width (number) + - height (number) + - callback (function) #optional callback function which is going to be executed in the context of newly created paper + * or + - all (array) (first 3 or 4 elements in the array are equal to [containerID, width, height] or [x, y, width, height]. The rest are element descriptions in format {type: type,red
”, “green
”, “cornflowerblue
”, etc)#000
”, “#fc0
”, etc)#000000
”, “#bd2300
”)rgb(200, 100, 0)
”)rgb(100%, 175%, 0%)
”)hsb(0.5, 0.25, 1)
”)Here is short list of commands available, for more details see SVG path string format.
+ #Command | Name | Parameters |
---|---|---|
M | moveto | (x y)+ |
Z | closepath | (none) |
L | lineto | (x y)+ |
H | horizontal lineto | x+ |
V | vertical lineto | y+ |
C | curveto | (x1 y1 x2 y2 x y)+ |
S | smooth curveto | (x2 y2 x y)+ |
Q | quadratic Bézier curveto | (x1 y1 x y)+ |
T | smooth quadratic Bézier curveto | (x y)+ |
A | elliptical arc | (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ |
R | Catmull-Rom curveto* | x1 y1 (x y)+ |
See also Easing demo.
+ \*/ + var ef = R.easing_formulas = { + linear: function (n) { + return n; + }, + "<": function (n) { + return pow(n, 1.7); + }, + ">": function (n) { + return pow(n, .48); + }, + "<>": function (n) { + var q = .48 - n / 1.04, + Q = math.sqrt(.1734 + q * q), + x = Q - q, + X = pow(abs(x), 1 / 3) * (x < 0 ? -1 : 1), + y = -Q - q, + Y = pow(abs(y), 1 / 3) * (y < 0 ? -1 : 1), + t = X + Y + .5; + return (1 - t) * 3 * t * t + t * t * t; + }, + backIn: function (n) { + var s = 1.70158; + return n * n * ((s + 1) * n - s); + }, + backOut: function (n) { + n = n - 1; + var s = 1.70158; + return n * n * ((s + 1) * n + s) + 1; + }, + elastic: function (n) { + if (n == !!n) { + return n; + } + return pow(2, -10 * n) * math.sin((n - .075) * (2 * PI) / .3) + 1; + }, + bounce: function (n) { + var s = 7.5625, + p = 2.75, + l; + if (n < (1 / p)) { + l = s * n * n; + } else { + if (n < (2 / p)) { + n -= (1.5 / p); + l = s * n * n + .75; + } else { + if (n < (2.5 / p)) { + n -= (2.25 / p); + l = s * n * n + .9375; + } else { + n -= (2.625 / p); + l = s * n * n + .984375; + } + } + } + return l; + } + }; + ef.easeIn = ef["ease-in"] = ef["<"]; + ef.easeOut = ef["ease-out"] = ef[">"]; + ef.easeInOut = ef["ease-in-out"] = ef["<>"]; + ef["back-in"] = ef.backIn; + ef["back-out"] = ef.backOut; + + var animationElements = [], + requestAnimFrame = window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function (callback) { + setTimeout(callback, 16); + }, + animation = function () { + var Now = +new Date, + l = 0; + for (; l < animationElements.length; l++) { + var e = animationElements[l]; + if (e.el.removed || e.paused) { + continue; + } + var time = Now - e.start, + ms = e.ms, + easing = e.easing, + from = e.from, + diff = e.diff, + to = e.to, + t = e.t, + that = e.el, + set = {}, + now, + init = {}, + key; + if (e.initstatus) { + time = (e.initstatus * e.anim.top - e.prev) / (e.percent - e.prev) * ms; + e.status = e.initstatus; + delete e.initstatus; + e.stop && animationElements.splice(l--, 1); + } else { + e.status = (e.prev + (e.percent - e.prev) * (time / ms)) / e.anim.top; + } + if (time < 0) { + continue; + } + if (time < ms) { + var pos = easing(time / ms); + for (var attr in from) if (from[has](attr)) { + switch (availableAnimAttrs[attr]) { + case nu: + now = +from[attr] + pos * ms * diff[attr]; + break; + case "colour": + now = "rgb(" + [ + upto255(round(from[attr].r + pos * ms * diff[attr].r)), + upto255(round(from[attr].g + pos * ms * diff[attr].g)), + upto255(round(from[attr].b + pos * ms * diff[attr].b)) + ].join(",") + ")"; + break; + case "path": + now = []; + for (var i = 0, ii = from[attr].length; i < ii; i++) { + now[i] = [from[attr][i][0]]; + for (var j = 1, jj = from[attr][i].length; j < jj; j++) { + now[i][j] = +from[attr][i][j] + pos * ms * diff[attr][i][j]; + } + now[i] = now[i].join(S); + } + now = now.join(S); + break; + case "transform": + if (diff[attr].real) { + now = []; + for (i = 0, ii = from[attr].length; i < ii; i++) { + now[i] = [from[attr][i][0]]; + for (j = 1, jj = from[attr][i].length; j < jj; j++) { + now[i][j] = from[attr][i][j] + pos * ms * diff[attr][i][j]; + } + } + } else { + var get = function (i) { + return +from[attr][i] + pos * ms * diff[attr][i]; + }; + // now = [["r", get(2), 0, 0], ["t", get(3), get(4)], ["s", get(0), get(1), 0, 0]]; + now = [["m", get(0), get(1), get(2), get(3), get(4), get(5)]]; + } + break; + case "csv": + if (attr == "clip-rect") { + now = []; + i = 4; + while (i--) { + now[i] = +from[attr][i] + pos * ms * diff[attr][i]; + } + } + break; + default: + var from2 = [][concat](from[attr]); + now = []; + i = that.paper.customAttributes[attr].length; + while (i--) { + now[i] = +from2[i] + pos * ms * diff[attr][i]; + } + break; + } + set[attr] = now; + } + that.attr(set); + (function (id, that, anim) { + setTimeout(function () { + eve("raphael.anim.frame." + id, that, anim); + }); + })(that.id, that, e.anim); + } else { + (function(f, el, a) { + setTimeout(function() { + eve("raphael.anim.frame." + el.id, el, a); + eve("raphael.anim.finish." + el.id, el, a); + R.is(f, "function") && f.call(el); + }); + })(e.callback, that, e.anim); + that.attr(to); + animationElements.splice(l--, 1); + if (e.repeat > 1 && !e.next) { + for (key in to) if (to[has](key)) { + init[key] = e.totalOrigin[key]; + } + e.el.attr(init); + runAnimation(e.anim, e.el, e.anim.percents[0], null, e.totalOrigin, e.repeat - 1); + } + if (e.next && !e.stop) { + runAnimation(e.anim, e.el, e.next, null, e.totalOrigin, e.repeat); + } + } + } + R.svg && that && that.paper && that.paper.safari(); + animationElements.length && requestAnimFrame(animation); + }, + upto255 = function (color) { + return color > 255 ? 255 : color < 0 ? 0 : color; + }; + /*\ + * Element.animateWith + [ method ] + ** + * Acts similar to @Element.animate, but ensure that given animation runs in sync with another given element. + ** + > Parameters + ** + - el (object) element to sync with + - anim (object) animation to sync with + - params (object) #optional final attributes for the element, see also @Element.attr + - ms (number) #optional number of milliseconds for animation to run + - easing (string) #optional easing type. Accept on of @Raphael.easing_formulas or CSS format: `cubic‐bezier(XX, XX, XX, XX)` + - callback (function) #optional callback function. Will be called at the end of animation. + * or + - element (object) element to sync with + - anim (object) animation to sync with + - animation (object) #optional animation object, see @Raphael.animation + ** + = (object) original element + \*/ + elproto.animateWith = function (el, anim, params, ms, easing, callback) { + var element = this; + if (element.removed) { + callback && callback.call(element); + return element; + } + var a = params instanceof Animation ? params : R.animation(params, ms, easing, callback), + x, y; + runAnimation(a, element, a.percents[0], null, element.attr()); + for (var i = 0, ii = animationElements.length; i < ii; i++) { + if (animationElements[i].anim == anim && animationElements[i].el == el) { + animationElements[ii - 1].start = animationElements[i].start; + break; + } + } + return element; + // + // + // var a = params ? R.animation(params, ms, easing, callback) : anim, + // status = element.status(anim); + // return this.animate(a).status(a, status * anim.ms / a.ms); + }; + function CubicBezierAtTime(t, p1x, p1y, p2x, p2y, duration) { + var cx = 3 * p1x, + bx = 3 * (p2x - p1x) - cx, + ax = 1 - cx - bx, + cy = 3 * p1y, + by = 3 * (p2y - p1y) - cy, + ay = 1 - cy - by; + function sampleCurveX(t) { + return ((ax * t + bx) * t + cx) * t; + } + function solve(x, epsilon) { + var t = solveCurveX(x, epsilon); + return ((ay * t + by) * t + cy) * t; + } + function solveCurveX(x, epsilon) { + var t0, t1, t2, x2, d2, i; + for(t2 = x, i = 0; i < 8; i++) { + x2 = sampleCurveX(t2) - x; + if (abs(x2) < epsilon) { + return t2; + } + d2 = (3 * ax * t2 + 2 * bx) * t2 + cx; + if (abs(d2) < 1e-6) { + break; + } + t2 = t2 - x2 / d2; + } + t0 = 0; + t1 = 1; + t2 = x; + if (t2 < t0) { + return t0; + } + if (t2 > t1) { + return t1; + } + while (t0 < t1) { + x2 = sampleCurveX(t2); + if (abs(x2 - x) < epsilon) { + return t2; + } + if (x > x2) { + t0 = t2; + } else { + t1 = t2; + } + t2 = (t1 - t0) / 2 + t0; + } + return t2; + } + return solve(t, 1 / (200 * duration)); + } + elproto.onAnimation = function (f) { + f ? eve.on("raphael.anim.frame." + this.id, f) : eve.unbind("raphael.anim.frame." + this.id); + return this; + }; + function Animation(anim, ms) { + var percents = [], + newAnim = {}; + this.ms = ms; + this.times = 1; + if (anim) { + for (var attr in anim) if (anim[has](attr)) { + newAnim[toFloat(attr)] = anim[attr]; + percents.push(toFloat(attr)); + } + percents.sort(sortByNumber); + } + this.anim = newAnim; + this.top = percents[percents.length - 1]; + this.percents = percents; + } + /*\ + * Animation.delay + [ method ] + ** + * Creates a copy of existing animation object with given delay. + ** + > Parameters + ** + - delay (number) number of ms to pass between animation start and actual animation + ** + = (object) new altered Animation object + | var anim = Raphael.animation({cx: 10, cy: 20}, 2e3); + | circle1.animate(anim); // run the given animation immediately + | circle2.animate(anim.delay(500)); // run the given animation after 500 ms + \*/ + Animation.prototype.delay = function (delay) { + var a = new Animation(this.anim, this.ms); + a.times = this.times; + a.del = +delay || 0; + return a; + }; + /*\ + * Animation.repeat + [ method ] + ** + * Creates a copy of existing animation object with given repetition. + ** + > Parameters + ** + - repeat (number) number iterations of animation. For infinite animation pass `Infinity` + ** + = (object) new altered Animation object + \*/ + Animation.prototype.repeat = function (times) { + var a = new Animation(this.anim, this.ms); + a.del = this.del; + a.times = math.floor(mmax(times, 0)) || 1; + return a; + }; + function runAnimation(anim, element, percent, status, totalOrigin, times) { + percent = toFloat(percent); + var params, + isInAnim, + isInAnimSet, + percents = [], + next, + prev, + timestamp, + ms = anim.ms, + from = {}, + to = {}, + diff = {}; + if (status) { + for (i = 0, ii = animationElements.length; i < ii; i++) { + var e = animationElements[i]; + if (e.el.id == element.id && e.anim == anim) { + if (e.percent != percent) { + animationElements.splice(i, 1); + isInAnimSet = 1; + } else { + isInAnim = e; + } + element.attr(e.totalOrigin); + break; + } + } + } else { + status = +to; // NaN + } + for (var i = 0, ii = anim.percents.length; i < ii; i++) { + if (anim.percents[i] == percent || anim.percents[i] > status * anim.top) { + percent = anim.percents[i]; + prev = anim.percents[i - 1] || 0; + ms = ms / anim.top * (percent - prev); + next = anim.percents[i + 1]; + params = anim.anim[percent]; + break; + } else if (status) { + element.attr(anim.anim[anim.percents[i]]); + } + } + if (!params) { + return; + } + if (!isInAnim) { + for (var attr in params) if (params[has](attr)) { + if (availableAnimAttrs[has](attr) || element.paper.customAttributes[has](attr)) { + from[attr] = element.attr(attr); + (from[attr] == null) && (from[attr] = availableAttrs[attr]); + to[attr] = params[attr]; + switch (availableAnimAttrs[attr]) { + case nu: + diff[attr] = (to[attr] - from[attr]) / ms; + break; + case "colour": + from[attr] = R.getRGB(from[attr]); + var toColour = R.getRGB(to[attr]); + diff[attr] = { + r: (toColour.r - from[attr].r) / ms, + g: (toColour.g - from[attr].g) / ms, + b: (toColour.b - from[attr].b) / ms + }; + break; + case "path": + var pathes = path2curve(from[attr], to[attr]), + toPath = pathes[1]; + from[attr] = pathes[0]; + diff[attr] = []; + for (i = 0, ii = from[attr].length; i < ii; i++) { + diff[attr][i] = [0]; + for (var j = 1, jj = from[attr][i].length; j < jj; j++) { + diff[attr][i][j] = (toPath[i][j] - from[attr][i][j]) / ms; + } + } + break; + case "transform": + var _ = element._, + eq = equaliseTransform(_[attr], to[attr]); + if (eq) { + from[attr] = eq.from; + to[attr] = eq.to; + diff[attr] = []; + diff[attr].real = true; + for (i = 0, ii = from[attr].length; i < ii; i++) { + diff[attr][i] = [from[attr][i][0]]; + for (j = 1, jj = from[attr][i].length; j < jj; j++) { + diff[attr][i][j] = (to[attr][i][j] - from[attr][i][j]) / ms; + } + } + } else { + var m = (element.matrix || new Matrix), + to2 = { + _: {transform: _.transform}, + getBBox: function () { + return element.getBBox(1); + } + }; + from[attr] = [ + m.a, + m.b, + m.c, + m.d, + m.e, + m.f + ]; + extractTransform(to2, to[attr]); + to[attr] = to2._.transform; + diff[attr] = [ + (to2.matrix.a - m.a) / ms, + (to2.matrix.b - m.b) / ms, + (to2.matrix.c - m.c) / ms, + (to2.matrix.d - m.d) / ms, + (to2.matrix.e - m.e) / ms, + (to2.matrix.f - m.f) / ms + ]; + // from[attr] = [_.sx, _.sy, _.deg, _.dx, _.dy]; + // var to2 = {_:{}, getBBox: function () { return element.getBBox(); }}; + // extractTransform(to2, to[attr]); + // diff[attr] = [ + // (to2._.sx - _.sx) / ms, + // (to2._.sy - _.sy) / ms, + // (to2._.deg - _.deg) / ms, + // (to2._.dx - _.dx) / ms, + // (to2._.dy - _.dy) / ms + // ]; + } + break; + case "csv": + var values = Str(params[attr])[split](separator), + from2 = Str(from[attr])[split](separator); + if (attr == "clip-rect") { + from[attr] = from2; + diff[attr] = []; + i = from2.length; + while (i--) { + diff[attr][i] = (values[i] - from[attr][i]) / ms; + } + } + to[attr] = values; + break; + default: + values = [][concat](params[attr]); + from2 = [][concat](from[attr]); + diff[attr] = []; + i = element.paper.customAttributes[attr].length; + while (i--) { + diff[attr][i] = ((values[i] || 0) - (from2[i] || 0)) / ms; + } + break; + } + } + } + var easing = params.easing, + easyeasy = R.easing_formulas[easing]; + if (!easyeasy) { + easyeasy = Str(easing).match(bezierrg); + if (easyeasy && easyeasy.length == 5) { + var curve = easyeasy; + easyeasy = function (t) { + return CubicBezierAtTime(t, +curve[1], +curve[2], +curve[3], +curve[4], ms); + }; + } else { + easyeasy = pipe; + } + } + timestamp = params.start || anim.start || +new Date; + e = { + anim: anim, + percent: percent, + timestamp: timestamp, + start: timestamp + (anim.del || 0), + status: 0, + initstatus: status || 0, + stop: false, + ms: ms, + easing: easyeasy, + from: from, + diff: diff, + to: to, + el: element, + callback: params.callback, + prev: prev, + next: next, + repeat: times || anim.times, + origin: element.attr(), + totalOrigin: totalOrigin + }; + animationElements.push(e); + if (status && !isInAnim && !isInAnimSet) { + e.stop = true; + e.start = new Date - ms * status; + if (animationElements.length == 1) { + return animation(); + } + } + if (isInAnimSet) { + e.start = new Date - e.ms * status; + } + animationElements.length == 1 && requestAnimFrame(animation); + } else { + isInAnim.initstatus = status; + isInAnim.start = new Date - isInAnim.ms * status; + } + eve("raphael.anim.start." + element.id, element, anim); + } + /*\ + * Raphael.animation + [ method ] + ** + * Creates an animation object that can be passed to the @Element.animate or @Element.animateWith methods. + * See also @Animation.delay and @Animation.repeat methods. + ** + > Parameters + ** + - params (object) final attributes for the element, see also @Element.attr + - ms (number) number of milliseconds for animation to run + - easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic‐bezier(XX, XX, XX, XX)` + - callback (function) #optional callback function. Will be called at the end of animation. + ** + = (object) @Animation + \*/ + R.animation = function (params, ms, easing, callback) { + if (params instanceof Animation) { + return params; + } + if (R.is(easing, "function") || !easing) { + callback = callback || easing || null; + easing = null; + } + params = Object(params); + ms = +ms || 0; + var p = {}, + json, + attr; + for (attr in params) if (params[has](attr) && toFloat(attr) != attr && toFloat(attr) + "%" != attr) { + json = true; + p[attr] = params[attr]; + } + if (!json) { + // if percent-like syntax is used and end-of-all animation callback used + if(callback){ + // find the last one + var lastKey = 0; + for(var i in params){ + var percent = toInt(i); + if(params[has](i) && percent > lastKey){ + lastKey = percent; + } + } + lastKey += '%'; + // if already defined callback in the last keyframe, skip + !params[lastKey].callback && (params[lastKey].callback = callback); + } + return new Animation(params, ms); + } else { + easing && (p.easing = easing); + callback && (p.callback = callback); + return new Animation({100: p}, ms); + } + }; + /*\ + * Element.animate + [ method ] + ** + * Creates and starts animation for given element. + ** + > Parameters + ** + - params (object) final attributes for the element, see also @Element.attr + - ms (number) number of milliseconds for animation to run + - easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic‐bezier(XX, XX, XX, XX)` + - callback (function) #optional callback function. Will be called at the end of animation. + * or + - animation (object) animation object, see @Raphael.animation + ** + = (object) original element + \*/ + elproto.animate = function (params, ms, easing, callback) { + var element = this; + if (element.removed) { + callback && callback.call(element); + return element; + } + var anim = params instanceof Animation ? params : R.animation(params, ms, easing, callback); + runAnimation(anim, element, anim.percents[0], null, element.attr()); + return element; + }; + /*\ + * Element.setTime + [ method ] + ** + * Sets the status of animation of the element in milliseconds. Similar to @Element.status method. + ** + > Parameters + ** + - anim (object) animation object + - value (number) number of milliseconds from the beginning of the animation + ** + = (object) original element if `value` is specified + * Note, that during animation following events are triggered: + * + * On each animation frame event `anim.frame.Please refer to the SVG specification for an explanation of these parameters.
+ o arrow-end (string) arrowhead on the end of the path. The format for string is `Please refer to SVG documentation regarding path string. Raphaël fully supports it.
+ > Colour Parsing + #red
”, “green
”, “cornflowerblue
”, etc)#000
”, “#fc0
”, etc)#000000
”, “#bd2300
”)rgb(200, 100, 0)
”)rgb(100%, 175%, 0%)
”)rgba(200, 100, 0, .5)
”)rgba(100%, 175%, 0%, 50%)
”)hsb(0.5, 0.25, 1)
”)hsl(240deg, 1, .5)
” or, if you want to go fancy, “hsl(240°, 1, .5)
”