dandelion 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -13,26 +13,42 @@ Alternatively, you can build the gem yourself:
13
13
  $ cd dandelion
14
14
  $ rake install
15
15
 
16
- Usage
17
- -----
18
- Deployment options are specified in a YAML file. By default, Dandelion looks for
19
- one named `dandelion.yml`, however, this can be overridden by passing a path as an
20
- argument.
16
+ Config
17
+ ------
18
+ Configuration options are specified in a YAML file (Dandelion looks for a file
19
+ named `dandelion.yml` by default):
21
20
 
21
+ # Required
22
22
  scheme: sftp
23
23
  host: example.com
24
24
  username: user
25
25
  password: pass
26
26
  path: path/to/deployment
27
27
 
28
+ # Optional
28
29
  exclude:
29
- - .gitignore
30
- - dandelion.yml
31
-
32
- To deploy the HEAD revision, ensure you are in the root of the repository and run:
30
+ - .gitignore
31
+ - dandelion.yml
32
+
33
+ Usage
34
+ -----
35
+ From the root directory of a Git repository, run:
33
36
 
34
37
  $ dandelion
35
38
 
36
- If the repository has previously been deployed then only the files that have
37
- changed since the last deployment will be transferred. All files (except those
38
- excluded) will be transferred on first deployment.
39
+ Or:
40
+
41
+ $ dandelion path/to/config.yml
42
+
43
+ This will deploy the local `HEAD` revision to the server specified in the config
44
+ file. Dandelion keeps track of the most recently deployed revision so that only
45
+ files which have changed since the last deployment need to be transferred.
46
+
47
+ For a more complete summary of usage options, run:
48
+
49
+ $ dandelion -h
50
+ Usage: dandelion [options] [config_file]
51
+ -f, --force Force deployment
52
+ -s, --status Display revision status
53
+ -v, --version Display the current version
54
+ -h, --help Display this screen
data/bin/dandelion CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  $:.unshift(File.dirname(__FILE__) + '/../lib') unless $:.include?(File.dirname(__FILE__) + '/../lib')
4
4
 
5
- require 'dandelion'
5
+ require 'dandelion/cli'
6
6
 
7
- Dandelion.run
7
+ Dandelion::Cli::Main.execute(ARGV.dup)
data/lib/dandelion.rb CHANGED
@@ -1,60 +1,18 @@
1
- require 'dandelion/deployment'
2
- require 'dandelion/service'
3
- require 'yaml'
4
-
5
1
  module Dandelion
6
2
  class << self
7
- def run
8
- unless File.exists? '.git'
9
- puts 'Not a git repository: .git'
10
- exit
11
- end
12
-
13
- if ARGV[0]
14
- config_file = ARGV[0].strip
15
- else
16
- config_file = 'dandelion.yml'
17
- end
18
-
19
- unless File.exists? config_file
20
- puts "Could not find file: #{config_file}"
21
- exit
22
- end
23
-
24
- config = YAML.load_file config_file
25
-
26
- if config['scheme'] == 'sftp'
27
- service = Service::SFTP.new(config['host'], config['username'], config['password'], config['path'])
28
- else
29
- puts "Unsupported scheme: #{config['scheme']}"
30
- exit
31
- end
32
-
33
- puts "Connecting to: #{service.uri}"
34
-
35
- begin
36
- begin
37
- # Deploy changes since remote revision
38
- deployment = Deployment::DiffDeployment.new('.', service, config['exclude'])
39
-
40
- puts "Remote revision: #{deployment.remote_revision}"
41
- puts "Local revision: #{deployment.local_revision}"
42
-
43
- deployment.deploy
44
- rescue Service::RemoteRevisionError
45
- # No remote revision, deploy everything
46
- deployment = Deployment::FullDeployment.new('.', service, config['exclude'])
47
-
48
- puts "Remote revision: ---"
49
- puts "Local revision: #{deployment.local_revision}"
50
-
51
- deployment.deploy
52
- end
53
-
54
- puts "Deployment complete"
55
- rescue Git::DiffError
56
- puts "Failed to deploy"
57
- puts "Try merging remote changes before deploying again"
3
+ def logger
4
+ return @log if @log
5
+ @log = Logger.new(STDOUT)
6
+ @log.level = Logger::INFO
7
+ @log.formatter = formatter
8
+ @log
9
+ end
10
+
11
+ private
12
+
13
+ def formatter
14
+ proc do |severity, datetime, progname, msg|
15
+ "#{msg}\n"
58
16
  end
59
17
  end
60
18
  end
@@ -0,0 +1,150 @@
1
+ require 'dandelion'
2
+ require 'dandelion/deployment'
3
+ require 'dandelion/git'
4
+ require 'dandelion/service'
5
+ require 'dandelion/version'
6
+ require 'optparse'
7
+ require 'yaml'
8
+
9
+ module Dandelion
10
+ module Cli
11
+ class UnsupportedSchemeError < StandardError; end
12
+
13
+ class Options
14
+ def initialize
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
22
+ end
23
+
24
+ @options[:status] = false
25
+ opts.on('-s', '--status', 'Display revision status') do
26
+ @options[:status] = true;
27
+ end
28
+
29
+ opts.on('-v', '--version', 'Display the current version') do
30
+ puts "Dandelion v#{Dandelion::VERSION}"
31
+ exit
32
+ end
33
+
34
+ opts.on('-h', '--help', 'Display this screen') do
35
+ puts opts
36
+ exit
37
+ end
38
+ end
39
+ end
40
+
41
+ def parse!(args)
42
+ @args = args
43
+ @optparse.parse!(@args)
44
+ end
45
+
46
+ def config_file
47
+ if @args[0]
48
+ @args[0].strip
49
+ else
50
+ 'dandelion.yml'
51
+ end
52
+ end
53
+
54
+ def [](key)
55
+ @options[key]
56
+ end
57
+
58
+ def []=(key, value)
59
+ @options[key] = value
60
+ end
61
+ end
62
+
63
+ class Main
64
+ class << self
65
+ def execute(args)
66
+ new(args).execute!
67
+ end
68
+ end
69
+
70
+ def initialize(args)
71
+ @options = Options.new
72
+ @options.parse!(args)
73
+ end
74
+
75
+ def log
76
+ Dandelion.logger
77
+ end
78
+
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
95
+ end
96
+ end
97
+
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
+
112
+ begin
113
+ deployment = Deployment::DiffDeployment.new(repo, service, config['exclude'])
114
+ rescue Deployment::RemoteRevisionError
115
+ deployment = Deployment::FullDeployment.new(repo, service, config['exclude'])
116
+ rescue Git::DiffError
117
+ log.fatal('Error: could not generate diff')
118
+ log.fatal('Try merging remote changes before running dandelion again')
119
+ exit
120
+ end
121
+
122
+ begin
123
+ repo.remote_list.each do |remote|
124
+ deployment.validate_state(remote)
125
+ end
126
+ rescue Deployment::FastForwardError
127
+ if !@options[:force] and !@options[:status]
128
+ log.warn('Warning: you are trying to deploy unpushed commits')
129
+ log.warn('This could potentially prevent others from being able to deploy')
130
+ log.warn('If you are sure you want to this, use the -f option to force deployment')
131
+ exit
132
+ end
133
+ 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]
142
+ exit
143
+ end
144
+
145
+ deployment.deploy
146
+ log.info("Deployment complete")
147
+ end
148
+ end
149
+ end
150
+ end
@@ -1,100 +1,126 @@
1
1
  require 'dandelion/git'
2
2
 
3
- module Deployment
4
- class Deployment
5
- def initialize(dir, service, exclude = nil, revision = 'HEAD')
6
- @service = service
7
- @exclude = exclude || []
8
- @tree = Git::Tree.new(dir, revision)
9
- end
3
+ module Dandelion
4
+ module Deployment
5
+ class RemoteRevisionError < StandardError; end
6
+ class FastForwardError < StandardError; end
7
+
8
+ class Deployment
9
+ def initialize(repo, service, exclude = nil, revision = 'HEAD')
10
+ @repo = repo
11
+ @service = service
12
+ @exclude = exclude || []
13
+ @tree = Git::Tree.new(@repo, revision)
14
+ end
10
15
 
11
- def local_revision
12
- @tree.revision
13
- end
16
+ def local_revision
17
+ @tree.revision
18
+ end
14
19
 
15
- def remote_uri
16
- @service.uri
17
- end
20
+ def remote_revision
21
+ nil
22
+ end
18
23
 
19
- def write_revision
20
- @service.write('.revision', local_revision)
21
- end
24
+ def remote_uri
25
+ @service.uri
26
+ end
22
27
 
23
- protected
28
+ def write_revision
29
+ @service.write('.revision', local_revision)
30
+ end
31
+
32
+ def validate_state(remote = nil)
33
+ if remote and @repo.git.native(:remote, {:raise => true}, 'show', remote) =~ /fast-forward/i
34
+ raise FastForwardError
35
+ end
36
+ end
37
+
38
+ def log
39
+ Dandelion.logger
40
+ end
24
41
 
25
- def exclude_file?(file)
26
- return @exclude.map { |e| file.start_with?(e) }.any?
27
- end
28
- end
29
-
30
- class DiffDeployment < Deployment
31
- def initialize(dir, service, exclude = nil, revision = 'HEAD')
32
- super(dir, service, exclude, revision)
33
- @diff = Git::Diff.new(dir, read_revision)
34
- end
42
+ protected
35
43
 
36
- def remote_revision
37
- @diff.revision
44
+ def exclude_file?(file)
45
+ return @exclude.map { |e| file.start_with?(e) }.any?
46
+ end
38
47
  end
48
+
49
+ class DiffDeployment < Deployment
50
+ def initialize(repo, service, exclude = nil, revision = 'HEAD')
51
+ super(repo, service, exclude, revision)
52
+ @diff = Git::Diff.new(@repo, read_remote_revision, revision)
53
+ end
39
54
 
40
- def deploy
41
- if !revisions_match? && any?
42
- deploy_changed
43
- deploy_deleted
44
- else
45
- puts "Nothing to deploy"
46
- end
47
- unless revisions_match?
48
- write_revision
55
+ def remote_revision
56
+ @diff.from_revision
49
57
  end
50
- end
51
58
 
52
- def deploy_changed
53
- @diff.changed.each do |file|
54
- if exclude_file?(file)
55
- puts "Skipping file: #{file}"
59
+ def deploy
60
+ if !revisions_match? && any?
61
+ deploy_changed
62
+ deploy_deleted
56
63
  else
57
- puts "Uploading file: #{file}"
58
- @service.write(file, @tree.show(file))
64
+ log.info("Nothing to deploy")
65
+ end
66
+ unless revisions_match?
67
+ write_revision
59
68
  end
60
69
  end
61
- end
62
70
 
63
- def deploy_deleted
64
- @diff.deleted.each do |file|
65
- if exclude_file?(file)
66
- puts "Skipping file: #{file}"
67
- else
68
- puts "Deleting file: #{file}"
69
- @service.delete(file)
71
+ def deploy_changed
72
+ @diff.changed.each do |file|
73
+ if exclude_file?(file)
74
+ log.info("Skipping file: #{file}")
75
+ else
76
+ log.info("Uploading file: #{file}")
77
+ @service.write(file, @tree.show(file))
78
+ end
70
79
  end
71
80
  end
72
- end
73
81
 
74
- def any?
75
- @diff.changed.any? || @diff.deleted.any?
76
- end
82
+ def deploy_deleted
83
+ @diff.deleted.each do |file|
84
+ if exclude_file?(file)
85
+ log.info("Skipping file: #{file}")
86
+ else
87
+ log.info("Deleting file: #{file}")
88
+ @service.delete(file)
89
+ end
90
+ end
91
+ end
77
92
 
78
- def revisions_match?
79
- remote_revision == local_revision
80
- end
93
+ def any?
94
+ @diff.changed.any? || @diff.deleted.any?
95
+ end
96
+
97
+ def revisions_match?
98
+ remote_revision == local_revision
99
+ end
81
100
 
82
- private
101
+ private
83
102
 
84
- def read_revision
85
- @service.read('.revision').chomp
103
+ def read_remote_revision
104
+ begin
105
+ @service.read('.revision').chomp
106
+ rescue Service::MissingFileError
107
+ raise RemoteRevisionError
108
+ end
109
+ end
86
110
  end
87
- end
88
111
 
89
- class FullDeployment < Deployment
90
- def deploy
91
- @tree.files.each do |file|
92
- unless exclude_file?(file)
93
- puts "Uploading file: #{file}"
94
- @service.write(file, @tree.show(file))
112
+ class FullDeployment < Deployment
113
+ def deploy
114
+ @tree.files.each do |file|
115
+ if exclude_file?(file)
116
+ log.info("Skipping file: #{file}")
117
+ else
118
+ log.info("Uploading file: #{file}")
119
+ @service.write(file, @tree.show(file))
120
+ end
95
121
  end
122
+ write_revision
96
123
  end
97
- write_revision
98
124
  end
99
125
  end
100
126
  end
data/lib/dandelion/git.rb CHANGED
@@ -1,60 +1,69 @@
1
1
  require 'grit'
2
2
 
3
- module Git
4
- class DiffError < StandardError; end
3
+ module Dandelion
4
+ module Git
5
+ class DiffError < StandardError; end
5
6
 
6
- class Diff
7
- attr_reader :revision
8
-
9
- def initialize(dir, revision)
10
- @revision = revision
11
- @raw = `cd #{dir}; git diff --name-status #{@revision} HEAD`
12
- check_state!
13
- end
14
-
15
- def changed
16
- files_flagged ['A', 'C', 'M']
17
- end
18
-
19
- def deleted
20
- files_flagged ['D']
7
+ class Repo < Grit::Repo
8
+ def initialize(dir)
9
+ super(dir)
10
+ end
21
11
  end
12
+
13
+ class Diff
14
+ attr_reader :from_revision, :to_revision
15
+
16
+ @files = nil
17
+
18
+ def initialize(repo, from_revision, to_revision)
19
+ @repo = repo
20
+ @from_revision = from_revision
21
+ @to_revision = to_revision
22
+ begin
23
+ @files = parse_diff @repo.git.native(:diff, {:name_status => true, :raise => true}, from_revision, to_revision)
24
+ rescue Grit::Git::CommandFailed
25
+ raise DiffError
26
+ end
27
+ end
22
28
 
23
- private
29
+ def changed
30
+ @files.select { |file, status| ['A', 'C', 'M'].include?(status) }.keys
31
+ end
24
32
 
25
- def files_flagged(statuses)
26
- items = []
27
- @raw.split("\n").each do |line|
28
- status, file = line.split("\t")
29
- items << file if statuses.include? status
33
+ def deleted
34
+ @files.select { |file, status| 'D' == status }.keys
30
35
  end
31
- items
32
- end
36
+
37
+ private
33
38
 
34
- def check_state!
35
- if $?.exitstatus != 0
36
- raise DiffError
39
+ def parse_diff(diff)
40
+ files = {}
41
+ diff.split("\n").each do |line|
42
+ status, file = line.split("\t")
43
+ files[file] = status
44
+ end
45
+ files
37
46
  end
38
47
  end
39
- end
40
48
 
41
- class Tree
42
- def initialize(dir, revision)
43
- @dir = dir
44
- @commit = Grit::Repo.new(dir).commit(revision)
45
- @tree = @commit.tree
46
- end
49
+ class Tree
50
+ def initialize(repo, revision)
51
+ @repo = repo
52
+ @commit = @repo.commit(revision)
53
+ @tree = @commit.tree
54
+ end
47
55
 
48
- def files
49
- `cd #{@dir}; git ls-tree --name-only -r #{revision}`.split("\n")
50
- end
56
+ def files
57
+ @repo.git.native(:ls_tree, {:name_only => true}, revision).split("\n")
58
+ end
51
59
 
52
- def show(file)
53
- (@tree / file).data
54
- end
60
+ def show(file)
61
+ (@tree / file).data
62
+ end
55
63
 
56
- def revision
57
- @commit.sha
64
+ def revision
65
+ @commit.sha
66
+ end
58
67
  end
59
68
  end
60
69
  end
@@ -1,89 +1,91 @@
1
1
  require 'net/sftp'
2
2
  require 'tempfile'
3
3
 
4
- module Service
5
- class RemoteRevisionError < StandardError; end
4
+ module Dandelion
5
+ module Service
6
+ class MissingFileError < StandardError; end
6
7
 
7
- class Service
8
- def initialize(host, username, path)
9
- @host = host
10
- @username = username
11
- @path = path
12
- end
8
+ class Service
9
+ def initialize(host, username, path)
10
+ @host = host
11
+ @username = username
12
+ @path = path
13
+ end
13
14
 
14
- def uri
15
- "#{@scheme}://#{@username}@#{@host}/#{@path}"
15
+ def uri
16
+ "#{@scheme}://#{@username}@#{@host}/#{@path}"
17
+ end
16
18
  end
17
- end
18
19
 
19
- class SFTP < Service
20
- def initialize(host, username, password, path)
21
- super(host, username, path)
22
- @scheme = 'sftp'
23
- @sftp = Net::SFTP.start(host, username, :password => password)
24
- end
20
+ class SFTP < Service
21
+ def initialize(host, username, password, path)
22
+ super(host, username, path)
23
+ @scheme = 'sftp'
24
+ @sftp = Net::SFTP.start(host, username, :password => password)
25
+ end
25
26
 
26
- def read(file)
27
- begin
28
- @sftp.file.open(File.join(@path, file), 'r') do |f|
29
- f.gets
27
+ def read(file)
28
+ begin
29
+ @sftp.file.open(File.join(@path, file), 'r') do |f|
30
+ f.gets
31
+ end
32
+ rescue Net::SFTP::StatusException => e
33
+ raise unless e.code == 2
34
+ raise MissingFileError
30
35
  end
31
- rescue Net::SFTP::StatusException => e
32
- raise unless e.code == 2
33
- raise RemoteRevisionError
34
36
  end
35
- end
36
37
 
37
- def write(file, data)
38
- path = File.join(@path, file)
39
- begin
40
- dir = File.dirname(path)
41
- @sftp.stat!(dir)
42
- rescue Net::SFTP::StatusException => e
43
- raise unless e.code == 2
44
- mkdir_p(dir)
38
+ def write(file, data)
39
+ path = File.join(@path, file)
40
+ begin
41
+ dir = File.dirname(path)
42
+ @sftp.stat!(dir)
43
+ rescue Net::SFTP::StatusException => e
44
+ raise unless e.code == 2
45
+ mkdir_p(dir)
46
+ end
47
+ tmp = Tempfile.new(file.gsub('/', '.'))
48
+ tmp << data
49
+ tmp.flush
50
+ @sftp.upload!(tmp.path, path)
51
+ tmp.close
45
52
  end
46
- tmp = Tempfile.new(file.gsub('/', '.'))
47
- tmp << data
48
- tmp.flush
49
- @sftp.upload!(tmp.path, path)
50
- tmp.close
51
- end
52
53
 
53
- def delete(file)
54
- begin
55
- path = File.join(@path, file)
56
- @sftp.remove!(path)
57
- cleanup(File.dirname(path))
58
- rescue Net::SFTP::StatusException => e
59
- raise unless e.code == 2
54
+ def delete(file)
55
+ begin
56
+ path = File.join(@path, file)
57
+ @sftp.remove!(path)
58
+ cleanup(File.dirname(path))
59
+ rescue Net::SFTP::StatusException => e
60
+ raise unless e.code == 2
61
+ end
60
62
  end
61
- end
62
63
 
63
- private
64
+ private
64
65
 
65
- def cleanup(dir)
66
- unless File.identical?(dir, @path)
67
- if empty?(dir)
68
- @sftp.rmdir!(dir)
69
- cleanup(File.dirname(dir))
66
+ def cleanup(dir)
67
+ unless File.identical?(dir, @path)
68
+ if empty?(dir)
69
+ @sftp.rmdir!(dir)
70
+ cleanup(File.dirname(dir))
71
+ end
70
72
  end
71
73
  end
72
- end
73
74
 
74
- def empty?(dir)
75
- @sftp.dir.entries(dir).map do |entry|
76
- entry.name unless entry.name == '.' or entry.name == '..'
77
- end.compact.empty?
78
- end
75
+ def empty?(dir)
76
+ @sftp.dir.entries(dir).map do |entry|
77
+ entry.name unless entry.name == '.' or entry.name == '..'
78
+ end.compact.empty?
79
+ end
79
80
 
80
- def mkdir_p(dir)
81
- begin
82
- @sftp.mkdir!(dir)
83
- rescue Net::SFTP::StatusException => e
84
- raise unless e.code == 2
85
- mkdir_p(File.dirname(dir))
86
- mkdir_p(dir)
81
+ def mkdir_p(dir)
82
+ begin
83
+ @sftp.mkdir!(dir)
84
+ rescue Net::SFTP::StatusException => e
85
+ raise unless e.code == 2
86
+ mkdir_p(File.dirname(dir))
87
+ mkdir_p(dir)
88
+ end
87
89
  end
88
90
  end
89
91
  end
@@ -1,3 +1,3 @@
1
1
  module Dandelion
2
- VERSION = '0.1.4'
2
+ VERSION = '0.1.5'
3
3
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: dandelion
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.1.4
5
+ version: 0.1.5
6
6
  platform: ruby
7
7
  authors:
8
8
  - Scott Nelson
@@ -52,6 +52,7 @@ files:
52
52
  - bin/dandelion
53
53
  - dandelion.gemspec
54
54
  - lib/dandelion.rb
55
+ - lib/dandelion/cli.rb
55
56
  - lib/dandelion/deployment.rb
56
57
  - lib/dandelion/git.rb
57
58
  - lib/dandelion/service.rb
@@ -83,6 +84,6 @@ rubyforge_project:
83
84
  rubygems_version: 1.5.0
84
85
  signing_key:
85
86
  specification_version: 3
86
- summary: dandelion-0.1.4
87
+ summary: dandelion-0.1.5
87
88
  test_files: []
88
89