dandelion 0.1.7 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/README.md +11 -8
  2. data/Rakefile +8 -1
  3. data/dandelion.gemspec +2 -2
  4. data/lib/dandelion/cli.rb +119 -74
  5. data/lib/dandelion/deployment.rb +5 -2
  6. data/lib/dandelion/service.rb +91 -15
  7. data/lib/dandelion/version.rb +1 -1
  8. data/test/fixtures/diff +3 -0
  9. data/test/fixtures/ls_tree +4 -0
  10. data/test/test_diff_deployment.rb +123 -0
  11. data/test/test_git.git/HEAD +1 -0
  12. data/test/test_git.git/config +5 -0
  13. data/test/test_git.git/description +1 -0
  14. data/test/test_git.git/hooks/applypatch-msg.sample +15 -0
  15. data/test/test_git.git/hooks/commit-msg.sample +24 -0
  16. data/test/test_git.git/hooks/post-commit.sample +8 -0
  17. data/test/test_git.git/hooks/post-receive.sample +15 -0
  18. data/test/test_git.git/hooks/post-update.sample +8 -0
  19. data/test/test_git.git/hooks/pre-applypatch.sample +14 -0
  20. data/test/test_git.git/hooks/pre-commit.sample +46 -0
  21. data/test/test_git.git/hooks/pre-rebase.sample +169 -0
  22. data/test/test_git.git/hooks/prepare-commit-msg.sample +36 -0
  23. data/test/test_git.git/hooks/update.sample +128 -0
  24. data/test/test_git.git/info/exclude +6 -0
  25. data/test/test_git.git/objects/0c/a605e9f0f1d42ce8193ac36db11ec3cc9efc08 +0 -0
  26. data/test/test_git.git/objects/11/bada4e36fd065c8d1d3ca97b8dffa496c8e021 +0 -0
  27. data/test/test_git.git/objects/57/16ca5987cbf97d6bb54920bea6adde242d87e6 +0 -0
  28. data/test/test_git.git/objects/88/d4480861346093048e08ce8dcc577d8aa69379 +1 -0
  29. data/test/test_git.git/objects/90/2dce0535b19f0c15ac8407fc4468256ad672d7 +0 -0
  30. data/test/test_git.git/objects/a6/394b3e8a82b76b0dd5b6b317f489dfe22426a6 +0 -0
  31. data/test/test_git.git/objects/a6/5140d5ec9f47064f614ecf8e43776baa5c0c11 +0 -0
  32. data/test/test_git.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 +0 -0
  33. data/test/test_git.git/objects/ea/41dba10b54a794284e0be009a11f0ff3716a28 +0 -0
  34. data/test/test_git.git/objects/f5/5f3c44c89e5d215fbaaef9d33563117fe0b61b +1 -0
  35. data/test/test_git.git/objects/ff/1f1d4bd0c99e1c9cca047c46b2194accf89504 +4 -0
  36. data/test/test_git.git/refs/heads/master +1 -0
  37. data/test/test_git.rb +44 -0
  38. 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/scottbnel/dandelion.git
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] [config_file]
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
@@ -1,3 +1,10 @@
1
1
  require 'bundler'
2
+ require 'rake/testtask'
2
3
 
3
- Bundler::GemHelper.install_tasks
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ Rake::TestTask.new(:test) do |test|
7
+ test.libs << 'lib' << 'test'
8
+ test.pattern = 'test/**/test_*.rb'
9
+ test.verbose = true
10
+ end
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/scottbnel/dandelion'
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
- @optparse = OptionParser.new do |opts|
17
- opts.banner = 'Usage: dandelion [options] [config_file]'
18
-
19
- @options[:force] = false
20
- opts.on('-f', '--force', 'Force deployment') do
21
- @options[:force] = true
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
- @options[:status] = false
25
- opts.on('-s', '--status', 'Display revision status') do
26
- @options[:status] = true;
27
- end
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 parse!(args)
42
- @args = args
43
- @optparse.parse!(@args)
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 config_file
47
- if @args[0]
48
- @args[0].strip
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 [](key)
55
- @options[key]
56
- end
57
-
58
- def []=(key, value)
59
- @options[key] = value
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!(args)
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 check_files!
80
- unless File.exists? '.git'
81
- log.fatal('Not a git repository: .git')
82
- exit
83
- end
84
- unless File.exists? @options.config_file
85
- log.fatal("Could not find file: #{@options.config_file}")
86
- exit
87
- end
88
- end
89
-
90
- def service(config)
91
- if config['scheme'] == 'sftp'
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
- def execute!
99
- check_files!
100
- config = YAML.load_file @options.config_file
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] and !@options[:status]
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
- remote_revision = deployment.remote_revision || '---'
136
- local_revision = deployment.local_revision
137
-
138
- log.info("Remote revision: #{remote_revision}")
139
- log.info("Local revision: #{local_revision}")
140
-
141
- if @options[:status]
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
@@ -30,8 +30,11 @@ module Dandelion
30
30
  end
31
31
 
32
32
  def validate_state(remote = nil)
33
- if remote and @repo.git.native(:remote, {:raise => true}, 'show', remote) =~ /fast-forward/i
34
- raise FastForwardError
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
 
@@ -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
- tmp = Tempfile.new(file.gsub('/', '.'))
48
- tmp << data
49
- tmp.flush
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)