dandelion 0.1.7 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +11 -8
- data/Rakefile +8 -1
- data/dandelion.gemspec +2 -2
- data/lib/dandelion/cli.rb +119 -74
- data/lib/dandelion/deployment.rb +5 -2
- data/lib/dandelion/service.rb +91 -15
- data/lib/dandelion/version.rb +1 -1
- data/test/fixtures/diff +3 -0
- data/test/fixtures/ls_tree +4 -0
- data/test/test_diff_deployment.rb +123 -0
- data/test/test_git.git/HEAD +1 -0
- data/test/test_git.git/config +5 -0
- data/test/test_git.git/description +1 -0
- data/test/test_git.git/hooks/applypatch-msg.sample +15 -0
- data/test/test_git.git/hooks/commit-msg.sample +24 -0
- data/test/test_git.git/hooks/post-commit.sample +8 -0
- data/test/test_git.git/hooks/post-receive.sample +15 -0
- data/test/test_git.git/hooks/post-update.sample +8 -0
- data/test/test_git.git/hooks/pre-applypatch.sample +14 -0
- data/test/test_git.git/hooks/pre-commit.sample +46 -0
- data/test/test_git.git/hooks/pre-rebase.sample +169 -0
- data/test/test_git.git/hooks/prepare-commit-msg.sample +36 -0
- data/test/test_git.git/hooks/update.sample +128 -0
- data/test/test_git.git/info/exclude +6 -0
- data/test/test_git.git/objects/0c/a605e9f0f1d42ce8193ac36db11ec3cc9efc08 +0 -0
- data/test/test_git.git/objects/11/bada4e36fd065c8d1d3ca97b8dffa496c8e021 +0 -0
- data/test/test_git.git/objects/57/16ca5987cbf97d6bb54920bea6adde242d87e6 +0 -0
- data/test/test_git.git/objects/88/d4480861346093048e08ce8dcc577d8aa69379 +1 -0
- data/test/test_git.git/objects/90/2dce0535b19f0c15ac8407fc4468256ad672d7 +0 -0
- data/test/test_git.git/objects/a6/394b3e8a82b76b0dd5b6b317f489dfe22426a6 +0 -0
- data/test/test_git.git/objects/a6/5140d5ec9f47064f614ecf8e43776baa5c0c11 +0 -0
- data/test/test_git.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 +0 -0
- data/test/test_git.git/objects/ea/41dba10b54a794284e0be009a11f0ff3716a28 +0 -0
- data/test/test_git.git/objects/f5/5f3c44c89e5d215fbaaef9d33563117fe0b61b +1 -0
- data/test/test_git.git/objects/ff/1f1d4bd0c99e1c9cca047c46b2194accf89504 +4 -0
- data/test/test_git.git/refs/heads/master +1 -0
- data/test/test_git.rb +44 -0
- metadata +84 -9
data/README.md
CHANGED
@@ -9,7 +9,7 @@ Ensure that Ruby and RubyGems are installed, then run:
|
|
9
9
|
|
10
10
|
Alternatively, you can build the gem yourself:
|
11
11
|
|
12
|
-
$ git clone git://github.com/
|
12
|
+
$ git clone git://github.com/scttnlsn/dandelion.git
|
13
13
|
$ cd dandelion
|
14
14
|
$ rake install
|
15
15
|
|
@@ -19,7 +19,7 @@ Configuration options are specified in a YAML file (Dandelion looks for a file
|
|
19
19
|
named `dandelion.yml` by default):
|
20
20
|
|
21
21
|
# Required
|
22
|
-
scheme: sftp
|
22
|
+
scheme: sftp # sftp/ftp
|
23
23
|
host: example.com
|
24
24
|
username: user
|
25
25
|
password: pass
|
@@ -34,11 +34,11 @@ Usage
|
|
34
34
|
-----
|
35
35
|
From the root directory of a Git repository, run:
|
36
36
|
|
37
|
-
$ dandelion
|
37
|
+
$ dandelion deploy
|
38
38
|
|
39
39
|
Or:
|
40
40
|
|
41
|
-
$ dandelion path/to/config.yml
|
41
|
+
$ dandelion deploy path/to/config.yml
|
42
42
|
|
43
43
|
This will deploy the local `HEAD` revision to the server specified in the config
|
44
44
|
file. Dandelion keeps track of the most recently deployed revision so that only
|
@@ -47,8 +47,11 @@ files which have changed since the last deployment need to be transferred.
|
|
47
47
|
For a more complete summary of usage options, run:
|
48
48
|
|
49
49
|
$ dandelion -h
|
50
|
-
Usage: dandelion [options] [
|
51
|
-
-f, --force Force deployment
|
52
|
-
-s, --status Display revision status
|
50
|
+
Usage: dandelion [options] [[command] [options]]
|
53
51
|
-v, --version Display the current version
|
54
|
-
-h, --help Display this screen
|
52
|
+
-h, --help Display this screen
|
53
|
+
--repo=[REPO] Use the given repository
|
54
|
+
|
55
|
+
Available commands:
|
56
|
+
deploy
|
57
|
+
status
|
data/Rakefile
CHANGED
data/dandelion.gemspec
CHANGED
@@ -8,9 +8,9 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ['Scott Nelson']
|
10
10
|
s.email = ['scottbnel@gmail.com']
|
11
|
-
s.homepage = 'http://github.com/
|
11
|
+
s.homepage = 'http://github.com/scttnlsn/dandelion'
|
12
12
|
s.summary = "dandelion-#{s.version}"
|
13
|
-
s.description = 'Git repository deployment via SFTP'
|
13
|
+
s.description = 'Git repository deployment via FTP/SFTP'
|
14
14
|
|
15
15
|
s.add_dependency 'net-sftp', '>= 2.0.5'
|
16
16
|
s.add_dependency 'grit', '>= 2.4.1'
|
data/lib/dandelion/cli.rb
CHANGED
@@ -8,23 +8,50 @@ require 'yaml'
|
|
8
8
|
|
9
9
|
module Dandelion
|
10
10
|
module Cli
|
11
|
-
class UnsupportedSchemeError < StandardError; end
|
12
|
-
|
13
11
|
class Options
|
12
|
+
attr_reader :config_file
|
13
|
+
|
14
14
|
def initialize
|
15
15
|
@options = {}
|
16
|
-
@
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
16
|
+
@config_file = 'dandelion.yml'
|
17
|
+
@global = global_parser
|
18
|
+
@commands = { 'deploy' => deploy_parser, 'status' => status_parser }
|
19
|
+
@commands_help = "\nAvailable commands:\n #{@commands.keys.join("\n ")}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse(args)
|
23
|
+
order @global, args
|
24
|
+
command = args.shift
|
25
|
+
if command and @commands[command]
|
26
|
+
order @commands[command], args
|
27
|
+
end
|
28
|
+
|
29
|
+
if @commands.key? command
|
30
|
+
@config_file = args.shift.strip if args[0]
|
31
|
+
command
|
32
|
+
else
|
33
|
+
if not @command.nil?
|
34
|
+
puts "Invalid command: #{command}"
|
22
35
|
end
|
36
|
+
puts @global.help
|
37
|
+
puts @commands_help
|
38
|
+
exit
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def [](key)
|
43
|
+
@options[key]
|
44
|
+
end
|
23
45
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
46
|
+
def []=(key, value)
|
47
|
+
@options[key] = value
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def global_parser
|
53
|
+
OptionParser.new do |opts|
|
54
|
+
opts.banner = 'Usage: dandelion [options] [[command] [options]]'
|
28
55
|
|
29
56
|
opts.on('-v', '--version', 'Display the current version') do
|
30
57
|
puts "Dandelion v#{Dandelion::VERSION}"
|
@@ -33,117 +60,135 @@ module Dandelion
|
|
33
60
|
|
34
61
|
opts.on('-h', '--help', 'Display this screen') do
|
35
62
|
puts opts
|
63
|
+
puts @commands_help
|
36
64
|
exit
|
37
65
|
end
|
66
|
+
|
67
|
+
@options[:repo] = '.'
|
68
|
+
opts.on('--repo=[REPO]', 'Use the given repository') do |repo|
|
69
|
+
@options[:repo] = repo
|
70
|
+
end
|
38
71
|
end
|
39
72
|
end
|
40
73
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
74
|
+
def deploy_parser
|
75
|
+
OptionParser.new do |opts|
|
76
|
+
opts.banner = 'Usage: dandelion deploy [options]'
|
77
|
+
|
78
|
+
@options[:force] = false
|
79
|
+
opts.on('-f', '--force', 'Force deployment') do
|
80
|
+
@options[:force] = true
|
81
|
+
end
|
82
|
+
end
|
44
83
|
end
|
45
84
|
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
else
|
50
|
-
'dandelion.yml'
|
85
|
+
def status_parser
|
86
|
+
OptionParser.new do |opts|
|
87
|
+
opts.banner = 'Usage: dandelion status'
|
51
88
|
end
|
52
89
|
end
|
53
90
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
91
|
+
def order(parser, args)
|
92
|
+
begin
|
93
|
+
parser.order!(args)
|
94
|
+
rescue OptionParser::InvalidOption => e
|
95
|
+
puts e.to_s.capitalize
|
96
|
+
puts parser.help
|
97
|
+
exit
|
98
|
+
end
|
60
99
|
end
|
61
100
|
end
|
62
101
|
|
63
102
|
class Main
|
64
103
|
class << self
|
65
104
|
def execute(args)
|
66
|
-
new(args).execute
|
105
|
+
new(args).execute
|
67
106
|
end
|
68
107
|
end
|
69
108
|
|
70
109
|
def initialize(args)
|
71
110
|
@options = Options.new
|
72
|
-
@options.parse
|
111
|
+
@command = @options.parse args
|
112
|
+
|
113
|
+
validate_files
|
114
|
+
@config = YAML.load_file(File.expand_path @options.config_file)
|
115
|
+
@repo = Git::Repo.new(File.expand_path @options[:repo])
|
73
116
|
end
|
74
117
|
|
75
118
|
def log
|
76
119
|
Dandelion.logger
|
77
120
|
end
|
78
121
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
Service::SFTP.new(config['host'], config['username'], config['password'], config['path'])
|
93
|
-
else
|
94
|
-
raise UnsupportedSchemeError
|
122
|
+
def execute
|
123
|
+
log.info("Connecting to: #{service.uri}")
|
124
|
+
deployment(service) do |d|
|
125
|
+
log.info("Remote revision: #{d.remote_revision || '---'}")
|
126
|
+
log.info("Local revision: #{d.local_revision}")
|
127
|
+
|
128
|
+
if @command == 'status'
|
129
|
+
exit
|
130
|
+
elsif @command == 'deploy'
|
131
|
+
validate_deployment d
|
132
|
+
d.deploy
|
133
|
+
log.info("Deployment complete")
|
134
|
+
end
|
95
135
|
end
|
96
136
|
end
|
97
137
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
begin
|
103
|
-
service = service config
|
104
|
-
rescue UnsupportedSchemeError
|
105
|
-
log.fatal("Unsupported scheme: #{config['scheme']}")
|
106
|
-
exit
|
107
|
-
end
|
108
|
-
|
109
|
-
log.info("Connecting to: #{service.uri}")
|
110
|
-
repo = Git::Repo.new('.')
|
111
|
-
|
138
|
+
private
|
139
|
+
|
140
|
+
def deployment(service)
|
112
141
|
begin
|
113
|
-
deployment = Deployment::DiffDeployment.new(repo, service, config['exclude'])
|
142
|
+
deployment = Deployment::DiffDeployment.new(@repo, service, @config['exclude'])
|
114
143
|
rescue Deployment::RemoteRevisionError
|
115
|
-
deployment = Deployment::FullDeployment.new(repo, service, config['exclude'])
|
144
|
+
deployment = Deployment::FullDeployment.new(@repo, service, @config['exclude'])
|
116
145
|
rescue Git::DiffError
|
117
146
|
log.fatal('Error: could not generate diff')
|
118
147
|
log.fatal('Try merging remote changes before running dandelion again')
|
119
148
|
exit
|
120
149
|
end
|
121
|
-
|
150
|
+
if block_given?
|
151
|
+
yield(deployment)
|
152
|
+
else
|
153
|
+
deployment
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def service
|
158
|
+
if @config['scheme'] == 'sftp'
|
159
|
+
Service::SFTP.new(@config['host'], @config['username'], @config['password'], @config['path'])
|
160
|
+
elsif @config['scheme'] == 'ftp'
|
161
|
+
Service::FTP.new(@config['host'], @config['username'], @config['password'], @config['path'])
|
162
|
+
else
|
163
|
+
log.fatal("Unsupported scheme: #{@config['scheme']}")
|
164
|
+
exit
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def validate_deployment(deployment)
|
122
169
|
begin
|
123
|
-
repo.remote_list.each do |remote|
|
170
|
+
@repo.remote_list.each do |remote|
|
124
171
|
deployment.validate_state(remote)
|
125
172
|
end
|
126
173
|
rescue Deployment::FastForwardError
|
127
|
-
if !@options[:force]
|
174
|
+
if !@options[:force]
|
128
175
|
log.warn('Warning: you are trying to deploy unpushed commits')
|
129
176
|
log.warn('This could potentially prevent others from being able to deploy')
|
130
177
|
log.warn('If you are sure you want to this, use the -f option to force deployment')
|
131
178
|
exit
|
132
179
|
end
|
133
180
|
end
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
181
|
+
end
|
182
|
+
|
183
|
+
def validate_files
|
184
|
+
unless File.exists? File.expand_path File.join(@options[:repo], '.git')
|
185
|
+
log.fatal("Not a git repository: #{@options[:repo]}")
|
186
|
+
exit
|
187
|
+
end
|
188
|
+
unless File.exists?(File.expand_path @options.config_file)
|
189
|
+
log.fatal("Could not find file: #{@options.config_file}")
|
142
190
|
exit
|
143
191
|
end
|
144
|
-
|
145
|
-
deployment.deploy
|
146
|
-
log.info("Deployment complete")
|
147
192
|
end
|
148
193
|
end
|
149
194
|
end
|
data/lib/dandelion/deployment.rb
CHANGED
@@ -30,8 +30,11 @@ module Dandelion
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def validate_state(remote = nil)
|
33
|
-
|
34
|
-
raise
|
33
|
+
begin
|
34
|
+
if remote and @repo.git.native(:remote, {:raise => true}, 'show', remote) =~ /fast-forward/i
|
35
|
+
raise FastForwardError
|
36
|
+
end
|
37
|
+
rescue Grit::Git::CommandFailed
|
35
38
|
end
|
36
39
|
end
|
37
40
|
|
data/lib/dandelion/service.rb
CHANGED
@@ -1,29 +1,107 @@
|
|
1
|
+
require 'net/ftp'
|
1
2
|
require 'net/sftp'
|
2
3
|
require 'tempfile'
|
3
4
|
|
4
5
|
module Dandelion
|
5
6
|
module Service
|
6
7
|
class MissingFileError < StandardError; end
|
7
|
-
|
8
|
+
|
8
9
|
class Service
|
9
10
|
def initialize(host, username, path)
|
10
11
|
@host = host
|
11
12
|
@username = username
|
12
13
|
@path = path
|
13
14
|
end
|
14
|
-
|
15
|
+
|
15
16
|
def uri
|
16
17
|
"#{@scheme}://#{@username}@#{@host}/#{@path}"
|
17
18
|
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def temp(file, data)
|
23
|
+
tmp = Tempfile.new(file.gsub('/', '.'))
|
24
|
+
tmp << data
|
25
|
+
tmp.flush
|
26
|
+
yield(tmp.path)
|
27
|
+
tmp.close
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class FTP < Service
|
32
|
+
def initialize(host, username, password, path)
|
33
|
+
super(host, username, path)
|
34
|
+
@scheme = 'ftp'
|
35
|
+
@ftp = Net::FTP.open(host, username, password)
|
36
|
+
@ftp.passive = true
|
37
|
+
@ftp.chdir(path)
|
38
|
+
end
|
39
|
+
|
40
|
+
def read(file)
|
41
|
+
begin
|
42
|
+
# Implementation of FTP#getbinaryfile differs between 1.8
|
43
|
+
# and 1.9 so we call FTP#retrbinary directly
|
44
|
+
content = ''
|
45
|
+
@ftp.retrbinary("RETR #{file}", 4096) do |data|
|
46
|
+
content += data
|
47
|
+
end
|
48
|
+
content
|
49
|
+
rescue Net::FTPPermError => e
|
50
|
+
raise MissingFileError
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def write(file, data)
|
55
|
+
# Creates directory only if necessary
|
56
|
+
mkdir_p(File.dirname(file))
|
57
|
+
|
58
|
+
temp(file, data) do |temp|
|
59
|
+
@ftp.putbinaryfile(temp, file)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def delete(file)
|
64
|
+
begin
|
65
|
+
@ftp.delete(file)
|
66
|
+
cleanup(File.dirname(file))
|
67
|
+
rescue Net::FTPPermError => e
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def cleanup(dir)
|
74
|
+
unless File.identical?(dir, @path)
|
75
|
+
if empty?(dir)
|
76
|
+
@ftp.rmdir(dir)
|
77
|
+
cleanup(File.dirname(dir))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def empty?(dir)
|
83
|
+
return @ftp.nlst(dir).empty?
|
84
|
+
end
|
85
|
+
|
86
|
+
def mkdir_p(dir)
|
87
|
+
return if dir == "."
|
88
|
+
parent_dir = File.dirname(dir)
|
89
|
+
file_names = @ftp.nlst(parent_dir)
|
90
|
+
unless file_names.include? dir
|
91
|
+
mkdir_p(parent_dir)
|
92
|
+
@ftp.mkdir(dir)
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
18
96
|
end
|
19
|
-
|
97
|
+
|
20
98
|
class SFTP < Service
|
21
99
|
def initialize(host, username, password, path)
|
22
100
|
super(host, username, path)
|
23
101
|
@scheme = 'sftp'
|
24
102
|
@sftp = Net::SFTP.start(host, username, :password => password)
|
25
103
|
end
|
26
|
-
|
104
|
+
|
27
105
|
def read(file)
|
28
106
|
begin
|
29
107
|
@sftp.file.open(File.join(@path, file), 'r') do |f|
|
@@ -34,7 +112,7 @@ module Dandelion
|
|
34
112
|
raise MissingFileError
|
35
113
|
end
|
36
114
|
end
|
37
|
-
|
115
|
+
|
38
116
|
def write(file, data)
|
39
117
|
path = File.join(@path, file)
|
40
118
|
begin
|
@@ -44,13 +122,11 @@ module Dandelion
|
|
44
122
|
raise unless e.code == 2
|
45
123
|
mkdir_p(dir)
|
46
124
|
end
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
@sftp.upload!(tmp.path, path)
|
51
|
-
tmp.close
|
125
|
+
temp(file, data) do |temp|
|
126
|
+
@sftp.upload!(temp, path)
|
127
|
+
end
|
52
128
|
end
|
53
|
-
|
129
|
+
|
54
130
|
def delete(file)
|
55
131
|
begin
|
56
132
|
path = File.join(@path, file)
|
@@ -60,9 +136,9 @@ module Dandelion
|
|
60
136
|
raise unless e.code == 2
|
61
137
|
end
|
62
138
|
end
|
63
|
-
|
139
|
+
|
64
140
|
private
|
65
|
-
|
141
|
+
|
66
142
|
def cleanup(dir)
|
67
143
|
unless File.identical?(dir, @path)
|
68
144
|
if empty?(dir)
|
@@ -71,13 +147,13 @@ module Dandelion
|
|
71
147
|
end
|
72
148
|
end
|
73
149
|
end
|
74
|
-
|
150
|
+
|
75
151
|
def empty?(dir)
|
76
152
|
@sftp.dir.entries(dir).map do |entry|
|
77
153
|
entry.name unless entry.name == '.' or entry.name == '..'
|
78
154
|
end.compact.empty?
|
79
155
|
end
|
80
|
-
|
156
|
+
|
81
157
|
def mkdir_p(dir)
|
82
158
|
begin
|
83
159
|
@sftp.mkdir!(dir)
|