git-deploy 0.4.1 → 0.5.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.markdown +82 -75
- data/bin/git-deploy +3 -0
- data/lib/git_deploy/configuration.rb +83 -0
- data/lib/git_deploy/generator.rb +28 -0
- data/lib/git_deploy/ssh_methods.rb +99 -0
- data/lib/git_deploy/templates/after_push.sh +17 -0
- data/lib/git_deploy/templates/before_restart.rb +39 -0
- data/lib/git_deploy/templates/restart.sh +3 -0
- data/lib/git_deploy.rb +67 -103
- data/lib/hooks/post-receive.sh +52 -0
- data/spec/configuration_spec.rb +48 -0
- metadata +81 -60
- data/lib/hooks/post-receive.rb +0 -57
- data/lib/hooks/post-reset.rb +0 -116
data/README.markdown
CHANGED
|
@@ -1,109 +1,116 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
Easy git deployment
|
|
2
|
+
===================
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
Straightforward, [Heroku][]-style, push-based deployment. Your deploys can become as simple as this:
|
|
5
5
|
|
|
6
|
-
$ git push
|
|
6
|
+
$ git push production master
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
To get started, install the "git-deploy" gem.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
gem install git-deploy
|
|
11
11
|
|
|
12
|
+
Only the person who is setting up deployment for the first time needs to install
|
|
13
|
+
the gem. You don't have to add it to your project's Gemfile.
|
|
12
14
|
|
|
13
|
-
Considerations
|
|
14
|
-
--------------
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Which app languages/frameworks are supported?
|
|
17
|
+
---------------------------------------------
|
|
17
18
|
|
|
19
|
+
Regardless of the fact that this tool is mostly written in Ruby, git-deploy can be useful for any kind of code that needs deploying on a remote server. The default scripts are suited for Ruby web apps, but can be edited to accommodate other frameworks.
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
-----------
|
|
21
|
+
Your deployment is customized with per-project callback scripts which can be written in any language.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
$ git remote add origin user@example.com:/path/to/myapp
|
|
25
|
-
|
|
26
|
-
The "/path/to/myapp" is the directory where your code will reside. It doesn't have to exist; it will be created for you during this setup.
|
|
23
|
+
The assumption is that you're deploying to a single host to which you connect over SSH using public/private key authentication.
|
|
27
24
|
|
|
28
|
-
2. Create/overwrite the following files in your project:
|
|
29
|
-
|
|
30
|
-
**config/deploy.rb** (entire file):
|
|
31
|
-
|
|
32
|
-
# set to the name of git remote you intend to deploy to
|
|
33
|
-
set :remote, "origin"
|
|
34
|
-
# specify the deployment branch
|
|
35
|
-
set :branch, "master"
|
|
36
|
-
# sudo will only be used to create the deployment directory
|
|
37
|
-
set :use_sudo, true
|
|
38
|
-
# the remote host is read automatically from your git remote specification
|
|
39
|
-
server remote_host, :app, :web, :db, :primary => true
|
|
40
|
-
|
|
41
|
-
**Capfile**:
|
|
42
|
-
|
|
43
|
-
require 'git_deploy'
|
|
44
|
-
load 'config/deploy'
|
|
45
|
-
|
|
46
|
-
Test it by running `cap -T`. You should see several deploy tasks listed.
|
|
47
25
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
$ cap deploy:setup
|
|
51
|
-
|
|
52
|
-
This will initialize a git repository in the target directory, install the push hook and push the branch you specified to the server.
|
|
26
|
+
Initial setup
|
|
27
|
+
-------------
|
|
53
28
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
29
|
+
1. Create a git remote for where you'll push the code on your server. The name of this remote in the examples is "production", but it can be whatever you wish ("online", "website", or other).
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
git remote add production "user@example.com:/apps/mynewapp"
|
|
33
|
+
```
|
|
58
34
|
|
|
35
|
+
`/apps/mynewapp` is the directory where you want your code to reside on the
|
|
36
|
+
remote server. If the directory doesn't exist, the next step creates it.
|
|
59
37
|
|
|
60
|
-
|
|
61
|
-
----------
|
|
38
|
+
2. Run the setup task:
|
|
62
39
|
|
|
63
|
-
|
|
40
|
+
```sh
|
|
41
|
+
git deploy setup -r "production"
|
|
42
|
+
```
|
|
64
43
|
|
|
65
|
-
|
|
44
|
+
This will initialize the remote git repository in the deploy directory
|
|
45
|
+
(`/apps/mynewapp` in the above example) and install the remote git hook.
|
|
66
46
|
|
|
67
|
-
|
|
47
|
+
3. Run the init task:
|
|
68
48
|
|
|
69
|
-
|
|
49
|
+
```sh
|
|
50
|
+
git deploy init
|
|
51
|
+
```
|
|
70
52
|
|
|
71
|
-
|
|
53
|
+
This generates default deploy callback scripts in the `deploy/` directory.
|
|
54
|
+
You should check them in git because they are going to be executed on the
|
|
55
|
+
server during each deploy.
|
|
72
56
|
|
|
73
|
-
|
|
57
|
+
4. Push the code.
|
|
74
58
|
|
|
75
|
-
|
|
76
|
-
|
|
59
|
+
```sh
|
|
60
|
+
git push production master
|
|
61
|
+
```
|
|
77
62
|
|
|
78
|
-
|
|
63
|
+
3. Login to your server and manually perform necessary one-time administrative operations. This might include:
|
|
64
|
+
* set up the Apache/nginx virtual host for this application;
|
|
65
|
+
* check your `config/database.yml` and create the production database.
|
|
79
66
|
|
|
80
|
-
1. clears cached css and javascript assets if any versioned files under "public/stylesheets" and "public/javascripts" have changed, respectively;
|
|
81
|
-
2. runs "rake db:migrate" if new migrations have been added;
|
|
82
|
-
3. sync submodule urls if ".gitmodules" file has changed;
|
|
83
|
-
4. initialize and update submodules;
|
|
84
|
-
5. touches "tmp/restart.txt" if app restart is needed.
|
|
85
67
|
|
|
86
|
-
|
|
68
|
+
Everyday deployments
|
|
69
|
+
--------------------
|
|
87
70
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
3. one or more files/submodules under "app", "config", "lib", "public", or "vendor" changed.
|
|
71
|
+
If you've set your app correctly, visiting <http://example.com> in your browser
|
|
72
|
+
should show it up and running.
|
|
91
73
|
|
|
92
|
-
|
|
74
|
+
Now, subsequent deployments are done simply **by pushing to the branch that is
|
|
75
|
+
currently checked out on the remote**:
|
|
93
76
|
|
|
94
|
-
|
|
77
|
+
git push production master
|
|
95
78
|
|
|
79
|
+
Because the deployments are performed with git, nobody else on the team needs to
|
|
80
|
+
install the "git-deploy" gem.
|
|
96
81
|
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
On every deploy, the default `deploy/after_push` script performs the following:
|
|
83
|
+
|
|
84
|
+
1. updates git submodules (if there are any);
|
|
85
|
+
2. runs `bundle install --deployment` if there is a Gemfile;
|
|
86
|
+
3. runs `rake db:migrate` if new migrations have been added;
|
|
87
|
+
4. clears cached CSS/JS assets in "public/stylesheets" and "public/javascripts";
|
|
88
|
+
5. restarts the web application.
|
|
89
|
+
|
|
90
|
+
You can customize all this by editing generated scripts in the `deploy/`
|
|
91
|
+
directory of your app.
|
|
92
|
+
|
|
93
|
+
Deployments are logged to `log/deploy.log` in your application's directory.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
How it works
|
|
97
|
+
------------
|
|
98
|
+
|
|
99
|
+
The `git deploy setup` command installed a `post-receive` git hook in the remote
|
|
100
|
+
repository. This is how your code on the server is kept up to date. This script
|
|
101
|
+
checks out the latest version of your project from the current branch and
|
|
102
|
+
runs the following callback scripts:
|
|
103
|
+
|
|
104
|
+
* `deploy/after_push` - on subsequent pushes. It in turn executes:
|
|
105
|
+
* `deploy/before_restart`
|
|
106
|
+
* `deploy/restart`
|
|
107
|
+
* `deploy/after_restart`
|
|
99
108
|
|
|
100
|
-
|
|
109
|
+
All of the callbacks are optional. These scripts are ordinary Unix executables.
|
|
110
|
+
The ones which get generated for you by `git deploy init` are written in shell
|
|
111
|
+
script and Ruby.
|
|
101
112
|
|
|
102
|
-
|
|
103
|
-
* Better configurability
|
|
104
|
-
* Steps forward to supporting more existing 3rd-party Capistrano tasks, like that of the EngineYard gem
|
|
105
|
-
* Support for multiple environments on the same server (production, staging, continuous integration, etc.) sharing the same git repo, so you don't have to push same objects twice
|
|
106
|
-
* Automatic submodule conflict resolving
|
|
113
|
+
It's worth remembering that "after_push" is done **asynchronously from your git push**. This is because migrating the database and updating submodules might take a long time and you don't want to wait for all that during a git push. But, this means that when the push is done, the server has *not yet restarted*. You might need to wait a few seconds or a minute.
|
|
107
114
|
|
|
108
115
|
|
|
109
|
-
[heroku]: http://heroku.com/
|
|
116
|
+
[heroku]: http://heroku.com/
|
data/bin/git-deploy
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'cgi'
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
|
|
5
|
+
class GitDeploy
|
|
6
|
+
module Configuration
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
extend Forwardable
|
|
10
|
+
def_delegator :remote_url, :host
|
|
11
|
+
def_delegator :remote_url, :port, :remote_port
|
|
12
|
+
def_delegator :remote_url, :path, :deploy_to
|
|
13
|
+
|
|
14
|
+
def remote_user
|
|
15
|
+
@user ||= begin
|
|
16
|
+
user = remote_url.user
|
|
17
|
+
user ? CGI.unescape(user) : `whoami`.chomp
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def branch
|
|
22
|
+
'master'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def git_config
|
|
26
|
+
@git_config ||= Hash.new do |cache, cmd|
|
|
27
|
+
git = ENV['GIT'] || 'git'
|
|
28
|
+
out = `#{git} #{cmd}`
|
|
29
|
+
if $?.success? then cache[cmd] = out.chomp
|
|
30
|
+
else cache[cmd] = nil
|
|
31
|
+
end
|
|
32
|
+
cache[cmd]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def remote_urls(remote)
|
|
37
|
+
git_config["remote -v"].to_s.split("\n").
|
|
38
|
+
select {|l| l =~ /^#{remote}\t/ }.
|
|
39
|
+
map {|l| l.split("\t")[1].sub(/\(.+?\)$/, '') }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def remote_url(remote = options[:remote])
|
|
43
|
+
@remote_url ||= {}
|
|
44
|
+
@remote_url[remote] ||= begin
|
|
45
|
+
url = remote_urls(remote).first
|
|
46
|
+
if url.nil?
|
|
47
|
+
abort "Error: Remote url not found for remote #{remote.inspect}"
|
|
48
|
+
elsif url =~ /(^|@)github\.com\b/
|
|
49
|
+
abort "Error: Remote url for #{remote.inspect} points to GitHub. Can't deploy there!"
|
|
50
|
+
else
|
|
51
|
+
url = 'ssh://' + url.sub(%r{:/?}, '/') unless url =~ %r{^[\w-]+://}
|
|
52
|
+
begin
|
|
53
|
+
url = URI.parse url
|
|
54
|
+
rescue
|
|
55
|
+
abort "Error parsing remote url #{url}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
url
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def current_branch
|
|
63
|
+
git_config['symbolic-ref -q HEAD']
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def tracked_branch
|
|
67
|
+
branch = current_branch && tracked_for(current_branch)
|
|
68
|
+
normalize_branch(branch) if branch
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def normalize_branch(branch)
|
|
72
|
+
branch.sub('refs/heads/', '')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def remote_for(branch)
|
|
76
|
+
git_config['config branch.%s.remote' % normalize_branch(branch)]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def tracked_for(branch)
|
|
80
|
+
git_config['config branch.%s.merge' % normalize_branch(branch)]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'thor/group'
|
|
2
|
+
|
|
3
|
+
class GitDeploy::Generator < Thor::Group
|
|
4
|
+
include Thor::Actions
|
|
5
|
+
|
|
6
|
+
def self.source_root
|
|
7
|
+
File.expand_path('../templates', __FILE__)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def copy_main_hook
|
|
11
|
+
copy_hook 'after_push.sh', 'deploy/after_push'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def copy_restart_hook
|
|
15
|
+
copy_hook 'restart.sh', 'deploy/restart'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def copy_restart_callbacks
|
|
19
|
+
copy_hook 'before_restart.rb', 'deploy/before_restart'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def copy_hook(template, destination)
|
|
25
|
+
copy_file template, destination
|
|
26
|
+
chmod destination, 0744 unless File.executable? destination
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
class GitDeploy
|
|
2
|
+
module SSHMethods
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
def sudo_cmd
|
|
6
|
+
"sudo -p 'sudo password: '"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def system(*args)
|
|
10
|
+
puts "[local] $ " + args.join(' ').gsub(' && ', " && \\\n ")
|
|
11
|
+
super unless options.noop?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run(cmd = nil)
|
|
15
|
+
cmd = yield(cmd) if block_given?
|
|
16
|
+
cmd = cmd.join(' && ') if Array === cmd
|
|
17
|
+
puts "[#{options[:remote]}] $ " + cmd.gsub(' && ', " && \\\n ")
|
|
18
|
+
|
|
19
|
+
unless options.noop?
|
|
20
|
+
status, output = ssh_exec cmd do |ch, stream, data|
|
|
21
|
+
case stream
|
|
22
|
+
when :stdout then $stdout.print data
|
|
23
|
+
when :stderr then $stderr.print data
|
|
24
|
+
end
|
|
25
|
+
ch.send_data(askpass) if data =~ /^sudo password: /
|
|
26
|
+
end
|
|
27
|
+
output
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run_test(cmd)
|
|
32
|
+
status, output = ssh_exec(cmd) { }
|
|
33
|
+
status == 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ssh_exec(cmd, &block)
|
|
37
|
+
status = nil
|
|
38
|
+
output = ''
|
|
39
|
+
|
|
40
|
+
channel = ssh_connection.open_channel do |chan|
|
|
41
|
+
chan.exec(cmd) do |ch, success|
|
|
42
|
+
raise "command failed: #{cmd.inspect}" unless success
|
|
43
|
+
# ch.request_pty
|
|
44
|
+
|
|
45
|
+
ch.on_data do |c, data|
|
|
46
|
+
output << data
|
|
47
|
+
yield(c, :stdout, data)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
ch.on_extended_data do |c, type, data|
|
|
51
|
+
output << data
|
|
52
|
+
yield(c, :stderr, data)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
ch.on_request "exit-status" do |ch, data|
|
|
56
|
+
status = data.read_long
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
channel.wait
|
|
62
|
+
[status, output]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# TODO: use Highline for cross-platform support
|
|
66
|
+
def askpass
|
|
67
|
+
tty_state = `stty -g`
|
|
68
|
+
system 'stty raw -echo -icanon isig' if $?.success?
|
|
69
|
+
pass = ''
|
|
70
|
+
while char = $stdin.getbyte and not (char == 13 or char == 10)
|
|
71
|
+
if char == 127 or char == 8
|
|
72
|
+
pass[-1,1] = '' unless pass.empty?
|
|
73
|
+
else
|
|
74
|
+
pass << char.chr
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
pass
|
|
78
|
+
ensure
|
|
79
|
+
system "stty #{tty_state}" unless tty_state.empty?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def scp_upload(files)
|
|
83
|
+
channels = []
|
|
84
|
+
files.each do |local, remote|
|
|
85
|
+
puts "FILE: [local] #{local.sub(LOCAL_DIR + '/', '')} -> [#{options[:remote]}] #{remote}"
|
|
86
|
+
channels << ssh_connection.scp.upload(local, remote) unless options.noop?
|
|
87
|
+
end
|
|
88
|
+
channels.each { |c| c.wait }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def ssh_connection
|
|
92
|
+
@ssh ||= begin
|
|
93
|
+
ssh = Net::SSH.start(host, remote_user, :port => remote_port)
|
|
94
|
+
at_exit { ssh.close }
|
|
95
|
+
ssh
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
oldrev=$1
|
|
4
|
+
newrev=$2
|
|
5
|
+
|
|
6
|
+
run() {
|
|
7
|
+
[ -x $1 ] && $1 $oldrev $newrev
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
echo files changed: $(git diff $oldrev $newrev --diff-filter=ACDMR --name-only | wc -l)
|
|
11
|
+
|
|
12
|
+
umask 002
|
|
13
|
+
|
|
14
|
+
git submodule sync && git submodule update --init --recursive
|
|
15
|
+
|
|
16
|
+
run deploy/before_restart
|
|
17
|
+
run deploy/restart && run deploy/after_restart
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
oldrev, newrev = ARGV
|
|
3
|
+
|
|
4
|
+
def run(cmd)
|
|
5
|
+
exit($?.exitstatus) unless system "umask 002 && #{cmd}"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
RAILS_ENV = ENV['RAILS_ENV'] || 'production'
|
|
9
|
+
use_bundler = File.file? 'Gemfile'
|
|
10
|
+
rake_cmd = use_bundler ? 'bundle exec rake' : 'rake'
|
|
11
|
+
|
|
12
|
+
if use_bundler
|
|
13
|
+
bundler_args = ['--deployment']
|
|
14
|
+
BUNDLE_WITHOUT = ENV['BUNDLE_WITHOUT'] || 'development:test'
|
|
15
|
+
bundler_args << '--without' << BUNDLE_WITHOUT unless BUNDLE_WITHOUT.empty?
|
|
16
|
+
|
|
17
|
+
# update gem bundle
|
|
18
|
+
run "bundle install #{bundler_args.join(' ')}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if File.file? 'Rakefile'
|
|
22
|
+
tasks = []
|
|
23
|
+
|
|
24
|
+
num_migrations = `git diff #{oldrev} #{newrev} --diff-filter=A --name-only -z db/migrate`.split("\0").size
|
|
25
|
+
# run migrations if new ones have been added
|
|
26
|
+
tasks << "db:migrate" if num_migrations > 0
|
|
27
|
+
|
|
28
|
+
# precompile assets
|
|
29
|
+
changed_assets = `git diff #{oldrev} #{newrev} --name-only -z app/assets`.split("\0")
|
|
30
|
+
tasks << "assets:precompile" if changed_assets.size > 0
|
|
31
|
+
|
|
32
|
+
run "#{rake_cmd} #{tasks.join(' ')} RAILS_ENV=#{RAILS_ENV}" if tasks.any?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# clear cached assets (unversioned/ignored files)
|
|
36
|
+
run "git clean -x -f -- public/stylesheets public/javascripts"
|
|
37
|
+
|
|
38
|
+
# clean unversioned files from vendor/plugins (e.g. old submodules)
|
|
39
|
+
run "git clean -d -f -- vendor/plugins"
|
data/lib/git_deploy.rb
CHANGED
|
@@ -1,121 +1,85 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'thor'
|
|
2
|
+
require 'net/ssh'
|
|
3
|
+
require 'net/scp'
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
unless exists?(name)
|
|
6
|
-
set(name, *args, &block)
|
|
7
|
-
end
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
_cset(:application) { abort "Please specify the name of your application, set :application, 'foo'" }
|
|
11
|
-
_cset :remote, "origin"
|
|
12
|
-
_cset :branch, "master"
|
|
5
|
+
class GitDeploy < Thor
|
|
6
|
+
LOCAL_DIR = File.expand_path('..', __FILE__)
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
_cset(:run_method) { fetch(:use_sudo, true) ? :sudo : :run }
|
|
19
|
-
_cset :group_writeable, false
|
|
8
|
+
require 'git_deploy/configuration'
|
|
9
|
+
require 'git_deploy/ssh_methods'
|
|
10
|
+
include Configuration
|
|
11
|
+
include SSHMethods
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
_cset(:source) { Capistrano::Deploy::SCM::Git.new(self) }
|
|
13
|
+
class_option :remote, :aliases => '-r', :type => :string, :default => 'origin'
|
|
14
|
+
class_option :noop, :aliases => '-n', :type => :boolean, :default => false
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# which (by default) is unset.
|
|
30
|
-
def try_sudo(*args)
|
|
31
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
32
|
-
command = args.shift
|
|
33
|
-
raise ArgumentError, "too many arguments" if args.any?
|
|
34
|
-
|
|
35
|
-
as = options.fetch(:as, fetch(:admin_runner, nil))
|
|
36
|
-
|
|
37
|
-
if command
|
|
38
|
-
invoke_command(command, :via => run_method, :as => as)
|
|
39
|
-
elsif :sudo == run_method
|
|
40
|
-
sudo(:as => as)
|
|
41
|
-
else
|
|
42
|
-
""
|
|
43
|
-
end
|
|
16
|
+
desc "init", "Generates deployment customization scripts for your app"
|
|
17
|
+
def init
|
|
18
|
+
require 'git_deploy/generator'
|
|
19
|
+
Generator::start([])
|
|
44
20
|
end
|
|
45
21
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
else
|
|
52
|
-
code
|
|
53
|
-
command = ["cd #{deploy_to}"]
|
|
54
|
-
command << ".git/hooks/post-reset `cat .git/ORIG_HEAD` HEAD 2>&1 | tee -a log/deploy.log"
|
|
55
|
-
|
|
56
|
-
run command.join(' && ')
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
task :push do
|
|
61
|
-
system source.local.scm('push', remote, "#{revision}:#{branch}")
|
|
62
|
-
end
|
|
22
|
+
desc "setup", "Create the remote git repository and install push hooks for it"
|
|
23
|
+
method_option :shared, :aliases => '-g', :type => :boolean, :default => true
|
|
24
|
+
method_option :sudo, :aliases => '-s', :type => :boolean, :default => true
|
|
25
|
+
def setup
|
|
26
|
+
sudo = options.sudo? ? "#{sudo_cmd} " : ''
|
|
63
27
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
run command.join(' && ')
|
|
28
|
+
unless run_test("test -x #{deploy_to}")
|
|
29
|
+
run ["#{sudo}mkdir -p #{deploy_to}"] do |cmd|
|
|
30
|
+
cmd << "#{sudo}chown $USER #{deploy_to}" if options.sudo?
|
|
31
|
+
cmd
|
|
32
|
+
end
|
|
70
33
|
end
|
|
71
34
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
command << "cd #{deploy_to}"
|
|
80
|
-
command << "git init #{shared ? '--shared' : ''}"
|
|
81
|
-
command << "sed -i'' -e 's/master/#{branch}/' .git/HEAD" unless branch == 'master'
|
|
82
|
-
command << "git config --bool receive.denyNonFastForwards false" if shared
|
|
83
|
-
command << "git config receive.denyCurrentBranch ignore"
|
|
84
|
-
run command.join(' && ')
|
|
85
|
-
|
|
86
|
-
install_hooks
|
|
87
|
-
push
|
|
35
|
+
run [] do |cmd|
|
|
36
|
+
cmd << "chmod g+ws #{deploy_to}" if options.shared?
|
|
37
|
+
cmd << "cd #{deploy_to}"
|
|
38
|
+
cmd << "git init #{options.shared? ? '--shared' : ''}"
|
|
39
|
+
cmd << "sed -i'' -e 's/master/#{branch}/' .git/HEAD" unless branch == 'master'
|
|
40
|
+
cmd << "git config --bool receive.denyNonFastForwards false" if options.shared?
|
|
41
|
+
cmd << "git config receive.denyCurrentBranch ignore"
|
|
88
42
|
end
|
|
89
43
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
remote_dir = "#{deploy_to}/.git/hooks"
|
|
44
|
+
invoke :hooks
|
|
45
|
+
end
|
|
93
46
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
47
|
+
desc "hooks", "Installs git hooks to the remote repository"
|
|
48
|
+
def hooks
|
|
49
|
+
hooks_dir = File.join(LOCAL_DIR, 'hooks')
|
|
50
|
+
remote_dir = "#{deploy_to}/.git/hooks"
|
|
98
51
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
52
|
+
scp_upload "#{hooks_dir}/post-receive.sh" => "#{remote_dir}/post-receive"
|
|
53
|
+
run "chmod +x #{remote_dir}/post-receive"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
desc "restart", "Restarts the application on the server"
|
|
57
|
+
def restart
|
|
58
|
+
run "cd #{deploy_to} && deploy/restart 2>&1 | tee -a log/deploy.log"
|
|
59
|
+
end
|
|
103
60
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
61
|
+
desc "rollback", "Rolls back the checkout to before the last push"
|
|
62
|
+
def rollback
|
|
63
|
+
run "cd #{deploy_to} && git reset --hard ORIG_HEAD"
|
|
64
|
+
invoke :restart
|
|
65
|
+
end
|
|
107
66
|
|
|
108
|
-
|
|
109
|
-
|
|
67
|
+
desc "log", "Shows the last part of the deploy log on the server"
|
|
68
|
+
method_option :tail, :aliases => '-t', :type => :boolean, :default => false
|
|
69
|
+
method_option :lines, :aliases => '-l', :type => :numeric, :default => 20
|
|
70
|
+
def log(n = nil)
|
|
71
|
+
tail_args = options.tail? ? '-f' : "-n#{n || options.lines}"
|
|
72
|
+
run "tail #{tail_args} #{deploy_to}/log/deploy.log"
|
|
73
|
+
end
|
|
110
74
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
files = (ENV["FILES"] || "").split(",").map { |f| Dir[f.strip] }.flatten
|
|
116
|
-
abort "Please specify at least one file or directory to update (via the FILES environment variable)" if files.empty?
|
|
75
|
+
desc "upload <files>", "Copy local files to the remote app"
|
|
76
|
+
def upload(*files)
|
|
77
|
+
files = files.map { |f| Dir[f.strip] }.flatten
|
|
78
|
+
abort "Error: Specify at least one file to upload" if files.empty?
|
|
117
79
|
|
|
118
|
-
|
|
119
|
-
|
|
80
|
+
scp_upload files.inject({}) { |all, file|
|
|
81
|
+
all[file] = File.join(deploy_to, file)
|
|
82
|
+
all
|
|
83
|
+
}
|
|
120
84
|
end
|
|
121
|
-
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
if [ "$GIT_DIR" = "." ]; then
|
|
5
|
+
# The script has been called as a hook; chdir to the working copy
|
|
6
|
+
cd ..
|
|
7
|
+
unset GIT_DIR
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
# try to obtain the usual system PATH
|
|
11
|
+
if [ -f /etc/profile ]; then
|
|
12
|
+
PATH=$(source /etc/profile; echo $PATH)
|
|
13
|
+
export PATH
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# get the current branch
|
|
17
|
+
head="$(git symbolic-ref HEAD)"
|
|
18
|
+
|
|
19
|
+
# read the STDIN to detect if this push changed the current branch
|
|
20
|
+
while read oldrev newrev refname
|
|
21
|
+
do
|
|
22
|
+
[ "$refname" = "$head" ] && break
|
|
23
|
+
done
|
|
24
|
+
|
|
25
|
+
# abort if there's no update, or in case the branch is deleted
|
|
26
|
+
if [ -z "${newrev//0}" ]; then
|
|
27
|
+
exit
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# check out the latest code into the working copy
|
|
31
|
+
umask 002
|
|
32
|
+
git reset --hard
|
|
33
|
+
|
|
34
|
+
logfile=log/deploy.log
|
|
35
|
+
restart=tmp/restart.txt
|
|
36
|
+
|
|
37
|
+
if [ -z "${oldrev//0}" ]; then
|
|
38
|
+
# this is the first push; this branch was just created
|
|
39
|
+
mkdir -p log tmp
|
|
40
|
+
chmod 0775 log tmp
|
|
41
|
+
touch $logfile $restart
|
|
42
|
+
chmod 0664 $logfile $restart
|
|
43
|
+
|
|
44
|
+
# init submodules
|
|
45
|
+
git submodule update --recursive --init 2>&1 | tee -a $logfile
|
|
46
|
+
else
|
|
47
|
+
# log timestamp
|
|
48
|
+
echo ==== $(date) ==== >> $logfile
|
|
49
|
+
|
|
50
|
+
# execute the deploy hook in background
|
|
51
|
+
[ -x deploy/after_push ] && (nohup deploy/after_push $oldrev $newrev 1>>$logfile 2>>$logfile &)
|
|
52
|
+
fi
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'rspec/autorun'
|
|
2
|
+
require 'git_deploy/configuration'
|
|
3
|
+
|
|
4
|
+
describe GitDeploy::Configuration do
|
|
5
|
+
|
|
6
|
+
subject {
|
|
7
|
+
mod = described_class
|
|
8
|
+
obj = Object.new
|
|
9
|
+
opt = options
|
|
10
|
+
(class << obj; self; end).class_eval do
|
|
11
|
+
include mod
|
|
12
|
+
mod.private_instance_methods.each {|m| public m }
|
|
13
|
+
define_method(:options) { opt }
|
|
14
|
+
end
|
|
15
|
+
obj
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let(:options) { {:remote => 'production'} }
|
|
19
|
+
|
|
20
|
+
def stub_git_config(cmd, value)
|
|
21
|
+
subject.git_config[cmd] = value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stub_remote_url(url, remote = options[:remote])
|
|
25
|
+
stub_git_config("remote -v", "#{remote}\t#{url} (fetch)")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "extracting user/host from remote url" do
|
|
29
|
+
context "ssh url" do
|
|
30
|
+
before { stub_remote_url 'ssh://jon%20doe@example.com:88/path/to/app' }
|
|
31
|
+
|
|
32
|
+
its(:host) { should eq('example.com') }
|
|
33
|
+
its(:remote_port) { should eq(88) }
|
|
34
|
+
its(:remote_user) { should eq('jon doe') }
|
|
35
|
+
its(:deploy_to) { should eq('/path/to/app') }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
context "scp-style" do
|
|
39
|
+
before { stub_remote_url 'git@example.com:/path/to/app' }
|
|
40
|
+
|
|
41
|
+
its(:host) { should eq('example.com') }
|
|
42
|
+
its(:remote_port) { should be_nil }
|
|
43
|
+
its(:remote_user) { should eq('git') }
|
|
44
|
+
its(:deploy_to) { should eq('/path/to/app') }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
metadata
CHANGED
|
@@ -1,86 +1,107 @@
|
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: git-deploy
|
|
3
|
-
version: !ruby/object:Gem::Version
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
segments:
|
|
7
|
-
- 0
|
|
8
|
-
- 4
|
|
9
|
-
- 1
|
|
10
|
-
version: 0.4.1
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
prerelease:
|
|
5
|
+
version: 0.5.5
|
|
11
6
|
platform: ruby
|
|
12
|
-
authors:
|
|
13
|
-
-
|
|
7
|
+
authors:
|
|
8
|
+
- Mislav Marohnić
|
|
14
9
|
autorequire:
|
|
15
10
|
bindir: bin
|
|
16
11
|
cert_chain: []
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
dependencies:
|
|
21
|
-
- !ruby/object:Gem::Dependency
|
|
22
|
-
name: capistrano
|
|
12
|
+
date: 2013-07-11 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
23
15
|
prerelease: false
|
|
24
|
-
|
|
16
|
+
type: :runtime
|
|
17
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
18
|
none: false
|
|
26
|
-
requirements:
|
|
19
|
+
requirements:
|
|
20
|
+
- - '='
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 0.14.6
|
|
23
|
+
name: thor
|
|
24
|
+
requirement: !ruby/object:Gem::Requirement
|
|
25
|
+
none: false
|
|
26
|
+
requirements:
|
|
27
|
+
- - '='
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: 0.14.6
|
|
30
|
+
- !ruby/object:Gem::Dependency
|
|
31
|
+
prerelease: false
|
|
32
|
+
type: :runtime
|
|
33
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
34
|
+
none: false
|
|
35
|
+
requirements:
|
|
27
36
|
- - ~>
|
|
28
|
-
- !ruby/object:Gem::Version
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 2.6.6
|
|
39
|
+
name: net-ssh
|
|
40
|
+
requirement: !ruby/object:Gem::Requirement
|
|
41
|
+
none: false
|
|
42
|
+
requirements:
|
|
43
|
+
- - ~>
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 2.6.6
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
prerelease: false
|
|
35
48
|
type: :runtime
|
|
36
|
-
version_requirements:
|
|
37
|
-
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
none: false
|
|
51
|
+
requirements:
|
|
52
|
+
- - ~>
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 1.1.0
|
|
55
|
+
name: net-scp
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
none: false
|
|
58
|
+
requirements:
|
|
59
|
+
- - ~>
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 1.1.0
|
|
62
|
+
description: A tool to install useful git hooks on your remote repository to enable
|
|
63
|
+
push-based, Heroku-like deployment on your host.
|
|
38
64
|
email: mislav.marohnic@gmail.com
|
|
39
|
-
executables:
|
|
40
|
-
|
|
65
|
+
executables:
|
|
66
|
+
- git-deploy
|
|
41
67
|
extensions: []
|
|
42
|
-
|
|
43
68
|
extra_rdoc_files: []
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
files:
|
|
70
|
+
- bin/git-deploy
|
|
71
|
+
- lib/git_deploy/configuration.rb
|
|
72
|
+
- lib/git_deploy/generator.rb
|
|
73
|
+
- lib/git_deploy/ssh_methods.rb
|
|
74
|
+
- lib/git_deploy/templates/after_push.sh
|
|
75
|
+
- lib/git_deploy/templates/before_restart.rb
|
|
76
|
+
- lib/git_deploy/templates/restart.sh
|
|
46
77
|
- lib/git_deploy.rb
|
|
47
|
-
- lib/hooks/post-receive.
|
|
48
|
-
-
|
|
78
|
+
- lib/hooks/post-receive.sh
|
|
79
|
+
- spec/configuration_spec.rb
|
|
49
80
|
- README.markdown
|
|
50
81
|
- LICENSE
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
82
|
+
homepage: https://github.com/mislav/git-deploy
|
|
83
|
+
licenses:
|
|
84
|
+
- MIT
|
|
55
85
|
post_install_message:
|
|
56
86
|
rdoc_options: []
|
|
57
|
-
|
|
58
|
-
require_paths:
|
|
87
|
+
require_paths:
|
|
59
88
|
- lib
|
|
60
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
90
|
none: false
|
|
62
|
-
requirements:
|
|
63
|
-
- -
|
|
64
|
-
- !ruby/object:Gem::Version
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
- 0
|
|
68
|
-
version: "0"
|
|
69
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ! '>='
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0'
|
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
96
|
none: false
|
|
71
|
-
requirements:
|
|
72
|
-
- -
|
|
73
|
-
- !ruby/object:Gem::Version
|
|
74
|
-
|
|
75
|
-
segments:
|
|
76
|
-
- 0
|
|
77
|
-
version: "0"
|
|
97
|
+
requirements:
|
|
98
|
+
- - ! '>='
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0'
|
|
78
101
|
requirements: []
|
|
79
|
-
|
|
80
102
|
rubyforge_project:
|
|
81
|
-
rubygems_version: 1.
|
|
103
|
+
rubygems_version: 1.8.23
|
|
82
104
|
signing_key:
|
|
83
105
|
specification_version: 3
|
|
84
106
|
summary: Simple git push-based application deployment
|
|
85
107
|
test_files: []
|
|
86
|
-
|
data/lib/hooks/post-receive.rb
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
if ENV['GIT_DIR'] == '.'
|
|
3
|
-
# this means the script has been called as a hook, not manually.
|
|
4
|
-
# get the proper GIT_DIR so we can descend into the working copy dir;
|
|
5
|
-
# if we don't then `git reset --hard` doesn't affect the working tree.
|
|
6
|
-
Dir.chdir('..')
|
|
7
|
-
ENV['GIT_DIR'] = '.git'
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
cmd = %(bash -c "[ -f /etc/profile ] && source /etc/profile; echo $PATH")
|
|
11
|
-
envpath = IO.popen(cmd, 'r') { |io| io.read.chomp }
|
|
12
|
-
ENV['PATH'] = envpath
|
|
13
|
-
|
|
14
|
-
# find out the current branch
|
|
15
|
-
head = `git symbolic-ref HEAD`.chomp
|
|
16
|
-
# abort if we're on a detached head
|
|
17
|
-
exit unless $?.success?
|
|
18
|
-
|
|
19
|
-
oldrev = newrev = nil
|
|
20
|
-
null_ref = '0' * 40
|
|
21
|
-
|
|
22
|
-
# read the STDIN to detect if this push changed the current branch
|
|
23
|
-
while newrev.nil? and gets
|
|
24
|
-
# each line of input is in form of "<oldrev> <newrev> <refname>"
|
|
25
|
-
revs = $_.split
|
|
26
|
-
oldrev, newrev = revs if head == revs.pop
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# abort if there's no update, or in case the branch is deleted
|
|
30
|
-
exit if newrev.nil? or newrev == null_ref
|
|
31
|
-
|
|
32
|
-
# update the working copy
|
|
33
|
-
`umask 002 && git reset --hard`
|
|
34
|
-
|
|
35
|
-
config = 'config/database.yml'
|
|
36
|
-
logfile = 'log/deploy.log'
|
|
37
|
-
restart = 'tmp/restart.txt'
|
|
38
|
-
|
|
39
|
-
if oldrev == null_ref
|
|
40
|
-
# this is the first push; this branch was just created
|
|
41
|
-
require 'fileutils'
|
|
42
|
-
FileUtils.mkdir_p %w(log tmp)
|
|
43
|
-
FileUtils.chmod 0775, %w(log tmp)
|
|
44
|
-
FileUtils.touch [logfile, restart]
|
|
45
|
-
FileUtils.chmod 0664, [logfile, restart]
|
|
46
|
-
|
|
47
|
-
unless File.exists?(config)
|
|
48
|
-
# install the database config from the example file
|
|
49
|
-
example = ['config/database.example.yml', config + '.example'].find { |f| File.exists? f }
|
|
50
|
-
FileUtils.cp example, config if example
|
|
51
|
-
end
|
|
52
|
-
else
|
|
53
|
-
# log timestamp
|
|
54
|
-
File.open(logfile, 'a') { |log| log.puts "==== #{Time.now} ====" }
|
|
55
|
-
# start the post-reset hook in background
|
|
56
|
-
system %(nohup .git/hooks/post-reset #{oldrev} #{newrev} 1>>#{logfile} 2>>#{logfile} &)
|
|
57
|
-
end
|
data/lib/hooks/post-reset.rb
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
RAILS_ENV = 'production'
|
|
3
|
-
oldrev, newrev = ARGV
|
|
4
|
-
$stdout.sync = true
|
|
5
|
-
|
|
6
|
-
def parse_configuration(file)
|
|
7
|
-
config = {}
|
|
8
|
-
current = nil
|
|
9
|
-
|
|
10
|
-
File.open(file).each_line do |line|
|
|
11
|
-
case line
|
|
12
|
-
when /^\[(\w+)(?: "(.+)")\]/
|
|
13
|
-
key, subkey = $1, $2
|
|
14
|
-
current = (config[key] ||= {})
|
|
15
|
-
current = (current[subkey] ||= {}) if subkey
|
|
16
|
-
else
|
|
17
|
-
key, value = line.strip.split(' = ')
|
|
18
|
-
current[key] = value
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
config
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
class Array
|
|
26
|
-
# scans the list of files to see if any of them are under the given path
|
|
27
|
-
def any_in_dir?(dir)
|
|
28
|
-
if Array === dir
|
|
29
|
-
exp = %r{^(?:#{dir.join('|')})/}
|
|
30
|
-
any? { |file| file =~ exp }
|
|
31
|
-
else
|
|
32
|
-
dir += '/'
|
|
33
|
-
any? { |file| file.index(dir) == 0 }
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# get a list of files that changed
|
|
39
|
-
changes = `git diff #{oldrev} #{newrev} --diff-filter=ACDMR --name-status`.split("\n")
|
|
40
|
-
|
|
41
|
-
# make a hash of files that changed and how they changed
|
|
42
|
-
changes_hash = changes.inject(Hash.new { |h, k| h[k] = [] }) do |hash, line|
|
|
43
|
-
modifier, filename = line.split("\t", 2)
|
|
44
|
-
hash[modifier] << filename
|
|
45
|
-
hash
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# create an array of files added, copied, modified or renamed
|
|
49
|
-
modified_files = %w(A C M R).inject([]) { |files, bit| files.concat changes_hash[bit] }
|
|
50
|
-
added_files = changes_hash['A'] # added
|
|
51
|
-
deleted_files = changes_hash['D'] # deleted
|
|
52
|
-
changed_files = modified_files + deleted_files # all
|
|
53
|
-
puts "files changed: #{changed_files.size}"
|
|
54
|
-
|
|
55
|
-
cached_assets_cleared = false
|
|
56
|
-
|
|
57
|
-
# detect modified asset dirs
|
|
58
|
-
asset_dirs = %w(public/stylesheets public/javascripts).select do |dir|
|
|
59
|
-
# did any on the assets under this dir change?
|
|
60
|
-
changed_files.any_in_dir?(dir)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
unless asset_dirs.empty?
|
|
64
|
-
# clear cached assets (unversioned/ignored files)
|
|
65
|
-
system %(git clean -x -f -- #{asset_dirs.join(' ')})
|
|
66
|
-
cached_assets_cleared = true
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
if changed_files.include?('Gemfile') || changed_files.include?('Gemfile.lock')
|
|
70
|
-
# update bundled gems if manifest file has changed
|
|
71
|
-
system %(umask 002 && bundle install --deployment)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# run migrations when new ones added
|
|
75
|
-
if new_migrations = added_files.any_in_dir?('db/migrate')
|
|
76
|
-
system %(umask 002 && rake db:migrate RAILS_ENV=#{RAILS_ENV})
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
if modified_files.include?('.gitmodules')
|
|
80
|
-
# initialize new submodules
|
|
81
|
-
system %(umask 002 && git submodule init)
|
|
82
|
-
# sync submodule remote urls in case of changes
|
|
83
|
-
config = parse_configuration('.gitmodules')
|
|
84
|
-
|
|
85
|
-
if config['submodule']
|
|
86
|
-
config['submodule'].values.each do |submodule|
|
|
87
|
-
path = submodule['path']
|
|
88
|
-
subconf = "#{path}/.git/config"
|
|
89
|
-
|
|
90
|
-
if File.exists? subconf
|
|
91
|
-
old_url = `git config -f "#{subconf}" remote.origin.url`.chomp
|
|
92
|
-
new_url = submodule['url']
|
|
93
|
-
unless old_url == new_url
|
|
94
|
-
puts "changing #{path.inspect} URL:\n #{old_url.inspect} → #{new_url.inspect}"
|
|
95
|
-
`git config -f "#{subconf}" remote.origin.url "#{new_url}"`
|
|
96
|
-
end
|
|
97
|
-
else
|
|
98
|
-
$stderr.puts "a submodule in #{path.inspect} doesn't exist"
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
# update existing submodules
|
|
104
|
-
system %(umask 002 && git submodule update)
|
|
105
|
-
|
|
106
|
-
# clean unversioned files from vendor (e.g. old submodules)
|
|
107
|
-
system %(git clean -d -f vendor)
|
|
108
|
-
|
|
109
|
-
# determine if app restart is needed
|
|
110
|
-
if cached_assets_cleared or new_migrations or !File.exists?('config/environment.rb') or
|
|
111
|
-
changed_files.any_in_dir?(%w(app config lib public vendor))
|
|
112
|
-
require 'fileutils'
|
|
113
|
-
# tell Passenger to restart this app
|
|
114
|
-
FileUtils.touch 'tmp/restart.txt'
|
|
115
|
-
puts "restarting Passenger app"
|
|
116
|
-
end
|