gitbak 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 :
|