diff options
134 files changed, 9263 insertions, 339 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 83a906932d0..219077d79b8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -129,56 +129,51 @@ spinach 7 10: *spinach-knapsack spinach 8 10: *spinach-knapsack spinach 9 10: *spinach-knapsack -# Execute all testing suites against Ruby 2.2 - -.ruby-22: &ruby-22 - image: "ruby:2.2" +# Execute all testing suites against Ruby 2.3 +.ruby-23: &ruby-23 + image: "ruby:2.3" only: - master - cache: - key: "ruby22" - paths: - - vendor -.rspec-knapsack-ruby22: &rspec-knapsack-ruby22 +.rspec-knapsack-ruby23: &rspec-knapsack-ruby23 <<: *rspec-knapsack - <<: *ruby-22 + <<: *ruby-23 -.spinach-knapsack-ruby22: &spinach-knapsack-ruby22 +.spinach-knapsack-ruby23: &spinach-knapsack-ruby23 <<: *spinach-knapsack - <<: *ruby-22 + <<: *ruby-23 -rspec 0 20 ruby22: *rspec-knapsack-ruby22 -rspec 1 20 ruby22: *rspec-knapsack-ruby22 -rspec 2 20 ruby22: *rspec-knapsack-ruby22 -rspec 3 20 ruby22: *rspec-knapsack-ruby22 -rspec 4 20 ruby22: *rspec-knapsack-ruby22 -rspec 5 20 ruby22: *rspec-knapsack-ruby22 -rspec 6 20 ruby22: *rspec-knapsack-ruby22 -rspec 7 20 ruby22: *rspec-knapsack-ruby22 -rspec 8 20 ruby22: *rspec-knapsack-ruby22 -rspec 9 20 ruby22: *rspec-knapsack-ruby22 -rspec 10 20 ruby22: *rspec-knapsack-ruby22 -rspec 11 20 ruby22: *rspec-knapsack-ruby22 -rspec 12 20 ruby22: *rspec-knapsack-ruby22 -rspec 13 20 ruby22: *rspec-knapsack-ruby22 -rspec 14 20 ruby22: *rspec-knapsack-ruby22 -rspec 15 20 ruby22: *rspec-knapsack-ruby22 -rspec 16 20 ruby22: *rspec-knapsack-ruby22 -rspec 17 20 ruby22: *rspec-knapsack-ruby22 -rspec 18 20 ruby22: *rspec-knapsack-ruby22 -rspec 19 20 ruby22: *rspec-knapsack-ruby22 - -spinach 0 10 ruby22: *spinach-knapsack-ruby22 -spinach 1 10 ruby22: *spinach-knapsack-ruby22 -spinach 2 10 ruby22: *spinach-knapsack-ruby22 -spinach 3 10 ruby22: *spinach-knapsack-ruby22 -spinach 4 10 ruby22: *spinach-knapsack-ruby22 -spinach 5 10 ruby22: *spinach-knapsack-ruby22 -spinach 6 10 ruby22: *spinach-knapsack-ruby22 -spinach 7 10 ruby22: *spinach-knapsack-ruby22 -spinach 8 10 ruby22: *spinach-knapsack-ruby22 -spinach 9 10 ruby22: *spinach-knapsack-ruby22 +rspec 0 20 ruby23: *rspec-knapsack-ruby23 +rspec 1 20 ruby23: *rspec-knapsack-ruby23 +rspec 2 20 ruby23: *rspec-knapsack-ruby23 +rspec 3 20 ruby23: *rspec-knapsack-ruby23 +rspec 4 20 ruby23: *rspec-knapsack-ruby23 +rspec 5 20 ruby23: *rspec-knapsack-ruby23 +rspec 6 20 ruby23: *rspec-knapsack-ruby23 +rspec 7 20 ruby23: *rspec-knapsack-ruby23 +rspec 8 20 ruby23: *rspec-knapsack-ruby23 +rspec 9 20 ruby23: *rspec-knapsack-ruby23 +rspec 10 20 ruby23: *rspec-knapsack-ruby23 +rspec 11 20 ruby23: *rspec-knapsack-ruby23 +rspec 12 20 ruby23: *rspec-knapsack-ruby23 +rspec 13 20 ruby23: *rspec-knapsack-ruby23 +rspec 14 20 ruby23: *rspec-knapsack-ruby23 +rspec 15 20 ruby23: *rspec-knapsack-ruby23 +rspec 16 20 ruby23: *rspec-knapsack-ruby23 +rspec 17 20 ruby23: *rspec-knapsack-ruby23 +rspec 18 20 ruby23: *rspec-knapsack-ruby23 +rspec 19 20 ruby23: *rspec-knapsack-ruby23 + +spinach 0 10 ruby23: *spinach-knapsack-ruby23 +spinach 1 10 ruby23: *spinach-knapsack-ruby23 +spinach 2 10 ruby23: *spinach-knapsack-ruby23 +spinach 3 10 ruby23: *spinach-knapsack-ruby23 +spinach 4 10 ruby23: *spinach-knapsack-ruby23 +spinach 5 10 ruby23: *spinach-knapsack-ruby23 +spinach 6 10 ruby23: *spinach-knapsack-ruby23 +spinach 7 10 ruby23: *spinach-knapsack-ruby23 +spinach 8 10 ruby23: *spinach-knapsack-ruby23 +spinach 9 10 ruby23: *spinach-knapsack-ruby23 # Other generic tests diff --git a/CHANGELOG b/CHANGELOG index 10b4bd3a663..362b5bd580a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.9.0 (unreleased) background during a refresh. - 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 - Don't show tags for revert and cherry-pick operations - Fix issue todo not remove when leave project !4150 (Long Nguyen) @@ -35,6 +36,7 @@ v 8.9.0 (unreleased) - 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 @@ -45,6 +47,8 @@ v 8.9.0 (unreleased) - 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 @@ -56,6 +60,7 @@ v 8.9.0 (unreleased) - 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 @@ -94,6 +99,7 @@ v 8.9.0 (unreleased) - Show categorised search queries in the search autocomplete - RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented - 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 @@ -101,6 +107,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 @@ -120,6 +127,9 @@ v 8.9.0 (unreleased) - Set inverse_of for Project/Service association to reduce the number of queries - Update tanuki logo highlight/loading colors - Use Git cached counters for branches and tags on project page + - Filter parameters for request_uri value on instrumented transactions. + - Cache user todo counts from TodoService + - Ensure Todos counters doesn't count Todos for projects pending delete v 8.8.5 - Import GitHub repositories respecting the API rate limit !4166 @@ -221,7 +221,7 @@ 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' diff --git a/Gemfile.lock b/Gemfile.lock index e5d0f8119dd..49e548fb94f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -866,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) diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee index 8eb005b0a22..12340bbce54 100644 --- a/app/assets/javascripts/right_sidebar.js.coffee +++ b/app/assets/javascripts/right_sidebar.js.coffee @@ -51,15 +51,19 @@ class @Sidebar $this = $(e.currentTarget) $todoLoading = $('.js-issuable-todo-loading') $btnText = $('.js-issuable-todo-text', $this) - ajaxType = if $this.attr('data-id') then 'PATCH' else 'POST' - ajaxUrlExtra = if $this.attr('data-id') then "/#{$this.attr('data-id')}" else '' + ajaxType = if $this.attr('data-delete-path') then 'DELETE' else 'POST' + + if $this.attr('data-delete-path') + url = "#{$this.attr('data-delete-path')}" + else + url = "#{$this.data('url')}" $.ajax( - url: "#{$this.data('url')}#{ajaxUrlExtra}" + url: url type: ajaxType dataType: 'json' data: - issuable_id: $this.data('issuable') + issuable_id: $this.data('issuable-id') issuable_type: $this.data('issuable-type') beforeSend: => @beforeTodoSend($this, $todoLoading) @@ -82,15 +86,15 @@ class @Sidebar else $todoPendingCount.removeClass 'hidden' - if data.todo? + if data.delete_path? $btn .attr 'aria-label', $btn.data('mark-text') - .attr 'data-id', data.todo.id + .attr 'data-delete-path', data.delete_path $btnText.text $btn.data('mark-text') else $btn .attr 'aria-label', $btn.data('todo-text') - .removeAttr 'data-id' + .removeAttr 'data-delete-path' $btnText.text $btn.data('todo-text') sidebarDropdownLoading: (e) -> diff --git a/app/assets/javascripts/users/calendar.js.coffee b/app/assets/javascripts/users/calendar.js.coffee index 26a26061539..c081f023b04 100644 --- a/app/assets/javascripts/users/calendar.js.coffee +++ b/app/assets/javascripts/users/calendar.js.coffee @@ -6,12 +6,6 @@ class @Calendar @daySizeWithSpace = @daySize + (@daySpace * 2) @monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] @months = [] - @highestValue = 0 - - # Get the highest value from the timestampes - _.each timestamps, (count) => - if count > @highestValue - @highestValue = count # Loop through the timestamps to create a group of objects # The group of objects will be grouped based on the day of the week they are @@ -39,8 +33,8 @@ class @Calendar i++ # Init color functions - @color = @initColor() @colorKey = @initColorKey() + @color = @initColor() # Init the svg element @renderSvg(group) @@ -104,7 +98,7 @@ class @Calendar .attr 'class', 'user-contrib-cell js-tooltip' .attr 'fill', (stamp) => if stamp.count isnt 0 - @color(stamp.count) + @color(Math.min(stamp.count, 40)) else '#ededed' .attr 'data-container', 'body' @@ -164,10 +158,11 @@ class @Calendar color initColor: -> + colorRange = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)] d3.scale - .linear() - .range(['#acd5f2', '#254e77']) - .domain([0, @highestValue]) + .threshold() + .domain([0, 10, 20, 30]) + .range(colorRange) initColorKey: -> d3.scale diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index fc3f214aba5..35ab28b3fea 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -26,6 +26,8 @@ .commit-info-row { margin-bottom: 10px; + line-height: 24px; + padding-top: 6px; &.commit-info-row-header { line-height: 34px; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 72d1b97bf56..dd1bc6f5d52 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :abilities, :can?, :current_application_settings - helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled? + helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -319,6 +319,10 @@ class ApplicationController < ActionController::Base current_application_settings.import_sources.include?('git') end + def gitlab_project_import_enabled? + current_application_settings.import_sources.include?('gitlab_project') + end + def two_factor_authentication_required? current_application_settings.require_two_factor_authentication end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index f9a1929c117..3a2db3e6eeb 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -1,44 +1,39 @@ class Dashboard::TodosController < Dashboard::ApplicationController - before_action :find_todos, only: [:index, :destroy, :destroy_all] + include TodosHelper + + before_action :find_todos, only: [:index, :destroy_all] def index @todos = @todos.page(params[:page]) end def destroy - todo.done - - todo_notice = 'Todo was successfully marked as done.' + TodoService.new.mark_todos_as_done([todo], current_user) respond_to do |format| - format.html { redirect_to dashboard_todos_path, notice: todo_notice } + format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' } format.js { head :ok } - format.json do - render json: { count: @todos.size, done_count: current_user.todos.done.count } - end + format.json { render json: { count: todos_pending_count, done_count: todos_done_count } } end end def destroy_all - @todos.each(&:done) + TodoService.new.mark_todos_as_done(@todos, current_user) respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } format.js { head :ok } - format.json do - find_todos - render json: { count: @todos.size, done_count: current_user.todos.done.count } - end + format.json { render json: { count: todos_pending_count, done_count: todos_done_count } } end end private def todo - @todo ||= current_user.todos.find(params[:id]) + @todo ||= find_todos.find(params[:id]) end def find_todos - @todos = TodosFinder.new(current_user, params).execute + @todos ||= TodosFinder.new(current_user, params).execute end end diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb new file mode 100644 index 00000000000..f99aa490d3e --- /dev/null +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -0,0 +1,48 @@ +class Import::GitlabProjectsController < Import::BaseController + before_action :verify_gitlab_project_import_enabled + + def new + @namespace_id = project_params[:namespace_id] + @namespace_name = Namespace.find(project_params[:namespace_id]).name + @path = project_params[:path] + end + + def create + unless file_is_valid? + return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." }) + end + + @project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id], + current_user, + File.expand_path(project_params[:file].path), + project_params[:path]).execute + + if @project.saved? + redirect_to( + project_path(@project), + notice: "Project '#{@project.name}' is being imported." + ) + else + redirect_to( + new_import_gitlab_project_path, + alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}" + ) + end + end + + private + + def file_is_valid? + project_params[:file] && project_params[:file].respond_to?(:read) + end + + def verify_gitlab_project_import_enabled + render_404 unless gitlab_project_import_enabled? + end + + def project_params + params.permit( + :path, :namespace_id, :file + ) + end +end diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index 175afbf8425..69959fe3687 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -5,7 +5,7 @@ class Profiles::AccountsController < Profiles::ApplicationController def unlink provider = params[:provider] - current_user.identities.find_by(provider: provider).destroy + current_user.identities.find_by(provider: provider).destroy unless provider.to_s == 'saml' redirect_to profile_account_path end end diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 46b242aa5ff..6dc495247c8 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -6,8 +6,10 @@ class Projects::TagsController < Projects::ApplicationController before_action :authorize_admin_project!, only: [:destroy] def index - sorted = VersionSorter.rsort(@repository.tag_names) - @tags = Kaminari.paginate_array(sorted).page(params[:page]) + @sort = params[:sort] || 'name' + @tags = @repository.tags_sorted_by(@sort) + @tags = Kaminari.paginate_array(@tags).page(params[:page]) + @releases = project.releases.where(tag: @tags) end diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb index a51bd5e2b49..23868d986e9 100644 --- a/app/controllers/projects/todos_controller.rb +++ b/app/controllers/projects/todos_controller.rb @@ -1,18 +1,12 @@ class Projects::TodosController < Projects::ApplicationController - def create - todos = TodoService.new.mark_todo(issuable, current_user) + before_action :authenticate_user!, only: [:create] - render json: { - todo: todos, - count: current_user.todos.pending.count, - } - end - - def update - current_user.todos.find_by_id(params[:id]).update(state: :done) + def create + todo = TodoService.new.mark_todo(issuable, current_user) render json: { - count: current_user.todos.pending.count, + count: current_user.todos_pending_count, + delete_path: dashboard_todo_path(todo) } end @@ -22,7 +16,13 @@ class Projects::TodosController < Projects::ApplicationController @issuable ||= begin case params[:issuable_type] when "issue" - @project.issues.find(params[:issuable_id]) + issue = @project.issues.find(params[:issuable_id]) + + if can?(current_user, :read_issue, issue) + issue + else + render_404 + end when "merge_request" @project.merge_requests.find(params[:issuable_id]) end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 482c11cf23c..8044c637825 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -7,7 +7,7 @@ class ProjectsController < Projects::ApplicationController before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists? # Authorize - before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping] + before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export] before_action :event_filter, only: [:show, :activity] layout :determine_layout @@ -186,6 +186,48 @@ class ProjectsController < Projects::ApplicationController ) end + def export + @project.add_export_job(current_user: current_user) + + redirect_to( + edit_project_path(@project), + notice: "Project export started. A download link will be sent by email." + ) + end + + def download_export + export_project_path = @project.export_project_path + + if export_project_path + send_file export_project_path, disposition: 'attachment' + else + redirect_to( + edit_project_path(@project), + alert: "Project export link has expired. Please generate a new export from your project settings." + ) + end + end + + def remove_export + if @project.remove_exports + flash[:notice] = "Project export has been deleted." + else + flash[:alert] = "Project export could not be deleted." + end + redirect_to(edit_project_path(@project)) + end + + def generate_new_export + if @project.remove_exports + export + else + redirect_to( + edit_project_path(@project), + alert: "Project export could not be deleted." + ) + end + end + def toggle_star current_user.toggle_star(@project) @project.reload diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index aa47c6c157e..58a00f88af7 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -123,7 +123,7 @@ class TodosFinder end def by_state(items) - case params[:state] + case params[:state].to_s when 'done' items.done else diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 07a3f452460..9051a493b9b 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -17,11 +17,21 @@ module ButtonHelper def clipboard_button(data = {}) content_tag :button, icon('clipboard'), - class: "btn", + class: "btn btn-clipboard", data: data, type: :button end + # Output a "Copy to Clipboard" button with a custom CSS class + # + # data - Data attributes passed to `content_tag` + # css_class - Class passed to the `content_tag` + # + # Examples: + # + # # Define the target element + # clipboard_button_with_class({clipboard_target: "div#foo"}, css_class: "btn-clipboard") + # # => "<button class='btn btn-clipboard' data-clipboard-target='div#foo'>...</button>" def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard') content_tag :button, icon('clipboard'), diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 8dbc51a689f..8231ce49fac 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -67,9 +67,9 @@ module IssuablesHelper end end - def has_todo(issuable) - unless current_user.nil? - current_user.todos.find_by(target_id: issuable.id, state: :pending) + def issuable_todo(issuable) + if current_user + current_user.todos.find_by(target: issuable, state: :pending) end end diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 877c77050be..ec106418f2d 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -6,6 +6,12 @@ module MembersHelper "#{action}_#{member.type.underscore}".to_sym end + def default_show_roles(member) + can?(current_user, action_member_permission(:update, member), member) || + can?(current_user, action_member_permission(:destroy, member), member) || + can?(current_user, action_member_permission(:admin, member), member.source) + end + def remove_member_message(member, user: nil) user = current_user if defined?(current_user) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 9adf5ef29f7..a832a6c8df7 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -1,10 +1,10 @@ module TodosHelper def todos_pending_count - current_user.todos.pending.count + TodosFinder.new(current_user, state: :pending).execute.count end def todos_done_count - current_user.todos.done.count + TodosFinder.new(current_user, state: :done).execute.count end def todo_action_name(todo) @@ -12,7 +12,7 @@ module TodosHelper when Todo::ASSIGNED then 'assigned you' when Todo::MENTIONED then 'mentioned you on' when Todo::BUILD_FAILED then 'The build failed for your' - when Todo::MARKED then 'marked this as a Todo for' + when Todo::MARKED then 'added a todo for' end end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 689fb3e0ffb..e0af7081411 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -9,6 +9,19 @@ module Emails subject: subject("Project was moved")) end + def project_was_exported_email(current_user, project) + @project = project + mail(to: current_user.notification_email, + subject: subject("Project was exported")) + end + + def project_was_not_exported_email(current_user, project, errors) + @project = project + @errors = errors + mail(to: current_user.notification_email, + subject: subject("Project export error")) + end + def repository_push_email(project_id, opts = {}) @message = Gitlab::Email::Message::RepositoryPush.new(self, project_id, opts) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index a744f937918..d914b0b26eb 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -123,7 +123,7 @@ class ApplicationSetting < ActiveRecord::Base 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/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index a5f2ac59001..5b264ecffc5 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -170,6 +170,10 @@ module Ci builds.where.not(environment: nil).success.pluck(:environment).uniq end + def notes + Note.for_commit_id(sha) + end + private def build_builds_for_stages(stages, user, status, trigger_request) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index e53c483b904..ab13db4b297 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,5 +1,6 @@ class CommitStatus < ActiveRecord::Base include Statuseable + include Importable self.table_name = 'ci_builds' @@ -7,7 +8,7 @@ class CommitStatus < ActiveRecord::Base belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true belongs_to :user - validates :pipeline, presence: true + validates :pipeline, presence: true, unless: :importing? validates_presence_of :name diff --git a/app/models/concerns/importable.rb b/app/models/concerns/importable.rb new file mode 100644 index 00000000000..019ef755849 --- /dev/null +++ b/app/models/concerns/importable.rb @@ -0,0 +1,6 @@ +module Importable + extend ActiveSupport::Concern + + attr_accessor :importing + alias_method :importing?, :importing +end diff --git a/app/models/member.rb b/app/models/member.rb index cea6d259760..4ee3f1bb5c2 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -1,5 +1,6 @@ class Member < ActiveRecord::Base include Sortable + include Importable include Gitlab::Access attr_accessor :raw_invite_token @@ -41,11 +42,11 @@ class Member < ActiveRecord::Base before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } - after_create :send_invite, if: :invite? - after_create :send_request, if: :request? - after_create :create_notification_setting, unless: :pending? - after_create :post_create_hook, unless: :pending? - after_update :post_update_hook, unless: :pending? + after_create :send_invite, if: :invite?, unless: :importing? + after_create :send_request, if: :request?, unless: :importing? + after_create :create_notification_setting, unless: [:pending?, :importing?] + after_create :post_create_hook, unless: [:pending?, :importing?] + after_update :post_update_hook, unless: [:pending?, :importing?] after_destroy :post_destroy_hook, unless: :pending? after_destroy :post_decline_request, if: :request? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7b8858b24d6..73bf182ec9f 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -4,6 +4,7 @@ class MergeRequest < ActiveRecord::Base include Referable include Sortable include Taskable + include Importable belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" @@ -13,7 +14,7 @@ class MergeRequest < ActiveRecord::Base serialize :merge_params, Hash - after_create :create_merge_request_diff + after_create :create_merge_request_diff, unless: :importing after_update :update_merge_request_diff delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil @@ -95,12 +96,12 @@ class MergeRequest < ActiveRecord::Base end end - validates :source_project, presence: true, unless: :allow_broken + validates :source_project, presence: true, unless: [:allow_broken, :importing?] validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true validates :merge_user, presence: true, if: :merge_when_build_succeeds? - validate :validate_branches, unless: :allow_broken + validate :validate_branches, unless: [:allow_broken, :importing?] validate :validate_fork scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 7d5103748f5..aca377cc600 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -1,5 +1,6 @@ class MergeRequestDiff < ActiveRecord::Base include Sortable + include Importable # Prevent store of diff if commits amount more then 500 COMMITS_SAFE_SIZE = 100 @@ -22,7 +23,7 @@ class MergeRequestDiff < ActiveRecord::Base serialize :st_commits serialize :st_diffs - after_create :reload_content + after_create :reload_content, unless: :importing? def reload_content reload_commits diff --git a/app/models/note.rb b/app/models/note.rb index 4b6748053ff..8d164647550 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -4,6 +4,7 @@ class Note < ActiveRecord::Base include Participable include Mentionable include Awardable + include Importable default_value_for :system, false @@ -28,11 +29,11 @@ class Note < ActiveRecord::Base validates :attachment, file_size: { maximum: :max_attachment_size } validates :noteable_type, presence: true - validates :noteable_id, presence: true, unless: :for_commit? + validates :noteable_id, presence: true, unless: [:for_commit?, :importing?] validates :commit_id, presence: true, if: :for_commit? validates :author, presence: true - validate unless: :for_commit? do |note| + validate unless: [:for_commit?, :importing?] do |note| unless note.noteable.try(:project) == note.project errors.add(:invalid_project, 'Note and noteable project mismatch') end diff --git a/app/models/project.rb b/app/models/project.rb index 8eef22356e2..ca3bc04e2dd 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -367,6 +367,11 @@ class Project < ActiveRecord::Base joins(join_body).reorder('join_note_counts.amount DESC') end + + # Deletes gitlab project export files older than 24 hours + def remove_gitlab_exports! + Gitlab::Popen.popen(%W(find #{Gitlab::ImportExport.storage_path} -not -path #{Gitlab::ImportExport.storage_path} -mmin +1440 -delete)) + end end def team @@ -470,7 +475,7 @@ class Project < ActiveRecord::Base end def import? - external_import? || forked? + external_import? || forked? || gitlab_project_import? end def no_import? @@ -501,6 +506,10 @@ class Project < ActiveRecord::Base Gitlab::UrlSanitizer.new(import_url).masked_url end + def gitlab_project_import? + import_type == 'gitlab_project' + end + def check_limit unless creator.can_create_project? or namespace.kind == 'group' projects_limit = creator.projects_limit @@ -1096,4 +1105,27 @@ class Project < ActiveRecord::Base ensure @errors = original_errors end + + def add_export_job(current_user:) + job_id = ProjectExportWorker.perform_async(current_user.id, self.id) + + if job_id + Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}" + else + Rails.logger.error "Export job failed to start for project ID #{self.id}" + end + end + + def export_path + File.join(Gitlab::ImportExport.storage_path, path_with_namespace) + end + + def export_project_path + Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) } + end + + def remove_exports + _, status = Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete)) + status.zero? + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 65d1bad511d..bbd7682d8e7 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -598,6 +598,21 @@ class Repository end end + def tags_sorted_by(value) + case value + when 'name' + # Would be better to use `sort_by` but `version_sorter` only exposes + # `sort` and `rsort` + VersionSorter.rsort(tag_names).map { |tag_name| find_tag(tag_name) } + when 'updated_desc' + tags_sorted_by_committed_date.reverse + when 'updated_asc' + tags_sorted_by_committed_date + else + tags + end + end + def contributors commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true) @@ -995,4 +1010,8 @@ class Repository def file_on_head(regex) tree(:head).blobs.find { |file| file.name =~ regex } end + + def tags_sorted_by_committed_date + tags.sort_by { |tag| commit(tag.target).committed_date } + end end diff --git a/app/models/user.rb b/app/models/user.rb index 051745fe252..2e458329cb9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -827,6 +827,23 @@ class User < ActiveRecord::Base assigned_open_issues_count(force: true) end + def todos_done_count(force: false) + Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do + todos.done.count + end + end + + def todos_pending_count(force: false) + Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do + todos.pending.count + end + end + + def update_todos_count_cache + todos_done_count(force: true) + todos_pending_count(force: true) + end + private def projects_union(min_access_level = nil) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index e455101f526..19832a19b2b 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -230,7 +230,7 @@ class NotificationService end def accept_group_invite(group_member) - mailer.member_invite_accepted_email(group_member.id).deliver_later + mailer.member_invite_accepted_email(group_member.real_source_type, group_member.id).deliver_later end def decline_group_invite(group_member) @@ -274,6 +274,14 @@ class NotificationService end end + def project_exported(project, current_user) + mailer.project_was_exported_email(current_user, project).deliver_later + end + + def project_not_exported(project, current_user, errors) + mailer.project_was_not_exported_email(current_user, project, errors).deliver_later + end + protected # Get project/group users with CUSTOM notification level diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 61cac5419ad..55956be2844 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -80,16 +80,18 @@ module Projects def after_create_actions log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") - @project.create_wiki if @project.wiki_enabled? + unless @project.gitlab_project_import? + @project.create_wiki if @project.wiki_enabled? - @project.build_missing_services + @project.build_missing_services - @project.create_labels + @project.create_labels + end event_service.create_project(@project, current_user) system_hook_service.execute_hooks_for(@project, :create) - unless @project.group + unless @project.group || @project.gitlab_project_import? @project.team << [current_user, :master, current_user] end end diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb new file mode 100644 index 00000000000..d6752377ce5 --- /dev/null +++ b/app/services/projects/import_export/export_service.rb @@ -0,0 +1,57 @@ +module Projects + module ImportExport + class ExportService < BaseService + + def execute(_options = {}) + @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work')) + save_all + end + + private + + def save_all + if [version_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save) + Gitlab::ImportExport::Saver.save(shared: @shared) + notify_success + else + cleanup_and_notify + end + end + + def version_saver + Gitlab::ImportExport::VersionSaver.new(shared: @shared) + end + + def project_tree_saver + Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared) + end + + def uploads_saver + Gitlab::ImportExport::UploadsSaver.new(project: project, shared: @shared) + end + + def repo_saver + Gitlab::ImportExport::RepoSaver.new(project: project, shared: @shared) + end + + def wiki_repo_saver + Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared) + end + + def cleanup_and_notify + FileUtils.rm_rf(@shared.export_path) + + notify_error + raise Gitlab::ImportExport::Error.new(@shared.errors.join(', ')) + end + + def notify_success + notification_service.project_exported(@project, @current_user) + end + + def notify_error + notification_service.project_not_exported(@project, @current_user, @shared.errors.join(', ')) + end + end + end +end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index c4838d31f2f..9159ec08959 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -9,26 +9,31 @@ module Projects 'fogbugz', 'gitlab', 'github', - 'google_code' + 'google_code', + 'gitlab_project' ] def execute - if unknown_url? - # In this case, we only want to import issues, not a repository. - create_repository - else - import_repository - end + add_repository_to_project unless project.gitlab_project_import? import_data success - rescue Error => e + rescue => e error(e.message) end private + def add_repository_to_project + if unknown_url? + # In this case, we only want to import issues, not a repository. + create_repository + else + import_repository + end + end + def create_repository unless project.create_repository raise Error, 'The repository could not be created.' @@ -46,7 +51,7 @@ module Projects def import_data return unless has_importer? - project.repository.before_import + project.repository.before_import unless project.gitlab_project_import? unless importer.execute raise Error, 'The remote data could not be imported.' @@ -58,6 +63,8 @@ module Projects end def importer + return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import? + class_name = "Gitlab::#{project.import_type.camelize}Import::Importer" class_name.constantize.new(project) end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index e1f9ea64dc4..540bf54b920 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -1,6 +1,6 @@ # TodoService class # -# Used for creating todos after certain user actions +# Used for creating/updating todos after certain user actions # # Ex. # TodoService.new.new_issue(issue, current_user) @@ -137,6 +137,15 @@ class TodoService def mark_pending_todos_as_done(target, user) attributes = attributes_for_target(target) pending_todos(user, attributes).update_all(state: :done) + user.update_todos_count_cache + end + + # When user marks some todos as done + def mark_todos_as_done(todos, current_user) + todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all) + + todos.update_all(state: :done) + current_user.update_todos_count_cache end # When user marks an issue as todo @@ -151,6 +160,7 @@ class TodoService Array(users).map do |user| next if pending_todos(user, attributes).exists? Todo.create(attributes.merge(user_id: user.id)) + user.update_todos_count_cache end end @@ -161,11 +171,16 @@ class TodoService def update_issuable(issuable, author) # Skip toggling a task list item in a description - return if issuable.tasks? && issuable.updated_tasks.any? + return if toggling_tasks?(issuable) create_mention_todos(issuable.project, issuable, author) end + def toggling_tasks?(issuable) + issuable.previous_changes.include?('description') && + issuable.tasks? && issuable.updated_tasks.any? + end + def handle_note(note, author) # Skip system notes, and notes on project snippet return if note.system? || note.for_snippet? diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 6c4a9d68d1f..7486b1423e2 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 00000000000..44e2653ca4a --- /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/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index a851cae4b56..39ea4920ccc 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 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 00000000000..b28fea35ad5 --- /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 00000000000..42c4d176876 --- /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 00000000000..c9e9ade2cf1 --- /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 00000000000..a07f6edacf7 --- /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 3d2a245ecbd..8efe486e01b 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/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index b117517c0dd..3ad866bb2f1 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -6,10 +6,10 @@ .pull-right.commit-action-buttons - if defined?(@notes_count) && @notes_count > 0 - %span.btn.disabled.btn-grouped.hidden-xs + %span.btn.disabled.btn-grouped.hidden-xs.append-right-10 = icon('comment') = @notes_count - = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped hidden-xs hidden-sm" do + = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-default append-right-10 hidden-xs hidden-sm" do Browse Files .dropdown.inline %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } } diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 8449fe1e4e0..27a94fe02dc 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/imports/show.html.haml b/app/views/projects/imports/show.html.haml index c0d1ce0d120..4d8ee562e6a 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/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index 0dbd159298e..b3bea900d42 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/new.html.haml b/app/views/projects/new.html.haml index 7e8b8f83467..3c1c6060504 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -84,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 @@ -115,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/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 2779084fe38..4ca1f58ac5c 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/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 380ab465bf4..094d6636c66 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 210c9b9aab5..adfab1af53e 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| diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml index ed0a6ebcf84..480e8ba6c85 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 0191814849a..a884e78e6e7 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -1,5 +1,4 @@ -- default_show_roles = can?(current_user, action_member_permission(:update, member), member) || can?(current_user, action_member_permission(:destroy, member), member) -- show_roles = local_assigns.fetch(:show_roles, default_show_roles) +- show_roles = local_assigns.fetch(:show_roles, default_show_roles(member)) - show_controls = local_assigns.fetch(:show_controls, true) - user = member.user 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 00000000000..1d91897d520 --- /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 00000000000..39f6037e077 --- /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 f2d12ba5a7d..98ddf5d0688 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/bin/spring b/bin/spring index 7fe232c3aae..e0d140fe0c7 100755 --- a/bin/spring +++ b/bin/spring @@ -3,7 +3,7 @@ # This file loads spring without using Bundler, in order to be fast. # It gets overwritten when you run the `spring binstub` command. -unless defined?(Spring) +unless (defined?(Spring) || ENV['ENABLE_SPRING'] != '1') && File.basename($0) != 'spring' require 'rubygems' require 'bundler' diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 916fd33e767..09ffc319065 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -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/routes.rb b/config/routes.rb index c2be083c34c..de6094fa0ed 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -179,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 # @@ -469,6 +473,10 @@ 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 end @@ -810,7 +818,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/doc/api/README.md b/doc/api/README.md index 71bb01e0d51..73f44603688 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) @@ -44,10 +45,10 @@ The following documentation is for the [internal CI API](ci/README.md): ## Authentication -All API requests require authentication via a token. There are three types of tokens +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 a token is invalid or omitted, an error message will be returned with +If a token is invalid or omitted, an error message will be returned with status code `401`: ```json @@ -58,8 +59,8 @@ 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 +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`). @@ -80,7 +81,7 @@ Read more about [GitLab as an OAuth2 client](oauth2.md). > **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 +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. diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md new file mode 100644 index 00000000000..b44f8cfd628 --- /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 00000000000..ebd131c94ca --- /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/development/instrumentation.md b/doc/development/instrumentation.md index 6cd9b274d11..c2272ab0a2b 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/features/dashboard/new_project.feature b/features/dashboard/new_project.feature index 76392068357..56b4a639c01 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 8677b450813..42f5d6d2af7 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/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index 5308e77fb19..29e6b9f1a01 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -20,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 @@ -37,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 19fedfbfcdf..60152d3da55 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/lib/api/api.rb b/lib/api/api.rb index 6cd909f6115..0e7a1cc2623 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::Gitignores mount ::API::GroupMembers - mount ::API::Users - mount ::API::Projects - mount ::API::Repositories + mount ::API::Groups + mount ::API::Internal mount ::API::Issues - mount ::API::Milestones - mount ::API::Session + mount ::API::Keys + mount ::API::Labels + mount ::API::Licenses mount ::API::MergeRequests + mount ::API::Milestones + mount ::API::Namespaces mount ::API::Notes - mount ::API::Internal - mount ::API::SystemHooks - mount ::API::ProjectSnippets - mount ::API::ProjectMembers - mount ::API::DeployKeys mount ::API::ProjectHooks + mount ::API::ProjectMembers + mount ::API::ProjectSnippets + mount ::API::Projects + mount ::API::Repositories + mount ::API::Runners mount ::API::Services - mount ::API::Files - mount ::API::Commits - mount ::API::CommitStatuses - mount ::API::Namespaces - mount ::API::Branches - mount ::API::Labels + mount ::API::Session mount ::API::Settings - mount ::API::Keys + mount ::API::SidekiqMetrics + mount ::API::Subscriptions + mount ::API::SystemHooks mount ::API::Tags mount ::API::Triggers - mount ::API::Builds + mount ::API::Users mount ::API::Variables - mount ::API::Runners - mount ::API::Licenses - mount ::API::Subscriptions - mount ::API::Gitignores end end diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb new file mode 100644 index 00000000000..985590312e3 --- /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 cc29c7ef428..2e397643ed1 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 diff --git a/lib/api/notes.rb b/lib/api/notes.rb index d4fcfd3d4d3..8bfa998dc53 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/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb new file mode 100644 index 00000000000..d3d6827dc54 --- /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/container_registry/client.rb b/lib/container_registry/client.rb index e0b3f14d384..42232b7129d 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/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 5e7532f57ae..28c34429c1f 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/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index dd3ff0ab18b..dec20d8659b 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) - - ## - # 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 - - total = exec_query("SELECT COUNT(*) AS count FROM #{quoted_table}"). - to_hash. - first['count']. - to_i + table = Arel::Table.new(table) + + count_arel = table.project(Arel.star.count.as('count')) + count_arel = yield table, count_arel if block_given? + + 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 - - # There are no more rows to process - break unless start_row - - 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']} - } + stop_arel = table.project(table[:id]). + where(table[:id].gteq(start_id)). + order(table[:id].asc). + take(1). + skip(batch_size) + + stop_arel = yield table, stop_arel if block_given? + stop_row = exec_query(stop_arel.to_sql).to_hash.first + + 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? + + execute(update_arel.to_sql) - processed += batch_size + # 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/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index e5cf66a0371..2286ac8829c 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/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb index 77c33db4b59..3d0418261bb 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 00000000000..624c1766024 --- /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 + '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 00000000000..d230de781d5 --- /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 00000000000..78664f076eb --- /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 00000000000..e341c4d9cf8 --- /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 00000000000..0e70d9282d5 --- /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 00000000000..164ab6238c4 --- /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 00000000000..d209e04f7be --- /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 00000000000..c569a35a48b --- /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 00000000000..89388d1984b --- /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 00000000000..dd71b92c522 --- /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 00000000000..9153088e966 --- /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 00000000000..19defd8f03a --- /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 00000000000..b872780f20a --- /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 00000000000..546dae4d122 --- /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 00000000000..cce43fe994b --- /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 00000000000..f38229c6c59 --- /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 00000000000..6aff05b886a --- /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 00000000000..df19354b76e --- /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 00000000000..7292e9d9712 --- /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 00000000000..cf5c62c5e3c --- /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 00000000000..f7f73dc9343 --- /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 00000000000..1eedae39f8a --- /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 ccfdfbe73e8..948d43582cf 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/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index d81d26754fe..dcec7543c13 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 00000000000..faf0d9b6318 --- /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 3fe27779d03..e61670f491c 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 2578ddc49f4..4bc5081aa03 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 + + # 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 - @metrics << Metric.new("#{prefix}#{series}", values, tags) + @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/spec/controllers/profiles/accounts_controller_spec.rb b/spec/controllers/profiles/accounts_controller_spec.rb new file mode 100644 index 00000000000..4eafc11abaa --- /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/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb new file mode 100644 index 00000000000..40a3403b660 --- /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/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb index b69cce3e7d7..bc0f437a8ce 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/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb new file mode 100644 index 00000000000..c5fb0fc783b --- /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 Binary files differnew file mode 100644 index 00000000000..1fd04416d95 --- /dev/null +++ b/spec/features/projects/import_export/test_project_export.tar.gz 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 00000000000..728c0e16361 --- /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 00000000000..4d5d656f00c --- /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 00000000000..c4ed92d2780 --- /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/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 8e1833a069e..0bdb1628c74 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/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb index 7998209b7b0..f75fdb739f6 100644 --- a/spec/helpers/members_helper_spec.rb +++ b/spec/helpers/members_helper_spec.rb @@ -9,6 +9,54 @@ describe MembersHelper do it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member } end + describe '#default_show_roles' do + let(:user) { double } + let(:member) { build(:project_member) } + + 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 let(:requester) { build(:user) } let(:project) { create(:project) } diff --git a/spec/lib/container_registry/repository_spec.rb b/spec/lib/container_registry/repository_spec.rb index 279709521c9..c364e759108 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 1ec539066a7..9096ad101b0 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 00000000000..f135a285dfb --- /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 00000000000..400d44ac162 --- /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 00000000000..7a40a43f8ae --- /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 00000000000..8d29b2f8fd1 --- /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 00000000000..109522fa626 --- /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 00000000000..590a9a7e1a5 --- /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 00000000000..b9ffc8694a5 --- /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/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index cdf641341cb..8809b7e3f12 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 00000000000..8d05081eecb --- /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 40289f8b972..f264ed64029 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 1d5a51a157e..3b1c67a2147 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/models/repository_spec.rb b/spec/models/repository_spec.rb index 8c2347992f1..d8350000bf6 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/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb new file mode 100644 index 00000000000..2e65e7f1920 --- /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/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb new file mode 100644 index 00000000000..41cbf0c6669 --- /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/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 26f09cdbaf9..b4522536724 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) + + 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 - service.update_issue(issue, author) + it 'does not raise an error when description not change' do + issue.update(title: 'Sample') - 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) + 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 00000000000..3ceec506401 --- /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/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb index 5a03bb77ebd..05e07789dac 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 |