diff options
author | Douwe Maan <douwe@gitlab.com> | 2015-04-03 15:29:27 +0200 |
---|---|---|
committer | Douwe Maan <douwe@gitlab.com> | 2015-04-03 15:29:27 +0200 |
commit | 7b5bc32cadbf2c0a3ac1e80643e46786fd8b1b56 (patch) | |
tree | 0dfa9add1156d8ce9ff8709e36da577b7c94ad1c | |
parent | 9157985cfce1391973673ea278dc7506a90f8f53 (diff) | |
download | gitlab-ce-7b5bc32cadbf2c0a3ac1e80643e46786fd8b1b56.tar.gz |
Allow projects to be imported from Google Code.
21 files changed, 1170 insertions, 14 deletions
diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index ccba65e3fd5..216f25cdcd5 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -119,6 +119,22 @@ li { line-height: 1.5; } + + a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] { + &:before { + margin-right: 4px; + + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + content: "\f0c6"; + } + + &:hover:before { + text-decoration: none; + } + } } @mixin str-truncated($max_width: 82%) { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index d66093bc2e5..facd7e19314 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -62,20 +62,8 @@ ul.notes { word-wrap: break-word; @include md-typography; - a[href*="/uploads/"] { - &:before { - margin-right: 4px; - - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - content: "\f0c6"; - } - - &:hover:before { - text-decoration: none; - } + hr { + margin: 10px 0; } } } diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb new file mode 100644 index 00000000000..9542a33193a --- /dev/null +++ b/app/controllers/import/google_code_controller.rb @@ -0,0 +1,63 @@ +class Import::GoogleCodeController < Import::BaseController + + def new + + end + + def callback + dump_file = params[:dump_file] + + unless dump_file.respond_to?(:read) + return redirect_to :back, alert: "You need to upload a Google Takeout JSON file." + end + + begin + dump = JSON.parse(dump_file.read) + rescue + return redirect_to :back, alert: "The uploaded file is not a valid Google Takeout JSON file." + end + + unless Gitlab::GoogleCodeImport::Client.new(dump).valid? + return redirect_to :back, alert: "The uploaded file is not a valid Google Takeout JSON file." + end + + session[:google_code_dump] = dump + redirect_to status_import_google_code_path + end + + def status + unless client.valid? + return redirect_to new_import_google_path + end + + @repos = client.repos + + @already_added_projects = current_user.created_projects.where(import_type: "google_code") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos.reject! { |repo| already_added_projects_names.include? repo.name } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "google_code").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @repo_id = params[:repo_id] + repo = client.repo(@repo_id) + @target_namespace = current_user.namespace + @project_name = repo.name + + namespace = @target_namespace + + @project = Gitlab::GoogleCodeImport::ProjectCreator.new(repo, namespace, current_user).execute + end + + private + + def client + @client ||= Gitlab::GoogleCodeImport::Client.new(session[:google_code_dump]) + end + +end diff --git a/app/models/project.rb b/app/models/project.rb index 79572f255db..9b0d5c509b4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -27,6 +27,7 @@ # import_type :string(255) # import_source :string(255) # avatar :string(255) +# import_data :text # require 'carrierwave/orm/activerecord' @@ -50,6 +51,8 @@ class Project < ActiveRecord::Base default_value_for :wall_enabled, false default_value_for :snippets_enabled, gitlab_config_features.snippets + serialize :import_data, JSON + # set last_activity_at to the same as created_at after_create :set_last_activity_at def set_last_activity_at @@ -185,6 +188,7 @@ class Project < ActiveRecord::Base state :failed after_transition any => :started, do: :add_import_job + after_transition any => :finished, do: :clear_import_data end class << self @@ -262,6 +266,11 @@ class Project < ActiveRecord::Base RepositoryImportWorker.perform_in(2.seconds, id) end + def clear_import_data + self.import_data = nil + self.save + end + def import? import_url.present? end diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml new file mode 100644 index 00000000000..8df5b5d31f4 --- /dev/null +++ b/app/views/import/google_code/new.html.haml @@ -0,0 +1,17 @@ +%h3.page-title + %i.fa.fa-google + Import projects from Google Code +%hr + += form_tag callback_import_google_code_path, class: 'form-horizontal', multipart: true do + %ul + %li + Use Google Takeout etc + + .form-group + = label_tag :dump_file, "Google Takeout JSON file", class: 'control-label' + .col-sm-10 + %input{type: "file", name: "dump_file", id: "dump_file"} + + .form-actions + = submit_tag 'Select projects to import', class: "btn btn-create" diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml new file mode 100644 index 00000000000..eba9c5296bc --- /dev/null +++ b/app/views/import/google_code/status.html.haml @@ -0,0 +1,45 @@ +%h3.page-title + %i.fa.fa-google + Import projects from Google Code + +%p.light + Select projects you want to import. +%hr +%p + = button_tag 'Import all projects', class: "btn btn-success js-import-all" + +%table.table.import-jobs + %thead + %tr + %th From Google Code + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank" + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" + %td.import-target + = "#{current_user.username}/#{repo.name}" + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" + +:coffeescript + new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}") diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 69909a8554e..a06c85b4251 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -62,6 +62,10 @@ %i.icon-gitorious.icon-gitorious-small Gitorious.org + = link_to new_import_google_code_path, class: 'btn' do + %i.fa.fa-google + Google Code + = link_to "#", class: 'btn js-toggle-button' do %i.fa.fa-git %span Any repo by URL diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 437640d2305..e6a50afedb1 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -18,6 +18,8 @@ class RepositoryImportWorker Gitlab::GitlabImport::Importer.new(project).execute elsif project.import_type == 'bitbucket' Gitlab::BitbucketImport::Importer.new(project).execute + elsif project.import_type == 'google_code' + Gitlab::GoogleCodeImport::Importer.new(project).execute else true end diff --git a/config/routes.rb b/config/routes.rb index 388858d2670..88e0227121b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -81,6 +81,12 @@ Gitlab::Application.routes.draw do get :callback get :jobs end + + resource :google_code, only: [:create, :new], controller: :google_code do + get :status + post :callback + get :jobs + end end # diff --git a/db/migrate/20150327150017_add_import_data_to_project.rb b/db/migrate/20150327150017_add_import_data_to_project.rb new file mode 100644 index 00000000000..12c00339eec --- /dev/null +++ b/db/migrate/20150327150017_add_import_data_to_project.rb @@ -0,0 +1,5 @@ +class AddImportDataToProject < ActiveRecord::Migration + def change + add_column :projects, :import_data, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 14e32a7946e..390cd4f36d8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -342,6 +342,7 @@ ActiveRecord::Schema.define(version: 20150328132231) do t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" + t.text "import_data" end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree diff --git a/lib/gitlab/google_code_import/client.rb b/lib/gitlab/google_code_import/client.rb new file mode 100644 index 00000000000..0514494d3ad --- /dev/null +++ b/lib/gitlab/google_code_import/client.rb @@ -0,0 +1,23 @@ +module Gitlab + module GoogleCodeImport + class Client + attr_reader :raw_data + + def initialize(raw_data) + @raw_data = raw_data + end + + def valid? + raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#user" && raw_data.has_key?("projects") + end + + def repos + @repos ||= raw_data["projects"].map { |raw_repo| GoogleCodeImport::Repository.new(raw_repo) }.select(&:git?) + end + + def repo(id) + repos.find { |repo| repo.id == id } + end + end + end +end diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb new file mode 100644 index 00000000000..da0f9a10b03 --- /dev/null +++ b/lib/gitlab/google_code_import/importer.rb @@ -0,0 +1,327 @@ +module Gitlab + module GoogleCodeImport + class Importer + attr_reader :project, :repo + + def initialize(project) + @project = project + @repo = GoogleCodeImport::Repository.new(project.import_data) + + @closed_statuses = [] + @known_labels = Set.new + end + + def execute + return true unless repo.valid? + + import_status_labels + + import_labels + + import_issues + + true + end + + private + + def import_status_labels + repo.raw_data["issuesConfig"]["statuses"].each do |status| + closed = !status["meansOpen"] + @closed_statuses << status["status"] if closed + + name = nice_status_name(status["status"]) + create_label(name) + @known_labels << name + end + end + + def import_labels + repo.raw_data["issuesConfig"]["labels"].each do |label| + name = nice_label_name(label["label"]) + create_label(name) + @known_labels << name + end + end + + def import_issues + return unless repo.raw_data["issues"] + + last_id = 0 + + deleted_issues = [] + + issues = repo.raw_data["issues"]["items"] + issues.each_with_index do |raw_issue, i| + while raw_issue["id"] > last_id + 1 + last_id += 1 + + issue = project.issues.create!( + title: "Deleted issue", + description: "*This issue has been deleted*", + author_id: project.creator_id, + state: "closed" + ) + deleted_issues << issue + end + last_id = raw_issue["id"] + + author = mask_email(raw_issue["author"]["name"]) + author_link = raw_issue["author"]["htmlLink"] + date = DateTime.parse(raw_issue["published"]).to_formatted_s(:long) + + body = [] + body << "*By [#{author}](#{author_link}) on #{date}*" + body << "---" + + comments = raw_issue["comments"]["items"] + + issue_comment = comments.shift + + content = format_content(issue_comment["content"]) + if content.blank? + content = "*(No description has been entered for this issue)*" + end + body << content + + attachments = format_attachments(raw_issue["id"], 0, issue_comment["attachments"]) + if attachments.any? + body << "---" + body += attachments + end + + labels = [] + raw_issue["labels"].each do |label| + name = nice_label_name(label) + labels << name + + unless @known_labels.include?(name) + create_label(name) + @known_labels << name + end + end + labels << nice_status_name(raw_issue["status"]) + + issue = project.issues.create!( + title: raw_issue["title"], + description: body.join("\n\n"), + author_id: project.creator_id, + state: raw_issue["state"] == "closed" ? "closed" : "opened" + ) + issue.add_labels_by_names(labels) + + import_issue_comments(issue, comments) + end + + deleted_issues.each(&:destroy!) + end + + def import_issue_comments(issue, comments) + comments.each_with_index do |raw_comment, i| + next if raw_comment.has_key?("deletedBy") + + author = mask_email(raw_comment["author"]["name"]) + author_link = raw_comment["author"]["htmlLink"] + date = DateTime.parse(raw_comment["published"]).to_formatted_s(:long) + + body = [] + body << "*By [#{author}](#{author_link}) on #{date}*" + body << "---" + + content = format_content(raw_comment["content"]) + if content.blank? + content = "*(No comment has been entered for this change)*" + end + body << content + + updates = format_updates(raw_comment["updates"]) + if updates.any? + body << "---" + body += updates + end + + attachments = format_attachments(issue.iid, raw_comment["id"], raw_comment["attachments"]) + if attachments.any? + body << "---" + body += attachments + end + + comment = issue.notes.create!( + project_id: project.id, + author_id: project.creator_id, + note: body.join("\n\n") + ) + end + end + + def nice_label_color(name) + case name + when /\AComponent:/ + "#fff39e" + when /\AOpSys:/ + "#e2e2e2" + when /\AMilestone:/ + "#fee3ff" + + when *@closed_statuses.map { |s| nice_status_name(s) } + "#cfcfcf" + when "Status: New" + "#428bca" + when "Status: Accepted" + "#5cb85c" + when "Status: NeedInfo" + "#f0ad4e" + when "Status: Started" + "#8e44ad" + when "Status: Wishlist" + "#a8d695" + # + when "Priority: Critical" + "#ffcfcf" + when "Priority: High" + "#deffcf" + when "Priority: Medium" + "#fff5cc" + when "Priority: Low" + "#cfe9ff" + # + when "Type: Defect" + "#d9534f" + when "Type: Enhancement" + "#44ad8e" + when "Type: Other" + "#7f8c8d" + when "Type: Review" + "#8e44ad" + when "Type: Task" + "#4b6dd0" + else + "#e2e2e2" + end + end + + def nice_label_name(name) + name.sub("-", ": ") + end + + def nice_status_name(name) + "Status: #{name}" + end + + def mask_email(author) + parts = author.split("@", 2) + parts[0] = "#{parts[0][0...-3]}..." + parts.join("@") + end + + def linkify_issues(s) + s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2') + end + + def escape_for_markdown(s) + s = s.gsub("*", "\\*") + s = s.gsub("#", "\\#") + s = s.gsub("`", "\\`") + s = s.gsub(":", "\\:") + s = s.gsub("-", "\\-") + s = s.gsub("+", "\\+") + s = s.gsub("_", "\\_") + s = s.gsub("(", "\\(") + s = s.gsub(")", "\\)") + s = s.gsub("[", "\\[") + s = s.gsub("]", "\\]") + s = s.gsub("<", "\\<") + s = s.gsub(">", "\\>") + s = s.gsub("\r", "") + s = s.gsub("\n", " \n") + s + end + + def create_label(name) + color = nice_label_color(name) + project.labels.create!(name: name, color: color) + end + + def format_content(raw_content) + linkify_issues(escape_for_markdown(raw_content)) + end + + def format_updates(raw_updates) + updates = [] + + if raw_updates.has_key?("status") + updates << "*Status: #{raw_updates["status"]}*" + end + + if raw_updates.has_key?("cc") + cc = raw_updates["cc"].map do |l| + deleted = l.start_with?("-") + l = l[1..-1] if deleted + l = mask_email(l) + l = "~~#{l}~~" if deleted + l + end + + updates << "*Cc: #{cc.join(", ")}*" + end + + if raw_updates.has_key?("labels") + labels = raw_updates["labels"].map do |l| + deleted = l.start_with?("-") + l = l[1..-1] if deleted + l = nice_label_name(l) + l = "~~#{l}~~" if deleted + l + end + + updates << "*Labels: #{labels.join(", ")}*" + end + + if raw_updates.has_key?("owner") + updates << "*Owner: #{raw_updates["owner"]}*" + end + + if raw_updates.has_key?("mergedInto") + updates << "*Merged into: ##{raw_updates["mergedInto"]}*" + end + + if raw_updates.has_key?("blockedOn") + blocked_ons = raw_updates["blockedOn"].map do |raw_blocked_on| + name, id = raw_blocked_on.split(":", 2) + if name == project.import_source + "##{id}" + else + "#{project.namespace.path}/#{name}##{id}" + end + end + updates << "*Blocked on: #{blocked_ons.join(", ")}*" + end + + if raw_updates.has_key?("blocking") + blockings = raw_updates["blocking"].map do |raw_blocked_on| + name, id = raw_blocked_on.split(":", 2) + if name == project.import_source + "##{id}" + else + "#{project.namespace.path}/#{name}##{id}" + end + end + updates << "*Blocking: #{blockings.join(", ")}*" + end + + updates + end + + def format_attachments(issue_id, comment_id, raw_attachments) + return [] unless raw_attachments + + raw_attachments.map do |attachment| + next if attachment["isDeleted"] + + link = "https://storage.googleapis.com/google-code-attachments/#{@repo.name}/issue-#{issue_id}/comment-#{comment_id}/#{attachment["fileName"]}" + "[#{attachment["fileName"]}](#{link})" + end.compact + end + end + end +end diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb new file mode 100644 index 00000000000..933e144d8ea --- /dev/null +++ b/lib/gitlab/google_code_import/project_creator.rb @@ -0,0 +1,40 @@ +module Gitlab + module GoogleCodeImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user + + def initialize(repo, namespace, current_user) + @repo = repo + @namespace = namespace + @current_user = current_user + end + + def execute + @project = Project.new( + name: repo.name, + path: repo.name, + description: repo.summary, + namespace: namespace, + creator: current_user, + visibility_level: Gitlab::VisibilityLevel::PUBLIC, + import_type: "google_code", + import_source: repo.name, + import_url: repo.import_url, + import_data: repo.raw_data + ) + + if @project.save! + @project.reload + + if @project.import_failed? + @project.import_retry + else + @project.import_start + end + end + + @project + end + end + end +end diff --git a/lib/gitlab/google_code_import/repository.rb b/lib/gitlab/google_code_import/repository.rb new file mode 100644 index 00000000000..39a884d8292 --- /dev/null +++ b/lib/gitlab/google_code_import/repository.rb @@ -0,0 +1,39 @@ +module Gitlab + module GoogleCodeImport + class Repository + attr_accessor :raw_data + + def initialize(raw_data) + @raw_data = raw_data + end + + def valid? + raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#project" + end + + def id + raw_data["externalId"] + end + + def name + raw_data["name"] + end + + def summary + raw_data["summary"] + end + + def description + raw_data["description"] + end + + def git? + raw_data["versionControlSystem"] == "git" + end + + def import_url + raw_data["repositoryUrls"].first + end + end + end +end diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb new file mode 100644 index 00000000000..037cddb4600 --- /dev/null +++ b/spec/controllers/import/google_code_controller_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Import::GoogleCodeController do + let(:user) { create(:user) } + let(:dump_file) { fixture_file_upload(Rails.root + 'spec/fixtures/GoogleCodeProjectHosting.json', 'application/json') } + + before do + sign_in(user) + end + + describe "POST callback" do + it "stores Google Takeout dump list in session" do + post :callback, dump_file: dump_file + + expect(session[:google_code_dump]).to be_a(Hash) + expect(session[:google_code_dump]["kind"]).to eq("projecthosting#user") + expect(session[:google_code_dump]).to have_key("projects") + end + end + + describe "GET status" do + before do + @repo = OpenStruct.new(name: 'vim') + controller.stub_chain(:client, :valid?).and_return(true) + end + + it "assigns variables" do + @project = create(:project, import_type: 'google_code', creator_id: user.id) + controller.stub_chain(:client, :repos).and_return([@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([@repo]) + end + + it "does not show already added project" do + @project = create(:project, import_type: 'google_code', creator_id: user.id, import_source: 'vim') + controller.stub_chain(:client, :repos).and_return([@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([]) + end + end +end diff --git a/spec/fixtures/GoogleCodeProjectHosting.json b/spec/fixtures/GoogleCodeProjectHosting.json new file mode 100644 index 00000000000..a1d8d32adf3 --- /dev/null +++ b/spec/fixtures/GoogleCodeProjectHosting.json @@ -0,0 +1,397 @@ +{ + "kind" : "projecthosting#user", + "id" : "@WRRVSlFXARlCVgB6", + "projects" : [ { + "kind" : "projecthosting#project", + "name" : "pmn", + "externalId" : "pmn", + "htmlLink" : "/p/pmn/", + "summary" : "Shows an icon in the system tray when you have new emails", + "description" : "IMAP client that shows an icon in the system tray when you have new emails.", + "labels" : [ "Mail" ], + "versionControlSystem" : "svn", + "repositoryUrls" : [ "https://pmn.googlecode.com/svn/" ], + "issuesConfig" : { + "kind" : "projecthosting#projectIssueConfig", + "statuses" : [ { + "status" : "New", + "meansOpen" : true, + "description" : "Issue has not had initial review yet" + }, { + "status" : "Accepted", + "meansOpen" : true, + "description" : "Problem reproduced / Need acknowledged" + }, { + "status" : "Started", + "meansOpen" : true, + "description" : "Work on this issue has begun" + }, { + "status" : "Fixed", + "meansOpen" : false, + "description" : "Developer made source code changes, QA should verify" + }, { + "status" : "Verified", + "meansOpen" : false, + "description" : "QA has verified that the fix worked" + }, { + "status" : "Invalid", + "meansOpen" : false, + "description" : "This was not a valid issue report" + }, { + "status" : "Duplicate", + "meansOpen" : false, + "description" : "This report duplicates an existing issue" + }, { + "status" : "WontFix", + "meansOpen" : false, + "description" : "We decided to not take action on this issue" + }, { + "status" : "Done", + "meansOpen" : false, + "description" : "The requested non-coding task was completed" + } ], + "labels" : [ { + "label" : "Type-Defect", + "description" : "Report of a software defect" + }, { + "label" : "Type-Enhancement", + "description" : "Request for enhancement" + }, { + "label" : "Type-Task", + "description" : "Work item that doesn't change the code or docs" + }, { + "label" : "Type-Review", + "description" : "Request for a source code review" + }, { + "label" : "Type-Other", + "description" : "Some other kind of issue" + }, { + "label" : "Priority-Critical", + "description" : "Must resolve in the specified milestone" + }, { + "label" : "Priority-High", + "description" : "Strongly want to resolve in the specified milestone" + }, { + "label" : "Priority-Medium", + "description" : "Normal priority" + }, { + "label" : "Priority-Low", + "description" : "Might slip to later milestone" + }, { + "label" : "OpSys-All", + "description" : "Affects all operating systems" + }, { + "label" : "OpSys-Windows", + "description" : "Affects Windows users" + }, { + "label" : "OpSys-Linux", + "description" : "Affects Linux users" + }, { + "label" : "OpSys-OSX", + "description" : "Affects Mac OS X users" + }, { + "label" : "Milestone-Release1.0", + "description" : "All essential functionality working" + }, { + "label" : "Component-UI", + "description" : "Issue relates to program UI" + }, { + "label" : "Component-Logic", + "description" : "Issue relates to application logic" + }, { + "label" : "Component-Persistence", + "description" : "Issue relates to data storage components" + }, { + "label" : "Component-Scripts", + "description" : "Utility and installation scripts" + }, { + "label" : "Component-Docs", + "description" : "Issue relates to end-user documentation" + }, { + "label" : "Security", + "description" : "Security risk to users" + }, { + "label" : "Performance", + "description" : "Performance issue" + }, { + "label" : "Usability", + "description" : "Affects program usability" + }, { + "label" : "Maintainability", + "description" : "Hinders future changes" + } ], + "prompts" : [ { + "name" : "Defect report from user", + "title" : "Enter one-line summary", + "description" : "What steps will reproduce the problem?\n1. \n2. \n3. \n\nWhat is the expected output? What do you see instead?\n\n\nWhat version of the product are you using? On what operating system?\n\n\nPlease provide any additional information below.\n", + "titleMustBeEdited" : true, + "status" : "New", + "labels" : [ "Type-Defect", "Priority-Medium" ] + }, { + "name" : "Defect report from developer", + "title" : "Enter one-line summary", + "description" : "What steps will reproduce the problem?\n1. \n2. \n3. \n\nWhat is the expected output? What do you see instead?\n\n\nPlease use labels and text to provide additional information.\n", + "titleMustBeEdited" : true, + "status" : "Accepted", + "labels" : [ "Type-Defect", "Priority-Medium" ], + "membersOnly" : true + }, { + "name" : "Review request", + "title" : "Code review request", + "description" : "Branch name:\n\nPurpose of code changes on this branch:\n\n\nWhen reviewing my code changes, please focus on:\n\n\nAfter the review, I'll merge this branch into:\n/trunk\n", + "status" : "New", + "labels" : [ "Type-Review", "Priority-Medium" ], + "membersOnly" : true, + "defaultToMember" : false + } ], + "defaultPromptForMembers" : 1, + "defaultPromptForNonMembers" : 0 + }, + "role" : "owner", + "members" : [ { + "kind" : "projecthosting#issuePerson", + "name" : "mrovi9000", + "htmlLink" : "https://code.google.com/u/106736353629303906862/" + } ], + "issues" : { + "kind" : "projecthosting#issueList", + "totalResults" : 0, + "items" : [ ] + } + }, { + "kind" : "projecthosting#project", + "name" : "tint2", + "externalId" : "tint2", + "htmlLink" : "/p/tint2/", + "summary" : "tint2 is a lightweight panel/taskbar.", + "description" : "tint2 is a simple _*panel/taskbar*_ unintrusive and light (memory / cpu / aestetic). <br>We follow freedesktop specifications.\r\n \r\n=== 0.11 features ===\r\n * panel with taskbar, systray, clock and battery status\r\n * easy to customize : color/transparency on font, icon, border and background\r\n * pager like capability : send task from one workspace to another, switch workspace\r\n * multi-monitor capability : one panel per monitor, show task from current monitor\r\n * customize mouse event\r\n * window manager's menu\r\n * tooltip\r\n * autohide\r\n * clock timezones\r\n * real & fake transparency with autodetection of composite manager\r\n * panel's theme switcher 'tint2conf' \r\n\r\n=== Other project ===\r\n * Lightweight volume control http://softwarebakery.com/maato/volumeicon.html\r\n * Lightweight calendar http://code.google.com/p/gsimplecal/\r\n * Graphical config tool http://code.google.com/p/tintwizard/\r\n * Command line theme switcher http://github.com/dbbolton/scripts/blob/master/tint2theme\r\n\r\n\r\n=== Snapshot SVN ===\r\n\r\nhttp://img252.imageshack.us/img252/1433/wallpaper2td.jpg\r\n\r\n\r\n", + "labels" : [ "taskbar", "panel", "lightweight", "desktop", "openbox", "pager", "tint2" ], + "versionControlSystem" : "git", + "repositoryUrls" : [ "https://tint2.googlecode.com/git/" ], + "issuesConfig" : { + "kind" : "projecthosting#projectIssueConfig", + "defaultColumns" : [ "ID", "Status", "Type", "Milestone", "Priority", "Component", "Owner", "Summary", "Modified", "Stars" ], + "defaultSorting" : [ "-ID" ], + "statuses" : [ { + "status" : "New", + "meansOpen" : true, + "description" : "Issue has not had initial review yet" + }, { + "status" : "NeedInfo", + "meansOpen" : true, + "description" : "More information is needed before deciding what action should be taken" + }, { + "status" : "Accepted", + "meansOpen" : true, + "description" : "A Defect that a developer has reproduced or an Enhancement that a developer has committed to addressing" + }, { + "status" : "Wishlist", + "meansOpen" : true, + "description" : "An Enhancement which is valid, but no developers have committed to addressing" + }, { + "status" : "Started", + "meansOpen" : true, + "description" : "Work on this issue has begun" + }, { + "status" : "Fixed", + "meansOpen" : false, + "description" : "Work has completed" + }, { + "status" : "Invalid", + "meansOpen" : false, + "description" : "This was not a valid issue report" + }, { + "status" : "Duplicate", + "meansOpen" : false, + "description" : "This report duplicates an existing issue" + }, { + "status" : "WontFix", + "meansOpen" : false, + "description" : "We decided to not take action on this issue" + }, { + "status" : "Incomplete", + "meansOpen" : false, + "description" : "Not enough information and no activity for a long period of time" + } ], + "labels" : [ { + "label" : "Type-Defect", + "description" : "Report of a software defect" + }, { + "label" : "Type-Enhancement", + "description" : "Request for enhancement" + }, { + "label" : "Type-Task", + "description" : "Work item that does not change the code" + }, { + "label" : "Type-Review", + "description" : "Request for a source code review" + }, { + "label" : "Type-Other", + "description" : "Some other kind of issue" + }, { + "label" : "Milestone-0.12", + "description" : "Fix should be included in release 0.12" + }, { + "label" : "Priority-Critical", + "description" : "Must resolve in the specified milestone" + }, { + "label" : "Priority-High", + "description" : "Strongly want to resolve in the specified milestone" + }, { + "label" : "Priority-Medium", + "description" : "Normal priority" + }, { + "label" : "Priority-Low", + "description" : "Might slip to later milestone" + }, { + "label" : "OpSys-All", + "description" : "Affects all operating systems" + }, { + "label" : "OpSys-Windows", + "description" : "Affects Windows users" + }, { + "label" : "OpSys-Linux", + "description" : "Affects Linux users" + }, { + "label" : "OpSys-OSX", + "description" : "Affects Mac OS X users" + }, { + "label" : "Security", + "description" : "Security risk to users" + }, { + "label" : "Performance", + "description" : "Performance issue" + }, { + "label" : "Usability", + "description" : "Affects program usability" + }, { + "label" : "Maintainability", + "description" : "Hinders future changes" + }, { + "label" : "Component-Panel", + "description" : "Issue relates to the panel (e.g. positioning, hiding, transparency)" + }, { + "label" : "Component-Taskbar", + "description" : "Issue relates to the taskbar (e.g. tasks, multiple desktops)" + }, { + "label" : "Component-Battery", + "description" : "Issue relates to the battery" + }, { + "label" : "Component-Systray", + "description" : "Issue relates to the system tray" + }, { + "label" : "Component-Clock", + "description" : "Issue relates to the clock" + }, { + "label" : "Component-Launcher", + "description" : "Issue relates to the launcher" + }, { + "label" : "Component-Tint2conf", + "description" : "Issue relates to the configuration GUI (tint2conf)" + }, { + "label" : "Component-Docs", + "description" : "Issue relates to end-user documentation" + }, { + "label" : "Component-New", + "description" : "Issue describes a new component proposal" + } ], + "prompts" : [ { + "name" : "Defect report from user", + "title" : "Enter one-line summary", + "description" : "What steps will reproduce the problem?\n1.\n2.\n3.\n\nWhat is the expected output? What do you see instead?\n\n\nWhat version of the product are you using? On what operating system?\n\n\nWhich window manager (e.g. openbox, xfwm, metacity, mutter, kwin) or\nwhich desktop environment (e.g. Gnome 2, Gnome 3, LXDE, XFCE, KDE)\nare you using?\n\n\nPlease provide any additional information below. It might be helpful\nto attach your tint2rc file (usually located at ~/.config/tint2/tint2rc).", + "titleMustBeEdited" : true, + "status" : "New", + "labels" : [ "Priority-Medium" ], + "defaultToMember" : true + }, { + "name" : "Defect report from developer", + "title" : "Enter one-line summary", + "description" : "What steps will reproduce the problem?\n1.\n2.\n3.\n\nWhat is the expected output? What do you see instead?\n\n\nPlease use labels and text to provide additional information.", + "titleMustBeEdited" : true, + "status" : "Accepted", + "labels" : [ "Type-Defect", "Priority-Medium" ], + "membersOnly" : true, + "defaultToMember" : true + }, { + "name" : "Review request", + "title" : "Code review request", + "description" : "Purpose of code changes on this branch:\n\n\nWhen reviewing my code changes, please focus on:\n\n\nAfter the review, I'll merge this branch into:\n/trunk", + "status" : "New", + "labels" : [ "Type-Review", "Priority-Medium" ], + "membersOnly" : true, + "defaultToMember" : true + } ], + "defaultPromptForMembers" : 1, + "defaultPromptForNonMembers" : 0, + "usersCanSetLabels" : false + }, + "role" : "owner", + "issues" : { + "kind" : "projecthosting#issueList", + "totalResults" : 473, + "items" : [ { + "kind" : "projecthosting#issue", + "id" : 169, + "title" : "Scrolling through tasks", + "summary" : "Scrolling through tasks", + "stars" : 1, + "starred" : false, + "status" : "Fixed", + "state" : "closed", + "labels" : [ "Type-Enhancement", "Priority-Medium" ], + "author" : { + "kind" : "projecthosting#issuePerson", + "name" : "schattenpr...", + "htmlLink" : "https://code.google.com/u/106498139506637530000/" + }, + "updated" : "2009-11-18T05:14:58.000Z", + "published" : "2009-11-18T00:20:19.000Z", + "closed" : "2009-11-18T05:14:58.000Z", + "projectId" : "tint2", + "canComment" : true, + "canEdit" : true, + "comments" : { + "kind" : "projecthosting#issueCommentList", + "totalResults" : 2, + "items" : [ { + "id" : 0, + "kind" : "projecthosting#issueComment", + "author" : { + "kind" : "projecthosting#issuePerson", + "name" : "schattenpr...", + "htmlLink" : "https://code.google.com/u/10649813950663753000/" + }, + "content" : "I like to scroll through the tasks with my scrollwheel (like in fluxbox). \r\n\r\nPatch is attached that adds two new mouse-actions (next_task+prev_task) \r\nthat can be used for exactly that purpose. \r\n\r\nall the best!", + "published" : "2009-11-18T00:20:19.000Z", + "updates" : { + "kind" : "projecthosting#issueCommentUpdate" + }, + "canDelete" : true, + "attachments" : [ { + "attachmentId" : "8901002890399325565", + "fileName" : "tint2_task_scrolling.diff", + "fileSize" : 3059, + "mimetype" : "text/x-c++; charset=us-ascii" + } ] + }, { + "id" : 1, + "kind" : "projecthosting#issueComment", + "author" : { + "kind" : "projecthosting#issuePerson", + "name" : "thilo...", + "htmlLink" : "https://code.google.com/u/104224918623172014000/" + }, + "content" : "applied, thanks.\r\n", + "published" : "2009-11-18T05:14:58.000Z", + "updates" : { + "kind" : "projecthosting#issueCommentUpdate", + "status" : "Fixed", + "labels" : [ "-Type-Defect", "Type-Enhancement" ] + }, + "canDelete" : true + } ] + } + } ] + } + } ] +} diff --git a/spec/lib/gitlab/gitorious_import/project_creator.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb index cf2318bb3a2..cf2318bb3a2 100644 --- a/spec/lib/gitlab/gitorious_import/project_creator.rb +++ b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb diff --git a/spec/lib/gitlab/google_code_import/client_spec.rb b/spec/lib/gitlab/google_code_import/client_spec.rb new file mode 100644 index 00000000000..d2bf871daa8 --- /dev/null +++ b/spec/lib/gitlab/google_code_import/client_spec.rb @@ -0,0 +1,34 @@ +require "spec_helper" + +describe Gitlab::GoogleCodeImport::Client do + let(:raw_data) { JSON.parse(File.read(Rails.root.join("spec/fixtures/GoogleCodeProjectHosting.json"))) } + subject { described_class.new(raw_data) } + + describe "#valid?" do + context "when the data is valid" do + it "returns true" do + expect(subject).to be_valid + end + end + + context "when the data is invalid" do + let(:raw_data) { "No clue" } + + it "returns true" do + expect(subject).to_not be_valid + end + end + end + + describe "#repos" do + it "returns only Git repositories" do + expect(subject.repos.length).to eq(1) + end + end + + describe "#repo" do + it "returns the referenced repository" do + expect(subject.repo("tint2").name).to eq("tint2") + end + end +end diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb new file mode 100644 index 00000000000..ee5f99c12b0 --- /dev/null +++ b/spec/lib/gitlab/google_code_import/importer_spec.rb @@ -0,0 +1,69 @@ +require "spec_helper" + +describe Gitlab::GoogleCodeImport::Importer do + let(:raw_data) { JSON.parse(File.read(Rails.root.join("spec/fixtures/GoogleCodeProjectHosting.json"))) } + let(:client) { Gitlab::GoogleCodeImport::Client.new(raw_data) } + let(:import_data) { client.repo("tint2").raw_data } + let(:project) { create(:project, import_data: import_data) } + subject { described_class.new(project) } + + describe "#execute" do + it "imports status labels" do + subject.execute + + %w(New NeedInfo Accepted Wishlist Started Fixed Invalid Duplicate WontFix Incomplete).each do |status| + expect(project.labels.find_by(name: "Status: #{status}")).to_not be_nil + end + end + + it "imports labels" do + subject.execute + + %w( + Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical + Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security + Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery + Component-Systray Component-Clock Component-Launcher Component-Tint2conf Component-Docs Component-New + ).each do |label| + label.sub!("-", ": ") + expect(project.labels.find_by(name: label)).to_not be_nil + end + end + + it "imports issues" do + subject.execute + + issue = project.issues.first + expect(issue).to_not be_nil + expect(issue.iid).to eq(169) + expect(issue.state).to eq("closed") + expect(issue.label_names).to include("Priority: Medium") + expect(issue.label_names).to include("Status: Fixed") + expect(issue.label_names).to include("Type: Enhancement") + expect(issue.title).to eq("Scrolling through tasks") + expect(issue.state).to eq("closed") + expect(issue.description).to include("schattenpr...") + expect(issue.description).to include("https://code.google.com/u/106498139506637530000/") + expect(issue.description).to include("November 18, 2009 00:20") + expect(issue.description).to include('I like to scroll through the tasks with my scrollwheel \(like in fluxbox\).') + expect(issue.description).to include('Patch is attached that adds two new mouse\-actions \(next\_taskprev\_task\)') + expect(issue.description).to include('that can be used for exactly that purpose.') + expect(issue.description).to include('all the best!') + expect(issue.description).to include('https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/tint2_task_scrolling.diff') + end + + it "imports issue comments" do + subject.execute + + note = project.issues.first.notes.first + expect(note).to_not be_nil + expect(note.note).to include("thilo...") + expect(note.note).to include("https://code.google.com/u/104224918623172014000/") + expect(note.note).to include("November 18, 2009 05:14") + expect(note.note).to include("applied, thanks.") + expect(note.note).to include("Status: Fixed") + expect(note.note).to include("~~Type: Defect~~") + expect(note.note).to include("Type: Enhancement") + end + end +end diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb new file mode 100644 index 00000000000..7fca396f152 --- /dev/null +++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Gitlab::GoogleCodeImport::ProjectCreator do + let(:user) { create(:user) } + let(:repo) { + Gitlab::GoogleCodeImport::Repository.new( + "name" => 'vim', + "summary" => 'VI Improved', + "repositoryUrls" => [ "https://vim.googlecode.com/git/" ] + ) + } + let(:namespace) { create(:namespace) } + + it 'creates project' do + allow_any_instance_of(Project).to receive(:add_import_job) + + project_creator = Gitlab::GoogleCodeImport::ProjectCreator.new(repo, namespace, user) + project_creator.execute + project = Project.last + + expect(project.import_url).to eq("https://vim.googlecode.com/git/") + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end +end |