├── .gitignore ├── Gemfile ├── lib ├── github-to-bitbucket-issues.rb └── github-to-bitbucket-issues │ ├── formatters │ ├── base.rb │ ├── milestone.rb │ ├── comment.rb │ └── issue.rb │ ├── downloaders │ ├── issue.rb │ ├── comment.rb │ ├── milestone.rb │ ├── org_repository.rb │ └── base.rb │ ├── core_ext │ └── string.rb │ └── export.rb ├── Gemfile.lock ├── README.md └── cli.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.zip 3 | 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'backports' 4 | gem 'octokit' 5 | gem 'rubyzip' 6 | gem 'json_pure' 7 | 8 | -------------------------------------------------------------------------------- /lib/github-to-bitbucket-issues.rb: -------------------------------------------------------------------------------- 1 | require 'backports/1.9.3' 2 | 3 | require_relative 'github-to-bitbucket-issues/export' 4 | 5 | -------------------------------------------------------------------------------- /lib/github-to-bitbucket-issues/formatters/base.rb: -------------------------------------------------------------------------------- 1 | module GTBI 2 | module Formatters 3 | class Base 4 | def initialize(raw) 5 | @raw = raw 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/github-to-bitbucket-issues/downloaders/issue.rb: -------------------------------------------------------------------------------- 1 | module GTBI 2 | module Downloaders 3 | class Issue < Base 4 | def client_method 5 | "list_issues" 6 | end 7 | end 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/github-to-bitbucket-issues/downloaders/comment.rb: -------------------------------------------------------------------------------- 1 | module GTBI 2 | module Downloaders 3 | class Comment < Base 4 | def client_method 5 | "issues_comments" 6 | end 7 | end 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/github-to-bitbucket-issues/downloaders/milestone.rb: -------------------------------------------------------------------------------- 1 | module GTBI 2 | module Downloaders 3 | class Milestone < Base 4 | def client_method 5 | "list_milestones" 6 | end 7 | end 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/github-to-bitbucket-issues/downloaders/org_repository.rb: -------------------------------------------------------------------------------- 1 | module GTBI 2 | module Downloaders 3 | class OrgRepository < Base 4 | def client_method 5 | "org_repos" 6 | end 7 | end 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/github-to-bitbucket-issues/formatters/milestone.rb: -------------------------------------------------------------------------------- 1 | module GTBI 2 | module Formatters 3 | class Milestone < Base 4 | def formatted 5 | { 6 | :name => @raw.title 7 | } 8 | end 9 | end 10 | end 11 | end 12 | 13 | -------------------------------------------------------------------------------- /lib/github-to-bitbucket-issues/core_ext/string.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def camelize(uppercase_first_letter = true) 3 | string = self 4 | if uppercase_first_letter 5 | string = string.sub(/^[a-z\d]*/) { $&.capitalize } 6 | else 7 | string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { $&.downcase } 8 | end 9 | string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" } 10 | string.gsub!(/\//, '::') 11 | string 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.7) 5 | backports (3.6.4) 6 | faraday (0.9.1) 7 | multipart-post (>= 1.2, < 3) 8 | json_pure (1.8.2) 9 | multipart-post (2.0.0) 10 | octokit (3.8.0) 11 | sawyer (~> 0.6.0, >= 0.5.3) 12 | rubyzip (1.1.7) 13 | sawyer (0.6.0) 14 | addressable (~> 2.3.5) 15 | faraday (~> 0.8, < 0.10) 16 | 17 | PLATFORMS 18 | ruby 19 | 20 | DEPENDENCIES 21 | backports 22 | json_pure 23 | octokit 24 | rubyzip 25 | -------------------------------------------------------------------------------- /lib/github-to-bitbucket-issues/formatters/comment.rb: -------------------------------------------------------------------------------- 1 | module GTBI 2 | module Formatters 3 | class Comment < Base 4 | def formatted 5 | { 6 | :content => @raw.body, 7 | :created_on => @raw.created_at, 8 | :id => @raw.id, 9 | :issue => get_issue(@raw), 10 | :updated_on => @raw.updated_at, 11 | :user => @raw.user.login 12 | } 13 | end 14 | 15 | private 16 | 17 | def get_issue(comment) 18 | comment.issue_url.split('/').last 19 | end 20 | 21 | end 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /lib/github-to-bitbucket-issues/downloaders/base.rb: -------------------------------------------------------------------------------- 1 | module GTBI 2 | module Downloaders 3 | class Base 4 | def initialize(client, repository, options = {}) 5 | @client = client 6 | @repository = repository 7 | @options = options 8 | end 9 | 10 | def fetch 11 | items = [] 12 | page = 1 13 | one_page = [] 14 | 15 | loop do 16 | @options.merge!({:page => page}) 17 | one_page = @client.send(client_method, @repository, @options) 18 | items += one_page 19 | page += 1 20 | break if one_page.empty? 21 | end 22 | 23 | items 24 | end 25 | 26 | protected 27 | 28 | def client_method 29 | end 30 | end 31 | end 32 | end 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Export issues from a Github repository to the [Bitbucket 2 | Issue Data Format](https://confluence.atlassian.com/display/BITBUCKET/Export+or+Import+Issue+Data) 3 | 4 | ## Usage 5 | 6 | Requirements: 7 | * Ruby 8 | * Bundler (http://bundler.io/) 9 | 10 | Make sure you have all dependencies: 11 | ``` 12 | bundle install 13 | ``` 14 | And then: 15 | ``` 16 | ruby cli.rb -u username -p password -r myrepo -o issues.zip 17 | ``` 18 | or: 19 | ``` 20 | ruby cli.rb -t token_here --organization your_org 21 | ``` 22 | Available options 23 | ``` 24 | -t, --access_token [ARG] Github access token 25 | -u, --username [ARG] Github username 26 | -p, --password [ARG] Github password 27 | --organization [ARG] Export all organization's repositories 28 | -o, --output [ARG] Output filename - defaults to [repo_name].zip 29 | -r, --repository [ARG] Export only one repository 30 | -h, --help Show this message 31 | ``` 32 | -------------------------------------------------------------------------------- /cli.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require './lib/github-to-bitbucket-issues' 4 | require 'optparse' 5 | 6 | options = {} 7 | opt_parse = OptionParser.new do |opts| 8 | opts.banner = "Usage: 9 | ruby cli.rb -u username -p password -r myrepo -o issues.zip 10 | or 11 | ruby cli.rb -t token_here --organization your_org 12 | " 13 | opts.on('-t [ARG]', '--access_token [ARG]', "Github access token") do |v| 14 | options[:access_token] = v 15 | end 16 | opts.on('-u [ARG]', '--username [ARG]', "Github username") do |v| 17 | options[:username] = v 18 | end 19 | opts.on('-p [ARG]', '--password [ARG]', "Github password") do |v| 20 | options[:password] = v 21 | end 22 | opts.on('--organization [ARG]', "Export all organization's repositories") do |v| 23 | options[:organization] = v 24 | end 25 | opts.on('-o [ARG]', '--output', 'Output filename - defaults to [repo_name].zip') do |v| 26 | options[:filename] = v 27 | end 28 | opts.on('-r [ARG]', '--repository', 'Export only one repository') do |v| 29 | options[:repository] = v 30 | end 31 | opts.on('-h', '--help', 'Show this message') do |v| 32 | puts opts 33 | exit 34 | end 35 | end 36 | opt_parse.parse! 37 | 38 | # 39 | # Required parameters 40 | # 41 | begin 42 | raise OptionParser::MissingArgument unless options[:username] and options[:password] or options[:access_token] 43 | raise OptionParser::MissingArgument unless options[:repository] or options[:organization] 44 | rescue OptionParser::MissingArgument 45 | puts "Argument Missing\n\n" 46 | puts opt_parse 47 | exit 48 | end 49 | 50 | GTBI::Export.new(options).generate 51 | 52 | -------------------------------------------------------------------------------- /lib/github-to-bitbucket-issues/formatters/issue.rb: -------------------------------------------------------------------------------- 1 | module GTBI 2 | module Formatters 3 | class Issue < Base 4 | def formatted 5 | { 6 | :assignee => get_assignee(@raw), 7 | :component => nil, 8 | :content => @raw.body || " ", 9 | :content_updated_on => @raw.updated_at, 10 | :created_on => @raw.updated_at, 11 | :edited_on => @raw.updated_at, 12 | :id => @raw.number, 13 | :kind => get_kind(@raw), 14 | :milestone => get_milestone(@raw), 15 | :priority => get_priority(@raw), 16 | :reporter => @raw.user.login, 17 | :status => get_status(@raw), 18 | :title => @raw.title, 19 | :updated_on => @raw.updated_at, 20 | :version => nil, 21 | :watchers => [] 22 | } 23 | end 24 | 25 | private 26 | 27 | def get_assignee(issue) 28 | issue.assignee.login if issue.assignee && issue.assignee.login 29 | end 30 | 31 | def get_status(issue) 32 | if issue.state == 'closed' 33 | 'resolved' 34 | else 35 | 'new' 36 | end 37 | end 38 | 39 | def get_kind(issue) 40 | if issue.labels.to_s =~ /enhancement/i 41 | 'enhancement' 42 | elsif issue.labels.to_s =~ /proposal/i 43 | 'proposal' 44 | elsif issue.labels.to_s =~ /task/i 45 | 'task' 46 | else 47 | 'bug' 48 | end 49 | end 50 | 51 | def get_priority(issue) 52 | if issue.labels.to_s =~ /trivial/i 53 | 'trivial' 54 | elsif issue.labels.to_s =~ /minor/i 55 | 'minor' 56 | elsif issue.labels.to_s =~ /critical/i 57 | 'critical' 58 | elsif issue.labels.to_s =~ /blocker/i 59 | 'blocker' 60 | else 61 | 'major' 62 | end 63 | end 64 | 65 | def get_milestone(issue) 66 | if issue.milestone 67 | milestone = issue.milestone.title 68 | end 69 | 70 | milestone 71 | end 72 | end 73 | end 74 | end 75 | 76 | -------------------------------------------------------------------------------- /lib/github-to-bitbucket-issues/export.rb: -------------------------------------------------------------------------------- 1 | require 'octokit' 2 | require 'zip' 3 | require 'json' 4 | 5 | require_relative 'formatters/base' 6 | require_relative 'formatters/issue' 7 | require_relative 'formatters/comment' 8 | require_relative 'formatters/milestone' 9 | require_relative 'downloaders/base' 10 | require_relative 'downloaders/issue' 11 | require_relative 'downloaders/comment' 12 | require_relative 'downloaders/org_repository' 13 | require_relative 'downloaders/milestone' 14 | require_relative 'core_ext/string' 15 | 16 | module GTBI 17 | class Export 18 | attr_reader :issues, :comments, :milestones 19 | 20 | def initialize(options) 21 | return if not options[:repository] and not options[:organization] 22 | 23 | @github_client = Octokit::Client.new({ 24 | :login => options[:username], 25 | :password => options[:password], 26 | :access_token => options[:access_token] 27 | }) 28 | @repository = options[:repository] 29 | @default_filename = options[:filename] 30 | @organization = options[:organization] 31 | @issues = [] 32 | @comments = [] 33 | @milestones = [] 34 | end 35 | 36 | def generate 37 | repos = if @organization then download_organization_repos else [@repository] end 38 | repos.each do |repo| 39 | puts "Fetch #{repo}" 40 | @filename = if @default_filename then @default_filename else repo.gsub('/','_') + '.zip' end 41 | @repository = repo 42 | 43 | # Skip existing files 44 | if File.exists? @filename 45 | puts "File #{@filename} already exists, skipping..." 46 | next 47 | end 48 | 49 | # Get the data 50 | begin 51 | download_issues 52 | download_comments 53 | download_milestones 54 | generate_archive 55 | rescue Octokit::ClientError => e 56 | puts "Error fetching data from github #{e.message}" 57 | end 58 | end 59 | end 60 | 61 | def to_json 62 | JSON.pretty_generate({ 63 | :issues => @issues, 64 | :comments => @comments, 65 | :milestones => @milestones, 66 | :attachments => [], 67 | :logs => [], 68 | :meta => { 69 | :default_assignee => nil, 70 | :default_component => nil, 71 | :default_kind => "bug", 72 | :default_milestone => nil, 73 | :default_version => nil 74 | }, 75 | :components => [], 76 | :versions => [] 77 | }) 78 | end 79 | 80 | def download_organization_repos 81 | repos = downloader("org_repository").new(@github_client, @organization, {}).fetch 82 | repos.map do |item| 83 | "#{@organization}/#{item.name}" 84 | end 85 | end 86 | 87 | private 88 | 89 | def download_issues 90 | @issues = [] 91 | %w(open closed).each do |state| 92 | @issues += download_all_of("issue", {:state => state}) 93 | end 94 | end 95 | 96 | def download_comments 97 | @comments = download_all_of("comment") 98 | end 99 | 100 | def download_milestones 101 | @milestones = download_all_of("milestone") 102 | end 103 | 104 | def download_all_of(type, options = {}) 105 | items = downloader(type).new(@github_client, @repository, options).fetch 106 | items.map do |item| 107 | formatter(type).new(item).formatted 108 | end 109 | end 110 | 111 | def downloader(type) 112 | Object.const_get("GTBI").const_get("Downloaders").const_get(type.camelize) 113 | end 114 | 115 | def formatter(type) 116 | Object.const_get("GTBI").const_get("Formatters").const_get(type.camelize) 117 | end 118 | 119 | def generate_archive 120 | Zip::File.open(@filename, Zip::File::CREATE) do |zipfile| 121 | zipfile.get_output_stream("db-1.0.json") do |f| 122 | f.puts to_json 123 | end 124 | end 125 | end 126 | end 127 | end 128 | 129 | --------------------------------------------------------------------------------