Skip to content

Instantly share code, notes, and snippets.

@obfusk
Last active August 29, 2015 14:03
Show Gist options
  • Save obfusk/f0a782194a0390a9bbed to your computer and use it in GitHub Desktop.
Save obfusk/f0a782194a0390a9bbed to your computer and use it in GitHub Desktop.
bitbucket -> gitlab
#!/usr/bin/ruby
# -- ; {{{1
#
# File : bitbucket2gitlab.rb
# Maintainer : Felix C. Stegerman <flx@obfusk.net>
# 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 { "git@bitbucket.org:#{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