Skip to content

Instantly share code, notes, and snippets.

@miyakawataku
Created May 28, 2013 16:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save miyakawataku/5664237 to your computer and use it in GitHub Desktop.
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.
#!/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