gitbak 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +77 -0
- data/bin/gitbak +2 -113
- data/lib/gitbak/config.rb +139 -0
- data/lib/gitbak/eval.rb +6 -0
- data/lib/gitbak/exec.rb +85 -0
- data/lib/gitbak/misc.rb +60 -0
- data/lib/gitbak/services.rb +114 -0
- data/lib/gitbak/version.rb +5 -2
- data/lib/gitbak.rb +86 -143
- metadata +12 -6
- data/README +0 -62
data/README.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
<!-- \{{{1 -->
|
2
|
+
|
3
|
+
File : README
|
4
|
+
Maintainer : Felix C. Stegerman <flx@obfusk.net>
|
5
|
+
Date : 2012-12-29
|
6
|
+
|
7
|
+
Copyright : Copyright (C) 2012 Felix C. Stegerman
|
8
|
+
Version : v0.3.0
|
9
|
+
|
10
|
+
<!-- }}}1 -->
|
11
|
+
|
12
|
+
## Description
|
13
|
+
<!-- \{{{1 -->
|
14
|
+
|
15
|
+
gitbak - bitbucket/github/gist backup
|
16
|
+
|
17
|
+
GitBak allows you to mirror Bitbucket/GitHub/Gist repositories
|
18
|
+
easily; you only need to specify paths, users, and authentication in
|
19
|
+
~/.gitbak and it does the rest.
|
20
|
+
|
21
|
+
<!-- }}}1 -->
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
<!-- \{{{1 -->
|
25
|
+
|
26
|
+
$ gitbak --help # read documentation
|
27
|
+
$ vim ~/.gitbak # configure
|
28
|
+
$ gitbak -v # mirror
|
29
|
+
|
30
|
+
<!-- }}}1 -->
|
31
|
+
|
32
|
+
## Installing
|
33
|
+
<!-- \{{{1 -->
|
34
|
+
|
35
|
+
$ gem install gitbak # rubygems
|
36
|
+
|
37
|
+
Get it at https://github.com/obfusk/gitbak. Depends: git, ruby.
|
38
|
+
|
39
|
+
<!-- }}}1 -->
|
40
|
+
|
41
|
+
## TODO
|
42
|
+
<!-- \{{{1 -->
|
43
|
+
|
44
|
+
Some things that may be useful/implemented at some point.
|
45
|
+
|
46
|
+
* ask password again on typo (^D) or auth fail
|
47
|
+
* tests?
|
48
|
+
* better error handling?
|
49
|
+
|
50
|
+
* custom services (should be easy to add already)
|
51
|
+
* metadata (issues, wikis, ...)
|
52
|
+
* teams/organisations
|
53
|
+
* starred repos/gists
|
54
|
+
* filtering
|
55
|
+
* oauth?
|
56
|
+
|
57
|
+
* specify ssh key(s)?
|
58
|
+
* https clone auth?
|
59
|
+
|
60
|
+
<!-- }}}1 -->
|
61
|
+
|
62
|
+
## License
|
63
|
+
<!-- \{{{1 -->
|
64
|
+
|
65
|
+
GPLv2 [1].
|
66
|
+
|
67
|
+
<!-- }}}1 -->
|
68
|
+
|
69
|
+
## References
|
70
|
+
<!-- \{{{1 -->
|
71
|
+
|
72
|
+
[1] GNU General Public License, version 2
|
73
|
+
--- http://www.opensource.org/licenses/GPL-2.0
|
74
|
+
|
75
|
+
<!-- }}}1 -->
|
76
|
+
|
77
|
+
<!-- vim: set tw=70 sw=2 sts=2 et fdm=marker : -->
|
data/bin/gitbak
CHANGED
@@ -1,118 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require 'gitbak'
|
4
|
-
require 'optparse'
|
3
|
+
require 'gitbak/exec'
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
usage = 'gitbak [<option(s)>]'
|
9
|
-
|
10
|
-
info = <<-END.gsub(/^ {2}/, '') # {{{1
|
11
|
-
gitbak - bitbucket/github/gist backup
|
12
|
-
|
13
|
-
=== Example Configuration ===
|
14
|
-
|
15
|
-
$ cat >> ~/.gitbak
|
16
|
-
dir = '/path/to/mirrors/dir'
|
17
|
-
|
18
|
-
%w{ user1 user2 }.each do |u|
|
19
|
-
GitBak::Cfg.auth :bitbucket, u
|
20
|
-
GitBak::Cfg.auth :github , u
|
21
|
-
|
22
|
-
GitBak::Cfg.bitbucket "\#{dir}/\#{u}/bitbucket", u, auth: true
|
23
|
-
GitBak::Cfg.github "\#{dir}/\#{u}/github" , u, auth: true
|
24
|
-
GitBak::Cfg.gist "\#{dir}/\#{u}/gist" , u, auth: true
|
25
|
-
end
|
26
|
-
^D
|
27
|
-
|
28
|
-
|
29
|
-
=== Configuration Methods ===
|
30
|
-
|
31
|
-
GitBak::Cfg.auth service, user[, password]
|
32
|
-
GitBak::Cfg.<service> dir, user[, options]
|
33
|
-
|
34
|
-
* password is optional; gitbak prompts for unspecified passwords.
|
35
|
-
* The services are: bitbucket, github, gist.
|
36
|
-
* Service options:
|
37
|
-
:auth is optional; can be true (same user) or 'username'.
|
38
|
-
:method is optional; defaults to :ssh.
|
39
|
-
* Each GitBak::Cfg.<service> call specifies a new configuration.
|
40
|
-
END
|
41
|
-
# }}}1
|
42
|
-
|
43
|
-
options = { cfgfile: "#{Dir.home}/.gitbak", verbose: false }
|
44
|
-
|
45
|
-
# --
|
46
|
-
|
47
|
-
OptionParser.new do |opts| # {{{1
|
48
|
-
opts.banner = usage
|
49
|
-
|
50
|
-
opts.on('-c', '--config-file FILE', 'Configuration file') do |f|
|
51
|
-
options[:cfgfile] = f
|
52
|
-
end
|
53
|
-
|
54
|
-
opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
|
55
|
-
options[:verbose] = v
|
56
|
-
end
|
57
|
-
|
58
|
-
opts.on_tail('-h', '--help', 'Show this message') do
|
59
|
-
puts opts, '', info
|
60
|
-
exit
|
61
|
-
end
|
62
|
-
|
63
|
-
opts.on_tail('--version', 'Show version') do
|
64
|
-
puts "gitbak v#{GitBak::VERSION}"
|
65
|
-
exit
|
66
|
-
end
|
67
|
-
end.parse! # }}}1
|
68
|
-
|
69
|
-
GitBak.die "usage: #{usage}" unless ARGV.length == 0
|
70
|
-
|
71
|
-
# --
|
72
|
-
|
73
|
-
module GitBak # {{{1
|
74
|
-
module Cfg
|
75
|
-
CFG__ = { bitbucket: [], github: [], gist: [], auth: {} }
|
76
|
-
|
77
|
-
def self.auth (service, user, pass = nil)
|
78
|
-
(CFG__[:auth][service] ||= {})[user] = \
|
79
|
-
{ user: user, pass: pass }
|
80
|
-
end
|
81
|
-
|
82
|
-
def self._service (name)
|
83
|
-
meta = class << self; self; end
|
84
|
-
meta.send(:define_method, name) do |dir, user, opts = {}|
|
85
|
-
CFG__[name] << opts.merge(dir: dir, user: user)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
SERVICES.each do |name|
|
90
|
-
_service name.downcase.to_sym
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end # }}}1
|
94
|
-
|
95
|
-
# --
|
96
|
-
|
97
|
-
GitBak.die "Configuration file (#{options[:cfgfile]}) not found." \
|
98
|
-
unless GitBak.exists? options[:cfgfile]
|
99
|
-
|
100
|
-
load options[:cfgfile]
|
101
|
-
|
102
|
-
# --
|
103
|
-
|
104
|
-
GitBak::SERVICES.each do |name| # {{{1
|
105
|
-
key = name.downcase.to_sym
|
106
|
-
GitBak::Cfg::CFG__[:auth].fetch(key, []).each do |k, v|
|
107
|
-
p = "#{name} password for #{v[:user]}: "
|
108
|
-
v[:pass] ||= GitBak.prompt p, true
|
109
|
-
end
|
110
|
-
end # }}}1
|
111
|
-
|
112
|
-
GitBak::Cfg::CFG__[:auth][:gist] ||= GitBak::Cfg::CFG__[:auth][:github]
|
113
|
-
|
114
|
-
# --
|
115
|
-
|
116
|
-
GitBak.main GitBak::Cfg::CFG__.merge(options)
|
5
|
+
GitBak::Executable.main
|
117
6
|
|
118
7
|
# vim: set tw=70 sw=2 sts=2 et fdm=marker :
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'gitbak/eval'
|
2
|
+
require 'gitbak/misc'
|
3
|
+
require 'gitbak/services'
|
4
|
+
|
5
|
+
# --
|
6
|
+
|
7
|
+
# gitbak namespace
|
8
|
+
module GitBak
|
9
|
+
|
10
|
+
# configuration
|
11
|
+
module Config # {{{1
|
12
|
+
|
13
|
+
# configuration error
|
14
|
+
class ConfigError < GitBak::Error; end
|
15
|
+
|
16
|
+
# --
|
17
|
+
|
18
|
+
# description
|
19
|
+
INFO = 'gitbak - bitbucket/github/gist backup'
|
20
|
+
|
21
|
+
# configuration example
|
22
|
+
CONFIG_EX = <<-END.gsub(/^ {6}/, '') # {{{2
|
23
|
+
=== Example Configuration ===
|
24
|
+
|
25
|
+
$ cat >> ~/.gitbak
|
26
|
+
dir = '/path/to/mirrors/dir'
|
27
|
+
|
28
|
+
GitBak.configure do |auth, repos|
|
29
|
+
%w{ user1 user2 }.each do |u|
|
30
|
+
repos.bitbucket "\#{dir}/\#{u}/bitbucket", u, auth: true
|
31
|
+
repos.github "\#{dir}/\#{u}/github" , u, auth: true
|
32
|
+
repos.gist "\#{dir}/\#{u}/gist" , u, auth: true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
^D
|
36
|
+
|
37
|
+
|
38
|
+
=== Configuration Methods ===
|
39
|
+
|
40
|
+
auth.<service> user[, password]
|
41
|
+
repos.<service> dir, user[, options]
|
42
|
+
|
43
|
+
|
44
|
+
The (default) services are: bitbucket, github, gist.
|
45
|
+
If a password is not specified, gitbak will prompt for it.
|
46
|
+
|
47
|
+
|
48
|
+
=== Optional Repository Options ===
|
49
|
+
|
50
|
+
:auth can be true (same user) or 'username'.
|
51
|
+
:method defaults to :ssh.
|
52
|
+
END
|
53
|
+
# }}}2
|
54
|
+
|
55
|
+
# --
|
56
|
+
|
57
|
+
# configuration base class
|
58
|
+
class ServiceCfg # {{{2
|
59
|
+
# data
|
60
|
+
attr_reader :_data
|
61
|
+
|
62
|
+
# init
|
63
|
+
def initialize
|
64
|
+
@_data = {}
|
65
|
+
end
|
66
|
+
|
67
|
+
# pass on to _service or super
|
68
|
+
def method_missing (meth, *args, &block)
|
69
|
+
if GitBak::Services::SERVICES.include? meth
|
70
|
+
_service meth, *args
|
71
|
+
else
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end # }}}2
|
76
|
+
|
77
|
+
# authentication configuration
|
78
|
+
class AuthCfg < ServiceCfg # {{{2
|
79
|
+
# set service auth
|
80
|
+
def _service (name, user, pass = nil)
|
81
|
+
(@_data[name] ||= {})[user] = { user: user, pass: pass }
|
82
|
+
end
|
83
|
+
end # }}}2
|
84
|
+
|
85
|
+
# repository configuration
|
86
|
+
class ReposCfg < ServiceCfg # {{{2
|
87
|
+
# set service repo
|
88
|
+
def _service (name, dir, user, opts = {})
|
89
|
+
c = opts.merge dir: dir, user: user
|
90
|
+
c[:auth] = c[:user] if c[:auth] == true
|
91
|
+
(@_data[name] ||= []) << c
|
92
|
+
end
|
93
|
+
end # }}}2
|
94
|
+
|
95
|
+
# authentication and repository configuration
|
96
|
+
class Cfg # {{{2
|
97
|
+
# data
|
98
|
+
attr_reader :auth, :repos
|
99
|
+
|
100
|
+
# init
|
101
|
+
def initialize
|
102
|
+
@auth = AuthCfg.new
|
103
|
+
@repos = ReposCfg.new
|
104
|
+
end
|
105
|
+
|
106
|
+
# get data
|
107
|
+
def data # {{{3
|
108
|
+
auth = @auth._data
|
109
|
+
repos = @repos._data
|
110
|
+
|
111
|
+
{ auth: auth, repos: repos }
|
112
|
+
end # }}}3
|
113
|
+
end # }}}2
|
114
|
+
|
115
|
+
# --
|
116
|
+
|
117
|
+
# load configuration file
|
118
|
+
def self.load (file) # {{{2
|
119
|
+
cfg = eval File.read(file), GitBak::Eval.new.binding, file # ???
|
120
|
+
|
121
|
+
raise ConfigError, "[#{file}] isn't a GitBak::Config::Cfg " \
|
122
|
+
"(#{cfg.class} instead)." \
|
123
|
+
unless Cfg === cfg
|
124
|
+
|
125
|
+
cfg
|
126
|
+
end # }}}2
|
127
|
+
|
128
|
+
end # }}}1
|
129
|
+
|
130
|
+
# configure
|
131
|
+
def self.configure (&block)
|
132
|
+
cfg = Config::Cfg.new
|
133
|
+
block[cfg.auth, cfg.repos]
|
134
|
+
cfg
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
# vim: set tw=70 sw=2 sts=2 et fdm=marker :
|
data/lib/gitbak/eval.rb
ADDED
data/lib/gitbak/exec.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'gitbak'
|
2
|
+
require 'gitbak/config'
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
# --
|
7
|
+
|
8
|
+
# gitbak namespace
|
9
|
+
module GitBak
|
10
|
+
# command-line executable
|
11
|
+
module Executable
|
12
|
+
|
13
|
+
# command-line usage
|
14
|
+
USAGE = 'gitbak [<option(s)>]'
|
15
|
+
|
16
|
+
# parse command line options; die on failure
|
17
|
+
def self.parse_options (args) # {{{1
|
18
|
+
args_ = args.dup
|
19
|
+
options = { cfgfile: "#{Dir.home}/.gitbak", verbose: false,
|
20
|
+
noact: false }
|
21
|
+
|
22
|
+
op = OptionParser.new do |opts| # {{{2
|
23
|
+
opts.banner = USAGE
|
24
|
+
|
25
|
+
opts.on('-c', '--config-file FILE',
|
26
|
+
'Configuration file') do |x|
|
27
|
+
options[:cfgfile] = x
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on('-v', '--[no-]verbose', 'Run verbosely') do |x|
|
31
|
+
options[:verbose] = x
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on('-n', '--no-act', 'List w/o mirroring') do |x|
|
35
|
+
options[:noact] = !x
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
39
|
+
puts GitBak::Config::INFO, '', opts
|
40
|
+
exit
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on_tail('-e', '--example',
|
44
|
+
'Show example configuration') do
|
45
|
+
puts GitBak::Config::CONFIG_EX
|
46
|
+
exit
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on_tail('--version', 'Show version') do
|
50
|
+
puts "gitbak v#{GitBak::VERSION}"
|
51
|
+
exit
|
52
|
+
end
|
53
|
+
end # }}}2
|
54
|
+
|
55
|
+
begin
|
56
|
+
op.parse! args_
|
57
|
+
rescue OptionParser::ParseError => e
|
58
|
+
GitBak::Misc.die! "#{e}\n\n#{op}"
|
59
|
+
end
|
60
|
+
|
61
|
+
GitBak::Misc.die! "usage: #{USAGE}" unless args_.length == 0
|
62
|
+
|
63
|
+
options
|
64
|
+
end # }}}1
|
65
|
+
|
66
|
+
# parse configuration file; die on failure
|
67
|
+
def self.parse_cfgfile (file)
|
68
|
+
GitBak::Misc.die! "configuration file (#{file}) not found" \
|
69
|
+
unless GitBak::Misc.exists? file
|
70
|
+
|
71
|
+
GitBak::Config.load file
|
72
|
+
end
|
73
|
+
|
74
|
+
# run!
|
75
|
+
def self.main (args = nil)
|
76
|
+
options = parse_options (args or ARGV)
|
77
|
+
cfg = parse_cfgfile options[:cfgfile]
|
78
|
+
|
79
|
+
GitBak.main options[:verbose], options[:noact], cfg.data
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# vim: set tw=70 sw=2 sts=2 et fdm=marker :
|
data/lib/gitbak/misc.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'io/console'
|
2
|
+
|
3
|
+
# --
|
4
|
+
|
5
|
+
# gitbak namespace
|
6
|
+
module GitBak
|
7
|
+
|
8
|
+
# base error class
|
9
|
+
class Error < RuntimeError; end
|
10
|
+
|
11
|
+
# miscellaneous
|
12
|
+
module Misc
|
13
|
+
|
14
|
+
# command execution failure
|
15
|
+
class SysError < GitBak::Error; end
|
16
|
+
|
17
|
+
# --
|
18
|
+
|
19
|
+
# deep copy using Marshal
|
20
|
+
def self.deepdup (obj)
|
21
|
+
Marshal.load(Marshal.dump obj)
|
22
|
+
end
|
23
|
+
|
24
|
+
# print msg to stderr and exit
|
25
|
+
def self.die! (msg)
|
26
|
+
STDERR.puts msg
|
27
|
+
exit 1
|
28
|
+
end
|
29
|
+
|
30
|
+
# does file/dir or symlink exists?
|
31
|
+
def self.exists? (path)
|
32
|
+
File.exists?(path) or File.symlink?(path)
|
33
|
+
end
|
34
|
+
|
35
|
+
# prompt for line; optionally hide input
|
36
|
+
def self.prompt (prompt, hide = false) # {{{1
|
37
|
+
STDOUT.print prompt
|
38
|
+
STDOUT.flush
|
39
|
+
|
40
|
+
if hide
|
41
|
+
line = STDIN.noecho { |i| i.gets }
|
42
|
+
STDOUT.puts
|
43
|
+
else
|
44
|
+
line = STDIN.gets
|
45
|
+
end
|
46
|
+
|
47
|
+
line and line.chomp
|
48
|
+
end # }}}1
|
49
|
+
|
50
|
+
# execute command; raises SysError on failure; optionally verbose
|
51
|
+
def self.sys (verbose, cmd, *args)
|
52
|
+
puts "$ #{ ([cmd] + args).join ' ' }" if verbose
|
53
|
+
system [cmd, cmd], *args or raise SysError,
|
54
|
+
"failed to run command #{ ([cmd] + args) } (#$?)"
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# vim: set tw=70 sw=2 sts=2 et fdm=marker :
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'gitbak/misc'
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'open-uri'
|
5
|
+
|
6
|
+
# --
|
7
|
+
|
8
|
+
# gitbak namespace
|
9
|
+
module GitBak
|
10
|
+
# git services
|
11
|
+
module Services
|
12
|
+
|
13
|
+
# authentication error
|
14
|
+
class AuthError < GitBak::Error; end
|
15
|
+
|
16
|
+
# --
|
17
|
+
|
18
|
+
# avaliable services
|
19
|
+
SERVICES = %w{ bitbucket github gist }.map(&:to_sym)
|
20
|
+
|
21
|
+
# use another service's authentication
|
22
|
+
USE_AUTH = { gist: :github }
|
23
|
+
|
24
|
+
# API urls
|
25
|
+
APIS = {
|
26
|
+
bitbucket: ->(user) { "api.bitbucket.org/1.0/users/#{user}" },
|
27
|
+
github: ->(user) { "api.github.com/users/#{user}/repos" },
|
28
|
+
gist: ->(user) { "api.github.com/users/#{user}/gists" },
|
29
|
+
}
|
30
|
+
|
31
|
+
# remote urls
|
32
|
+
REMOTES = # {{{1
|
33
|
+
{
|
34
|
+
bitbucket: {
|
35
|
+
ssh: ->(u, r) { "git@bitbucket.org:#{u}/#{r}.git" },
|
36
|
+
https: ->(u, r) { "https://bitbucket.org/#{u}/#{r}.git" },
|
37
|
+
},
|
38
|
+
github: {
|
39
|
+
ssh: ->(u, r) { "git@github.com:#{u}/#{r}.git" },
|
40
|
+
https: ->(u, r) { "https://github.com/#{u}/#{r}.git" },
|
41
|
+
},
|
42
|
+
gist: {
|
43
|
+
ssh: ->(id) { "git@gist.github.com:#{id}.git" },
|
44
|
+
https: ->(id) { "https://gist.github.com/#{id}.git" },
|
45
|
+
},
|
46
|
+
} # }}}1
|
47
|
+
|
48
|
+
# long keyword^wsymbol ;-)
|
49
|
+
AUTH = :http_basic_authentication
|
50
|
+
|
51
|
+
# --
|
52
|
+
|
53
|
+
# get data from API
|
54
|
+
def self.api_get (service, user, auth) # {{{1
|
55
|
+
url = "https://#{APIS[service][user]}"
|
56
|
+
opts = auth ? { AUTH => [auth[:user], auth[:pass]] } : {}
|
57
|
+
|
58
|
+
begin
|
59
|
+
data = open url, opts
|
60
|
+
rescue OpenURI::HTTPError => e
|
61
|
+
if e.io.status[0] == '401'
|
62
|
+
raise AuthError,
|
63
|
+
"401 for #{auth[:user]} on #{service}/#{user}"
|
64
|
+
else
|
65
|
+
raise
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
data
|
70
|
+
end # }}}1
|
71
|
+
|
72
|
+
# get repositories from service; uses api_get if service in APIS,
|
73
|
+
# api_get_<service> otherwise
|
74
|
+
# -> [{name:,remote:,description:},...]
|
75
|
+
def self.repositories (service, cfg, auth)
|
76
|
+
rem = REMOTES[service][cfg.fetch(:method, :ssh).to_sym]
|
77
|
+
args = [service, cfg[:user], auth]
|
78
|
+
data = APIS[service] ? api_get(*args) :
|
79
|
+
send("api_get_#{service}", *args)
|
80
|
+
send service, cfg, data, rem
|
81
|
+
end
|
82
|
+
|
83
|
+
# --
|
84
|
+
|
85
|
+
# turn bitbucket API data into a list of repositories
|
86
|
+
def self.bitbucket (cfg, data, rem)
|
87
|
+
data_ = JSON.load data
|
88
|
+
repos = data_['repositories'].select { |r| r['scm'] == 'git' }
|
89
|
+
repos.map do |r|
|
90
|
+
{ name: r['name'], remote: rem[cfg[:user], r['name']],
|
91
|
+
description: r['description'] }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# turn github API data into a list of repositories
|
96
|
+
def self.github (cfg, data, rem)
|
97
|
+
JSON.load(data).map do |r|
|
98
|
+
{ name: r['name'], remote: rem[cfg[:user], r['name']],
|
99
|
+
description: r['description'] }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# turn gist API data into a list of repositories
|
104
|
+
def self.gist (cfg, data, rem)
|
105
|
+
JSON.load(data).map do |r|
|
106
|
+
{ name: r['id'], remote: rem[r['id']],
|
107
|
+
description: r['description'] }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# vim: set tw=70 sw=2 sts=2 et fdm=marker :
|
data/lib/gitbak/version.rb
CHANGED
data/lib/gitbak.rb
CHANGED
@@ -1,173 +1,116 @@
|
|
1
|
+
require 'gitbak/misc'
|
2
|
+
require 'gitbak/services'
|
1
3
|
require 'gitbak/version'
|
2
4
|
|
3
5
|
require 'fileutils'
|
4
|
-
require 'io/console'
|
5
|
-
require 'json'
|
6
|
-
require 'open-uri'
|
7
6
|
|
8
7
|
# --
|
9
8
|
|
9
|
+
# gitbak namespace
|
10
10
|
module GitBak
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
gist: ->(user) { "api.github.com/users/#{user}/gists" },
|
18
|
-
}
|
19
|
-
|
20
|
-
REMOTES = { # {{{1
|
21
|
-
bitbucket: {
|
22
|
-
ssh: ->(u, r) { "git@bitbucket.org:#{u}/#{r}.git" },
|
23
|
-
https: ->(u, r) { "https://bitbucket.org/#{u}/#{r}.git" },
|
24
|
-
},
|
25
|
-
github: {
|
26
|
-
ssh: ->(u, r) { "git@github.com:#{u}/#{r}.git" },
|
27
|
-
https: ->(u, r) { "https://github.com/#{u}/#{r}.git" },
|
28
|
-
},
|
29
|
-
gist: {
|
30
|
-
ssh: ->(id) { "git@gist.github.com:#{id}.git" },
|
31
|
-
https: ->(id) { "https://gist.github.com/#{id}.git" },
|
32
|
-
},
|
33
|
-
} # }}}1
|
34
|
-
|
35
|
-
REMOTE = ->(s, x) { REMOTES[s][x.fetch(:method, :ssh).to_sym] }
|
36
|
-
|
37
|
-
class << self
|
38
|
-
|
39
|
-
def die (msg)
|
40
|
-
STDERR.puts msg
|
41
|
-
exit 1
|
42
|
-
end
|
43
|
-
|
44
|
-
def exists? (path)
|
45
|
-
File.exists?(path) or File.symlink?(path)
|
46
|
-
end
|
47
|
-
|
48
|
-
def sys (verbose, cmd, *args)
|
49
|
-
puts " $ #{([cmd] + args).join ' '}" if verbose
|
50
|
-
system [cmd, cmd], *args or raise 'OOPS' # TODO
|
51
|
-
end
|
52
|
-
|
53
|
-
def prompt (prompt, hide = false) # {{{1
|
54
|
-
STDOUT.print prompt
|
55
|
-
STDOUT.flush
|
56
|
-
|
57
|
-
if hide
|
58
|
-
line = STDIN.noecho { |i| i.readline }
|
59
|
-
STDOUT.puts
|
60
|
-
line
|
61
|
-
else
|
62
|
-
STDIN.readline
|
63
|
-
end .chomp
|
64
|
-
end # }}}1
|
12
|
+
# extract name from remote; e.g. "git@server:foo/bar.git" and
|
13
|
+
# "https://server/foo/bar.git" become "bar"
|
14
|
+
def self.repo_name (remote)
|
15
|
+
remote.sub(%r!^.*[/:]!, '').sub(/\.git$/, '')
|
16
|
+
end
|
65
17
|
|
66
|
-
|
18
|
+
# clone (from remote) or update repository (in dir); optionally
|
19
|
+
# verbose
|
20
|
+
def self.mirror_repo (verbose, remote, dir) # {{{1
|
21
|
+
name = repo_name remote
|
22
|
+
name_ = name + '.git'
|
23
|
+
repo_dir = "#{dir}/#{name_}"
|
67
24
|
|
68
|
-
|
69
|
-
opts = auth ? { http_basic_authentication:
|
70
|
-
[auth[:user], auth[:pass]] } : {}
|
25
|
+
FileUtils.mkdir_p dir
|
71
26
|
|
72
|
-
|
27
|
+
if Misc.exists? repo_dir
|
28
|
+
puts "$ cd #{repo_dir}" if verbose
|
29
|
+
FileUtils.cd(repo_dir) do
|
30
|
+
Misc.sys verbose, *%w{ git remote update }
|
31
|
+
end
|
32
|
+
else
|
33
|
+
puts "$ cd #{dir}" if verbose
|
34
|
+
FileUtils.cd(dir) do
|
35
|
+
Misc.sys verbose,
|
36
|
+
*( %w{ git clone --mirror -n } + [remote, name_] )
|
37
|
+
end
|
73
38
|
end
|
39
|
+
end # }}}1
|
74
40
|
|
75
|
-
|
76
|
-
remote.sub(%r!^.*[/:]!, '').sub(/\.git$/, '')
|
77
|
-
end
|
41
|
+
# --
|
78
42
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
repo_dir = "#{dir}/#{name_}"
|
43
|
+
# check auth; ask passwords
|
44
|
+
def self.process_config (config) # {{{1
|
45
|
+
config_ = Misc.deepdup config
|
83
46
|
|
84
|
-
|
47
|
+
config_[:repos].each do |service, cfgs|
|
48
|
+
auth = config_[:auth][service] ||= {}
|
49
|
+
cfgs.each do |cfg|
|
50
|
+
user = cfg[:auth]
|
51
|
+
auth[user] = { user: user, pass: nil } if user && !auth[user]
|
52
|
+
end
|
53
|
+
end
|
85
54
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
FileUtils.cd(dir) do
|
92
|
-
sys verbose,
|
93
|
-
*( %w{ git clone --mirror -n } + [remote, name_] )
|
94
|
-
end
|
55
|
+
config_[:auth].each do |service, auth|
|
56
|
+
next if GitBak::Services::USE_AUTH[service]
|
57
|
+
auth.each_value do |x|
|
58
|
+
p = "#{service} password for #{x[:user]}: "
|
59
|
+
x[:pass] ||= Misc.prompt p, true # TODO
|
95
60
|
end
|
96
|
-
end
|
61
|
+
end
|
97
62
|
|
98
|
-
|
63
|
+
[config_[:auth], config_[:repos]]
|
64
|
+
end # }}}1
|
99
65
|
|
100
|
-
|
101
|
-
|
66
|
+
# fetch repository lists; optionally verbose
|
67
|
+
def self.fetch (verbose, auth, repos) # {{{1
|
68
|
+
repos.map do |service, cfgs|
|
69
|
+
au = auth[Services::USE_AUTH.fetch service, service]
|
70
|
+
cfgs.map do |cfg|
|
71
|
+
puts "listing #{service}/#{cfg[:user]} ..." if verbose
|
102
72
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
73
|
+
begin
|
74
|
+
rs = Services.repositories service, cfg, au[cfg[:auth]]
|
75
|
+
rescue Services::AuthError => e
|
76
|
+
Misc.die! "authentication failure: #{e}"
|
107
77
|
end
|
108
|
-
end # }}}1
|
109
|
-
|
110
|
-
def repos_github (x, auth) # {{{1
|
111
|
-
rem = REMOTE[:github, x]
|
112
78
|
|
113
|
-
|
114
|
-
{ remote: rem[x[:user], r['name']],
|
115
|
-
description: r['description'], name: r['name'] }
|
79
|
+
[service, cfg[:user], cfg[:dir], rs]
|
116
80
|
end
|
117
|
-
end
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
# --
|
129
|
-
|
130
|
-
def mirror_service (service, x, auth, verbose) # {{{1
|
131
|
-
puts "#{service} for #{x[:user]} ..."
|
132
|
-
|
133
|
-
auth_ = x[:auth] && auth[x[ x[:auth] == true ? :user : :auth ]]
|
134
|
-
repos = send "repos_#{service}", x, auth_
|
135
|
-
|
136
|
-
repos.each do |r|
|
137
|
-
d = r[:description]
|
138
|
-
puts "==> #{service} | #{x[:user]} | #{r[:name]} | #{d} <==" \
|
139
|
-
if verbose
|
140
|
-
mirror r[:remote], x[:dir], verbose
|
81
|
+
end .flatten 1
|
82
|
+
end # }}}1
|
83
|
+
|
84
|
+
# mirror repositories; optionally verbose
|
85
|
+
def self.mirror verbose, repos # {{{1
|
86
|
+
repos.each do |s, usr, dir, rs|
|
87
|
+
rs.each do |r|
|
88
|
+
name, desc = r[:name], r[:description]
|
89
|
+
puts "==> #{s} | #{usr} | #{name} | #{desc} <==" if verbose
|
90
|
+
mirror_repo verbose, r[:remote], dir
|
141
91
|
puts if verbose
|
142
92
|
end
|
93
|
+
end
|
94
|
+
end # }}}1
|
95
|
+
|
96
|
+
# print summary
|
97
|
+
def self.summary repos # {{{1
|
98
|
+
puts '', '=== Summary ===', ''
|
99
|
+
repos.each do |service, usr, dir, rs|
|
100
|
+
printf " %-15s for %-20s: %10s repositories\n",
|
101
|
+
service, usr, rs.length
|
102
|
+
end
|
103
|
+
puts
|
104
|
+
end # }}}1
|
143
105
|
|
144
|
-
|
145
|
-
end # }}}1
|
146
|
-
|
147
|
-
# --
|
148
|
-
|
149
|
-
def main (config) # {{{1
|
150
|
-
sum = {}
|
151
|
-
|
152
|
-
SERVICES.map { |s| s.downcase.to_sym } .each do |s|
|
153
|
-
sum[s] = {}
|
154
|
-
config[s].each do |x|
|
155
|
-
sum[s][x[:user]] = mirror_service s, x,
|
156
|
-
config[:auth][s], config[:verbose]
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
if config[:verbose]
|
161
|
-
puts '', "=== Summary ===", ''
|
162
|
-
sum.each_pair do |s, x|
|
163
|
-
x.each_pair do |u, n|
|
164
|
-
printf " %-15s for %-20s: %10s repositories\n", s, u, n
|
165
|
-
end
|
166
|
-
end
|
167
|
-
puts
|
168
|
-
end
|
169
|
-
end # }}}1
|
106
|
+
# --
|
170
107
|
|
108
|
+
# run!
|
109
|
+
def self.main (verbose, noact, config)
|
110
|
+
auth, repos = process_config config
|
111
|
+
repositories = fetch verbose, auth, repos
|
112
|
+
mirror verbose, repositories unless noact
|
113
|
+
summary repositories if verbose
|
171
114
|
end
|
172
115
|
|
173
116
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gitbak
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-12-
|
12
|
+
date: 2012-12-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json
|
16
|
-
requirement: &
|
16
|
+
requirement: &9773800 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,8 +21,8 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
25
|
-
description: ! 'GitBak allows you to mirror GitHub/
|
24
|
+
version_requirements: *9773800
|
25
|
+
description: ! 'GitBak allows you to mirror Bitbucket/GitHub/Gist repositories
|
26
26
|
|
27
27
|
easily; you only need to specify paths, users, and authentication
|
28
28
|
|
@@ -36,9 +36,14 @@ executables:
|
|
36
36
|
extensions: []
|
37
37
|
extra_rdoc_files: []
|
38
38
|
files:
|
39
|
-
- README
|
39
|
+
- README.md
|
40
40
|
- bin/gitbak
|
41
|
+
- lib/gitbak/exec.rb
|
42
|
+
- lib/gitbak/config.rb
|
41
43
|
- lib/gitbak/version.rb
|
44
|
+
- lib/gitbak/misc.rb
|
45
|
+
- lib/gitbak/eval.rb
|
46
|
+
- lib/gitbak/services.rb
|
42
47
|
- lib/gitbak.rb
|
43
48
|
homepage: https://github.com/obfusk/gitbak
|
44
49
|
licenses:
|
@@ -66,3 +71,4 @@ signing_key:
|
|
66
71
|
specification_version: 3
|
67
72
|
summary: bitbucket/github/gist backup
|
68
73
|
test_files: []
|
74
|
+
has_rdoc:
|
data/README
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
-- {{{1
|
2
|
-
|
3
|
-
File : README
|
4
|
-
Maintainer : Felix C. Stegerman <flx@obfusk.net>
|
5
|
-
Date : 2012-12-27
|
6
|
-
|
7
|
-
Copyright : Copyright (C) 2012 Felix C. Stegerman
|
8
|
-
Version : v0.2.0
|
9
|
-
|
10
|
-
-- }}}1
|
11
|
-
|
12
|
-
=== Description === {{{1
|
13
|
-
|
14
|
-
gitbak - bitbucket/github/gist backup
|
15
|
-
|
16
|
-
GitBak allows you to mirror GitHub/Bitbucket/Gist repositories
|
17
|
-
easily; you only need to specify paths, users, and authentication in
|
18
|
-
~/.gitbak and it does the rest.
|
19
|
-
}}}1
|
20
|
-
|
21
|
-
=== Usage === {{{1
|
22
|
-
|
23
|
-
$ gitbak --help # read documentation
|
24
|
-
$ vim ~/.gitbak # configure
|
25
|
-
$ gitbak # mirror
|
26
|
-
}}}1
|
27
|
-
|
28
|
-
=== Installing === {{{1
|
29
|
-
|
30
|
-
$ gem install gitbak # rubygems
|
31
|
-
|
32
|
-
Get it at https://github.com/obfusk/gitbak. Depends: git, ruby.
|
33
|
-
}}}1
|
34
|
-
|
35
|
-
=== TODO === {{{1
|
36
|
-
|
37
|
-
Some things that may be useful/implemented at some point.
|
38
|
-
|
39
|
-
* custom services (should be easy already)
|
40
|
-
* metadata (issues, wikis, ...)
|
41
|
-
* teams/organisations
|
42
|
-
* filtering
|
43
|
-
* starred repos/gists
|
44
|
-
* specify ssh key(s)
|
45
|
-
* oauth ?!
|
46
|
-
* https clone auth
|
47
|
-
}}}1
|
48
|
-
|
49
|
-
=== License === {{{1
|
50
|
-
|
51
|
-
GPLv2 [1].
|
52
|
-
}}}1
|
53
|
-
|
54
|
-
=== References === {{{1
|
55
|
-
|
56
|
-
[1] GNU General Public License, version 2
|
57
|
-
http://www.opensource.org/licenses/GPL-2.0
|
58
|
-
}}}1
|
59
|
-
|
60
|
-
--
|
61
|
-
|
62
|
-
vim: set tw=70 sw=2 sts=2 et fdm=marker :
|