dandelion 0.1.7 → 0.2.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 +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)
|