dandelion 0.1.4 → 0.1.5

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 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