Created
May 28, 2013 16:52
-
-
Save miyakawataku/5664237 to your computer and use it in GitHub Desktop.
Extracts issues of a project on Google Code, and transforms them to BitBucket issues package.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# vim: et sw=2 sts=2 | |
require 'rexml/document' | |
require 'open-uri' | |
require 'nokogiri' | |
require 'json' | |
class Issue | |
def initialize(entry) | |
@id = REXML::XPath.first(entry, 'issues:id/text()').value.to_i | |
@title = REXML::XPath.first(entry, 'title/text()').value | |
@author = REXML::XPath.first(entry, 'author/name/text()').value | |
@labels = REXML::XPath.each(entry, 'issues:label/text()').map {|t| t.value} .to_a | |
@state = REXML::XPath.first(entry, 'issues:state/text()').value | |
@status = REXML::XPath.first(entry, 'issues:status/text()').value | |
@published = REXML::XPath.first(entry, 'published/text()').value | |
@updated = REXML::XPath.first(entry, 'updated/text()').value | |
@content_html = REXML::XPath.first(entry, 'content/text()').value | |
@comments_url = REXML::XPath.first(entry, "link[@rel = 'replies']").attribute('href').value() | |
owner = REXML::XPath.first(entry, 'issues:owner/issues:username/text()') | |
if owner | |
@owner = owner.value | |
end | |
end | |
def to_issue_obj(mapping) | |
{ | |
'assignee' => mapping.user(self.owner), | |
'component' => self.component, | |
'content' => self.content.gsub(/^/, ' '), | |
'content_updated_on' => nil, | |
'created_on' => self.published, | |
'edited_on' => nil, | |
'id' => self.id, | |
'kind' => mapping.kind(self.type), | |
'milestone' => self.milestone, | |
'priority' => mapping.priority(self.priority), | |
'reporter' => mapping.user(self.author), | |
'status' => mapping.status(self.status), | |
'title' => self.title, | |
'updated_on' => self.updated, | |
'version' => nil, | |
'watchers' => [mapping.user(self.author)].uniq, | |
} | |
end | |
def self.read_issues(project_name) | |
url = "https://code.google.com/feeds/issues/p/#{project_name}/issues/full" | |
issues = [] | |
while true | |
open(url) { |atom| | |
doc = REXML::Document.new(atom) | |
REXML::XPath.each(doc, '/feed/entry') { |entry| | |
issue = Issue.new(entry) | |
issues.push(issue) | |
} | |
next_link = REXML::XPath.first(doc, "/feed/link[@rel = 'next']") | |
if ! next_link | |
return issues | |
end | |
url = next_link.attribute('href').value | |
} | |
end | |
end | |
def id | |
@id | |
end | |
def title | |
@title | |
end | |
def author | |
@author | |
end | |
def owner | |
@owner | |
end | |
def labels | |
@labels | |
end | |
def comments_url | |
@comments_url | |
end | |
def content_html | |
@content_html | |
end | |
def content | |
Nokogiri::HTML(self.content_html).text | |
end | |
def type | |
in_label('Type') | |
end | |
def priority | |
in_label('Priority') | |
end | |
def component | |
in_label('Component') | |
end | |
def milestone | |
in_label('Milestone') | |
end | |
def state | |
@state | |
end | |
def status | |
@status | |
end | |
def published | |
@published | |
end | |
def updated | |
@updated | |
end | |
private | |
def in_label(prefix) | |
@labels.each { |label| | |
if label =~ /#{prefix}-(.+)/ | |
return Regexp.last_match[1] | |
end | |
} | |
return nil | |
end | |
end | |
class Comment | |
@@id_counter = 1 | |
def initialize(issue, entry) | |
@id = @@id_counter | |
@@id_counter += 1 | |
@issue = issue | |
content_text = REXML::XPath.first(entry, 'content/text()') | |
@content_html = (content_text ? content_text.value : nil) | |
@author = REXML::XPath.first(entry, 'author/name/text()').value | |
@published = REXML::XPath.first(entry, 'published/text()').value | |
@updated = REXML::XPath.first(entry, 'updated/text()').value | |
@labels = REXML::XPath.each(entry, 'issues:updates/issues:label/text()').map {|t| t.value} .to_a | |
status_text = REXML::XPath.first(entry, 'issues:updates/issues:status/text()') | |
@status = (status_text ? status_text.value : nil) | |
end | |
def to_comment_obj(mapping) | |
{ | |
'content' => self.content.gsub(/^/, ' '), | |
'created_on' => self.published, | |
'id' => self.id, | |
'issue' => self.issue.id, | |
'updated_on' => self.updated, | |
'user' => mapping.user(self.author), | |
} | |
end | |
def to_status_log_obj(mapping) | |
{ | |
'changed_from' => nil, | |
'changed_to' => mapping.status(self.status), | |
'comment' => self.id, | |
'created_on' => self.published, | |
'field' => 'status', | |
'issue' => self.issue.id, | |
'user' => mapping.user(self.author), | |
} | |
end | |
def self.read_comments(issues) | |
comments = [] | |
issues.each { |issue| | |
url = issue.comments_url | |
while url | |
open(url) { |atom| | |
doc = REXML::Document.new(atom) | |
REXML::XPath.each(doc, '/feed/entry') { |entry| | |
comment = Comment.new(issue, entry) | |
comments.push(comment) | |
} | |
next_link = REXML::XPath.first(doc, "/feed/link[@rel = 'next']") | |
url = (next_link ? next_link.attribute('href').value : nil) | |
} | |
end | |
} | |
comments | |
end | |
def id | |
@id | |
end | |
def issue | |
@issue | |
end | |
def content_html | |
@content_html | |
end | |
def content | |
Nokogiri::HTML(self.content_html).text | |
end | |
def author | |
@author | |
end | |
def published | |
@published | |
end | |
def updated | |
@updated | |
end | |
def labels | |
@labels | |
end | |
def state | |
@state | |
end | |
def status | |
@status | |
end | |
def type | |
in_label('Type') | |
end | |
def priority | |
in_label('Priority') | |
end | |
def component | |
in_label('Component') | |
end | |
def milestone | |
in_label('Milestone') | |
end | |
private | |
def in_label(prefix) | |
@labels.each { |label| | |
if label =~ /#{prefix}-(.+)/ | |
return Regexp.last_match[1] | |
end | |
} | |
return nil | |
end | |
end | |
class Mapping | |
def initialize | |
@user_mapping = {} | |
@kind_mapping = {} | |
@priority_mapping = {} | |
end | |
def map_user(google_user, bitbucket_user) | |
@user_mapping[google_user] = bitbucket_user | |
self | |
end | |
def user(google_user) | |
@user_mapping[google_user] | |
end | |
def map_kind(google_type, bitbucket_kind) | |
@kind_mapping[google_type] = bitbucket_kind | |
self | |
end | |
def kind(google_type) | |
@kind_mapping.fetch(google_type, 'bug') | |
end | |
def map_priority(google_priority, bitbucket_priority) | |
@priority_mapping[google_priority] = bitbucket_priority | |
self | |
end | |
def priority(google_priority) | |
@priority_mapping.fetch(google_priority, 'major') | |
end | |
def status(google_status) | |
case google_status | |
when 'New', 'Accepted', 'Started' | |
'new' | |
when 'Fixed', 'Verified', 'Done' | |
'resolved' | |
when 'Invalid' | |
'invalid' | |
when 'Duplicate' | |
'duplicate' | |
when 'WontFix' | |
'wontfix' | |
else | |
'new' | |
end | |
end | |
end | |
def extract_logs(comments, mapping) | |
comments.select { |c| | |
c.status | |
} .map { |c| | |
c.to_status_log_obj(mapping) | |
} | |
end | |
def extract_components(issues, comments) | |
(issues + comments).select { |e| | |
e.component | |
} .map { |e| | |
e.component | |
} .uniq.map { |component| | |
{'name' => component} | |
} | |
end | |
def extract_milestones(issues, comments) | |
(issues + comments).select { |e| | |
e.milestone | |
} .map { |e| | |
e.milestone | |
} .uniq.map { |milestone| | |
{'name' => milestone} | |
} | |
end | |
issues = Issue.read_issues('kink-lang') | |
comments = Comment.read_comments(issues) | |
mapping = Mapping.new | |
.map_user('miyakawa...@gmail.com', 'miyakawataku') | |
.map_kind('Defect', 'bug') | |
.map_kind('Enhancement', 'enhancement') | |
.map_kind('Task', 'task') | |
.map_kind('Review', 'task') | |
.map_kind('Other', 'proposal') | |
.map_priority('Critical', 'blocker') | |
.map_priority('High', 'critical') | |
.map_priority('Medium', 'major') | |
.map_priority('Low', 'minor') | |
db = { | |
'issues' => issues.map { |i| i.to_issue_obj(mapping) }, | |
'comments' => comments.map { |c| c.to_comment_obj(mapping) }, | |
'attachments' => [], | |
'logs' => extract_logs(comments, mapping), | |
'components' => extract_components(issues, comments), | |
'milestones' => extract_milestones(issues, comments), | |
'versions' => [], | |
'meta' => { | |
'default_assignee' => nil, | |
'default_component' => nil, | |
'default_kind' => 'bug', | |
'default_milestone' => nil, | |
'default_version' => nil, | |
}, | |
} | |
puts db.to_json |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment