Last active
August 29, 2015 14:03
-
-
Save obfusk/f0a782194a0390a9bbed to your computer and use it in GitHub Desktop.
bitbucket -> gitlab
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/ruby | |
# -- ; {{{1 | |
# | |
# File : bitbucket2gitlab.rb | |
# Maintainer : Felix C. Stegerman <[email protected]> | |
# Date : 2014-07-17 | |
# | |
# Copyright : Copyright (C) 2014 Felix C. Stegerman | |
# Licence : GPLv3+ | |
# | |
# Description: | |
# | |
# bitbucket2gitlab - copy bitbucket teams to gitlab groups | |
# | |
# bitbucket2gitlab copies all repositories from each specified | |
# BitBucket team to the specified (existing) GitLab group. | |
# | |
# GitLab groups are specified by path (instead of name). | |
# Repositories are cloned and pushed via ssh. | |
# | |
# BitBucket password and GitLab token can be specified via | |
# $BB_PASSWORD and $GL_TOKEN; when not specified, bitbucket2gitlab | |
# will prompt for them. | |
# | |
# Caveats: | |
# | |
# bitbucket2gitlab cannot overwrite an existing repository with an | |
# empty one; it will leave the existing repository as-is. | |
# | |
# Options: | |
# | |
# $ bitbucket2gitlab --help | |
# | |
# Dependencies: | |
# | |
# $ gem install excon obfusk-util | |
# | |
# TODO: | |
# | |
# * issues? | |
# * wikis? | |
# * github? | |
# * *<->*? | |
# * gem? | |
# | |
# -- ; }}}1 | |
require 'excon' | |
require 'json' | |
require 'obfusk/util/term' | |
require 'optparse' | |
require 'tmpdir' | |
module BitBucket2GitLab | |
class Error < RuntimeError; end | |
OUT = Obfusk::Util::Term | |
INFO = 'bitbucket2gitlab - copy bitbucket teams to gitlab groups' | |
USAGE = 'bitbucket2gitlab [<option(s)>] [<bb_team>[:<gl_group>] ...]' | |
BB_URL_BASE = 'https://bitbucket.org/api/2.0' | |
GL_URL_BASE = -> srv { "https://#{srv}/api/v3" } | |
BB_REPO_URL = -> team, repo { "[email protected]:#{team}/#{repo}.git" } | |
GL_REPO_URL = -> srv { -> grp, repo { "git@#{srv}:#{grp}/#{repo}.git" } } | |
def self.sys(*args) | |
puts "$ #{args*' '}" | |
Kernel.system(*args).tap do | |
$?.success? or raise Error, "#{args*' '} returned #$?" | |
end | |
end | |
def self.sys_out(cmd) | |
%x[ #{cmd} ].tap { $?.success? or raise Error, "#{cmd} returned #$?" } | |
end | |
def self.check_repo_name(t, r) | |
t =~ /\A[a-z_][a-z0-9_-]*\z/ or raise Error, | |
"team #{t} has a problematic name" | |
r =~ /\A[a-z_][a-z0-9_-]*\z/ or raise Error, | |
"repository #{t}/#{r} has a problematic name" | |
end | |
def self.auth_for(which, auth) | |
which == :bb ? { user: auth[:user], password: auth[:pass] } : | |
{ headers: { 'PRIVATE-TOKEN' => auth[:token] } } | |
end | |
def self.link_header_next_page(r, d) | |
(l = r.headers['Link']) ? | |
(n = l.split(',').grep(/rel="next"/).first) ? | |
n.match(/<(.*)>/)[1] : nil : nil | |
end | |
def self.bb_next_page(r, d) | |
d['next'] | |
end | |
def self.get(which, url, auth, opts = {}) | |
one = opts[:one]; pag = opts[:pag] || method(:link_header_next_page) | |
ds = [] ; u = url | |
while true | |
puts "GET #{u}" | |
r = Excon.get u, auth_for(which, auth).merge(expects: [200]) | |
ds << d = JSON.parse(r.body); u = pag[r,d] || break | |
end | |
one ? ds.first : ds | |
end | |
def self.post(which, url, body, auth) | |
puts "POST #{url} << #{body}" | |
Excon.post url, auth_for(which, auth).merge(expects: [201], body: body) | |
end | |
def self.bb_team_repos(team, auth) | |
pag = method :bb_next_page | |
get(:bb, "#{BB_URL_BASE}/repositories/#{team}", auth, pag: pag) \ | |
.map { |x| x['values'] } .flatten(1) | |
end | |
def self.bb_team_repos_names(team, auth) | |
bb_team_repos(team, auth).map { |r| r['full_name'].split('/')[1] } | |
end | |
def self.bb_clone_repo(team, dir, repo) | |
args = %w{ git clone --bare } + [BB_REPO_URL[team, repo], "#{repo}.git"] | |
sys(*args, chdir: dir) | |
end | |
def self.gl_groups(auth) | |
@gl_groups ||= \ | |
get(:gl, "#{GL_URL_BASE[auth[:server]]}/groups", auth).flatten(1) | |
end | |
def self.gl_group_id_for(grp, auth) | |
gl_groups(auth).find { |g| g['path'] == grp } ['id'] | |
end | |
def self.gl_group(grp_id, auth) | |
@gl_groups_data ||= {} | |
@gl_groups_data[grp_id] ||= \ | |
get(:gl, "#{GL_URL_BASE[auth[:server]]}/groups/#{grp_id}", | |
auth, one: true) | |
end | |
def self.gl_group_repos_names(grp_id, auth) | |
gl_group(grp_id, auth)['projects'].map { |r| r['path'] } | |
end | |
def self.gl_create_repo(grp_id, repo, auth) | |
body = URI.encode_www_form(name: repo, namespace_id: grp_id) | |
post(:gl, "#{GL_URL_BASE[auth[:server]]}/projects", body, auth) | |
end | |
def self.gl_push_repo(grp, dir, repo, url_base) | |
Dir.chdir("#{dir}/#{repo}.git") do | |
if sys_out('git branch -a').empty? | |
puts '(empty repository -- nothing to push)' | |
else | |
args = %w{ git push --mirror } + [url_base[grp, repo]] | |
sys(*args) | |
end | |
end | |
end | |
def self.configure(*args) | |
os = { gl_server: 'gitlab.com', wait: 10 } | |
op = OptionParser.new(USAGE) do |o| | |
o.on('-u', '--bitbucket-user NAME', 'BitBucket user name') do |x| | |
os[:bb_user] = x | |
end | |
o.on('-s', '--gitlab-server HOST', | |
'GitLab server; defaults to gitlab.com') do |x| | |
os[:gl_srv] = x | |
end | |
o.on('-k', '--skip', 'Skip existing repo; defaults to no') do | |
os[:skip] = true | |
end | |
o.on('-o', '--overwrite', 'Overwrite existing repo; defaults to no') do | |
os[:overwrite] = true | |
end | |
o.on('-a', '--ask', 'Ask whether to overwrite repo; defaults to no') do | |
os[:ask] = true | |
end | |
o.on('-c', '--continue', 'Continue on errors; defaults to no') do | |
os[:continue] = true | |
end | |
o.on('-w', '--wait SECS', Integer, | |
'Wait SECS seconds after creating repo; defaults to 10') do |x| | |
os[:wait] = x | |
end | |
o.on_tail('-h', '--help', 'Show this message') do | |
puts INFO, '', o; exit | |
end | |
end | |
begin | |
op.parse! args | |
rescue OptionParser::ParseError => e | |
$stderr.puts "Error: #{e}"; exit 1 | |
end | |
unless os[:bb_user] | |
$stderr.puts 'BitBucket user required'; exit 1 | |
end | |
if os.values_at(:skip, :overwrite, :ask).count { |x| x } > 1 | |
$stderr.puts 'You can only have one of --skip, --overwrite, --ask' | |
exit 1 | |
end | |
os[:bb_pass] = ENV['BB_PASSWORD'] || | |
OUT.prompt('bitbucket password: ', :hide) | |
os[:gl_tok] = ENV['GL_TOKEN'] || | |
OUT.prompt('gitlab token: ', :hide) | |
os[:bb_auth] = { user: os[:bb_user] , pass: os[:bb_pass] } | |
os[:gl_auth] = { server: os[:gl_srv] , token: os[:gl_tok] } | |
os[:copy] = args.map { |x| x.split(':', 2) } \ | |
.map { |f,t| { from: f, to: t || f } } | |
os | |
end | |
def self.choose_overwrite_skip_error(os, grp, repos, r) | |
msg = "repository #{grp}/#{r} already exists" | |
return :create unless repos.include? r | |
return :skip if os[:skip] | |
return :overwrite if os[:overwrite] | |
raise Error, msg unless os[:ask] | |
OUT.prompt("#{msg}; overwrite? [y/N] ") =~ /^Y/i ? :overwrite : :skip | |
end | |
def self.main(*args) | |
os = configure(*args) | |
os[:copy].each do |c| | |
bb_team = c[:from]; gl_grp = c[:to] | |
bb_repos = bb_team_repos_names bb_team , os[:bb_auth] | |
gl_grp_id = gl_group_id_for gl_grp , os[:gl_auth] | |
gl_repos = gl_group_repos_names gl_grp_id , os[:gl_auth] | |
Dir.mktmpdir do |t| | |
bb_repos.each do |r| | |
begin | |
check_repo_name bb_team, r | |
choice = choose_overwrite_skip_error os, gl_grp, gl_repos, r | |
next if choice == :skip | |
bb_clone_repo bb_team, t, r | |
if choice == :create | |
gl_create_repo gl_grp_id, r, os[:gl_auth] | |
if os[:wait] > 0 | |
puts '(let server breathe...)'; sleep os[:wait] | |
end | |
end | |
gl_push_repo gl_grp, t, r, GL_REPO_URL[os[:gl_srv]] | |
rescue Error, Excon::Errors::Error => e | |
puts "*** ERROR: #{e} ***"; exit 1 unless os[:continue] | |
end | |
end | |
end | |
end | |
end | |
end | |
BitBucket2GitLab.main(*ARGV) | |
# vim: set tw=70 sw=2 sts=2 et fdm=marker : |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment