git-deploy 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.markdown CHANGED
@@ -1,109 +1,134 @@
1
- Capistrano strategy for smart git deployment
2
- ============================================
1
+ Easy git deployment
2
+ ===================
3
3
 
4
- Let's set up a straightforward, [Heroku][]-style, push-based deployment, shall we? The goal is that our deployment looks like this:
4
+ Straightforward, [Heroku][]-style, push-based deployment. Your deploys can become as simple as this:
5
5
 
6
- $ git push origin production
6
+ $ git push production master
7
7
 
8
- Assumptions are that you are using git for your Rails app and Passenger on the server. For now, we're going to deploy on a single host.
8
+ To get started, install the "git-deploy" gem.
9
9
 
10
- To get started, install the "git-deploy" gem from Gemcutter.org.
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
- This is not "yet another Capistrano strategy". Capistrano is only used for setup, after which it's all git hooks (see detailed description in "Deployment"). This actually replaces the default Capistrano recipe (which is not loaded) with only a basic set of tasks. If you have more advanced deployment (multiple hosts, many "after deploy" hooks) then this library cannot suit your needs at present.
17
-
18
-
19
- Setup steps
20
- -----------
21
-
22
- 1. Create a git remote for where you'll push the code on your server. The name of this remote in the examples is "origin", but it can be whatever you wish ("online", "website", or other).
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.
27
-
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
-
48
- 3. Run the setup task:
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.
53
-
54
- 4. Login to your server to perform necessary one-time administrative operations. This might include:
16
+ Which app languages/frameworks are supported?
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.
20
+
21
+ Your deployment is customized with per-project callback scripts which can be written in any language.
22
+
23
+ The assumption is that you're deploying to a single host to which you connect over SSH using public/private key authentication.
24
+
25
+
26
+ Initial setup
27
+ -------------
28
+
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
+ ```
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.
37
+
38
+ 2. Run the setup task:
39
+
40
+ ```sh
41
+ git deploy setup -r "production"
42
+ ```
43
+
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.
46
+
47
+ 3. Run the init task:
48
+
49
+ ```sh
50
+ git deploy init
51
+ ```
52
+
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.
56
+
57
+ 4. Push the code.
58
+
59
+ ```sh
60
+ git push production master
61
+ ```
62
+
63
+ 3. Login to your server and manually perform necessary one-time administrative operations. This might include:
55
64
  * set up the Apache/nginx virtual host for this application;
56
- * check out the branch which you will push production code into (often this is "production");
57
- * check your config/database.yml and create or import the production database.
65
+ * check your `config/database.yml` and create the production database.
58
66
 
59
67
 
60
- Deployment
61
- ----------
68
+ Everyday deployments
69
+ --------------------
62
70
 
63
- After you've set everything up, visiting "http://example.com" in your browser should show your app up and running. Subsequent deployments are done simply by pushing to the branch that is currently checked out on our server (see step 4.). The branch is by default "master", but it's suggested to have production code in another branch like "production" or other. This, of course, depends on your git workflow.
71
+ If you've set your app correctly, visiting <http://example.com> in your browser
72
+ should show it up and running.
64
73
 
65
- We've reached our goal; our deployment now looks like:
74
+ Now, subsequent deployments are done simply **by pushing to the branch that is
75
+ currently checked out on the remote**:
66
76
 
67
- $ git push origin production
77
+ git push production master
68
78
 
69
- In fact, running "cap deploy" does exactly this. So what does it do?
79
+ Because the deployments are performed with git, nobody else on the team needs to
80
+ install the "git-deploy" gem.
70
81
 
71
- The "deploy:setup" task installed a couple of hooks in the remote git repository: "post-receive" and "post-reset". The former is a git hook which is invoked after every push to your server, while the latter is a *custom* hook that's called asynchronously by "post-receive" when we updated the deployment branch. This is how your working copy on the server is kept up-to-date.
82
+ On every deploy, the default `deploy/after_push` script performs the following:
72
83
 
73
- Thus, on first push your server automatically:
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.
74
89
 
75
- 1. creates the "log" and "tmp" directories;
76
- 2. copies "config/database.example.yml" or "config/database.yml.example" to "config/database.yml".
90
+ You can customize all this by editing generated scripts in the `deploy/`
91
+ directory of your app.
77
92
 
78
- On every subsequent deploy, the "post-reset" script analyzes changes and:
93
+ Deployments are logged to `log/deploy.log` in your application's directory.
79
94
 
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
95
 
86
- Finally, these are the conditions that dictate an app restart:
96
+ How it works
97
+ ------------
87
98
 
88
- 1. css/javascript assets have been cleared;
89
- 2. the database has migrated;
90
- 3. one or more files/submodules under "app", "config", "lib", "public", or "vendor" changed.
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:
91
103
 
92
- The output of "post-reset" is logged to "log/deploy.log" in your application.
104
+ * `deploy/setup` - on first push.
105
+ * `deploy/after_push` - on subsequent pushes. It in turn executes:
106
+ * `deploy/before_restart`
107
+ * `deploy/restart`
108
+ * `deploy/after_restart`
109
+ * `deploy/rollback` - executed for `git deploy rollback`.
93
110
 
94
- It's worth remembering that "post-reset" is done asynchronously from your push operation. This is because migrating the database and updating submodules might take a long time and we don't want to wait for all that while we're doing 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, depending on what you pushed.
111
+ All of the callbacks are optional. These scripts are ordinary Unix executables.
112
+ The ones which get generated for you by `git deploy init` are written in shell
113
+ script and Ruby.
95
114
 
96
115
 
97
- In the future
98
- -------------
116
+ Extra commands
117
+ --------------
118
+
119
+ * `git deploy hooks` - Updates git hooks on the remote repository
120
+
121
+ * `git deploy log [N=20]` - Shows last 20 lines of deploy log on the server
122
+
123
+ * `git deploy rerun` - Re-runs the `deploy/after_push` callback as if a git push happened
124
+
125
+ * `git deploy restart` - Runs the `deploy/restart` callback
126
+
127
+ * `git deploy rollback` - Undo a deploy by checking out the previous revision,
128
+ runs `deploy/callback` if exists instead of `deploy/after_push`
99
129
 
100
- Next steps for this library are:
130
+ * `git deploy upload <files>` - Copy local files to the remote app
101
131
 
102
- * Support for deployment on multiple hosts. This is a slightly different strategy based on git pull instead of push; something in-between regular "remote cache" strategy and the aforementioned
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
107
132
 
108
133
 
109
- [heroku]: http://heroku.com/
134
+ [heroku]: http://heroku.com/
data/bin/git-deploy ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'git_deploy'
3
+ GitDeploy.start
@@ -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,102 @@
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, opt = {})
15
+ cmd = yield(cmd) if block_given?
16
+ cmd = cmd.join(' && ') if Array === cmd
17
+
18
+ if opt.fetch(:echo, true)
19
+ puts "[#{options[:remote]}] $ " + cmd.gsub(' && ', " && \\\n ")
20
+ end
21
+
22
+ unless options.noop?
23
+ status, output = ssh_exec cmd do |ch, stream, data|
24
+ case stream
25
+ when :stdout then $stdout.print data
26
+ when :stderr then $stderr.print data
27
+ end
28
+ ch.send_data(askpass) if data =~ /^sudo password: /
29
+ end
30
+ output
31
+ end
32
+ end
33
+
34
+ def run_test(cmd)
35
+ status, output = ssh_exec(cmd) { }
36
+ status == 0
37
+ end
38
+
39
+ def ssh_exec(cmd, &block)
40
+ status = nil
41
+ output = ''
42
+
43
+ channel = ssh_connection.open_channel do |chan|
44
+ chan.exec(cmd) do |ch, success|
45
+ raise "command failed: #{cmd.inspect}" unless success
46
+ # ch.request_pty
47
+
48
+ ch.on_data do |c, data|
49
+ output << data
50
+ yield(c, :stdout, data)
51
+ end
52
+
53
+ ch.on_extended_data do |c, type, data|
54
+ output << data
55
+ yield(c, :stderr, data)
56
+ end
57
+
58
+ ch.on_request "exit-status" do |ch, data|
59
+ status = data.read_long
60
+ end
61
+ end
62
+ end
63
+
64
+ channel.wait
65
+ [status, output]
66
+ end
67
+
68
+ # TODO: use Highline for cross-platform support
69
+ def askpass
70
+ tty_state = `stty -g`
71
+ system 'stty raw -echo -icanon isig' if $?.success?
72
+ pass = ''
73
+ while char = $stdin.getbyte and not (char == 13 or char == 10)
74
+ if char == 127 or char == 8
75
+ pass[-1,1] = '' unless pass.empty?
76
+ else
77
+ pass << char.chr
78
+ end
79
+ end
80
+ pass
81
+ ensure
82
+ system "stty #{tty_state}" unless tty_state.empty?
83
+ end
84
+
85
+ def scp_upload(files)
86
+ channels = []
87
+ files.each do |local, remote|
88
+ puts "FILE: [local] #{local.sub(LOCAL_DIR + '/', '')} -> [#{options[:remote]}] #{remote}"
89
+ channels << ssh_connection.scp.upload(local, remote) unless options.noop?
90
+ end
91
+ channels.each { |c| c.wait }
92
+ end
93
+
94
+ def ssh_connection
95
+ @ssh ||= begin
96
+ ssh = Net::SSH.start(host, remote_user, :port => remote_port)
97
+ at_exit { ssh.close }
98
+ ssh
99
+ end
100
+ end
101
+ end
102
+ 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"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ touch tmp/restart.txt
3
+ echo "restarting Passenger app"
data/lib/git_deploy.rb CHANGED
@@ -1,121 +1,104 @@
1
- require 'capistrano/recipes/deploy/scm/git'
1
+ require 'thor'
2
+ require 'net/ssh'
3
+ require 'net/scp'
2
4
 
3
- Capistrano::Configuration.instance(true).load do
4
- def _cset(name, *args, &block)
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
- _cset(:multiple_hosts) { roles.values.map{ |v| v.servers}.flatten.uniq.size > 1 }
15
- _cset(:repository) { `#{ source.local.scm('config', "remote.#{remote}.url") }`.chomp }
16
- _cset(:remote_host) { repository.split(':', 2).first }
17
- _cset(:deploy_to) { repository.split(':', 2).last }
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
- _cset(:current_branch) { File.read('.git/HEAD').chomp.split(' refs/heads/').last }
22
- _cset(:revision) { branch }
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
- # If :run_method is :sudo (or :use_sudo is true), this executes the given command
26
- # via +sudo+. Otherwise is uses +run+. If :as is given as a key, it will be
27
- # passed as the user to sudo as, if using sudo. If the :as key is not given,
28
- # it will default to whatever the value of the :admin_runner variable is,
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
- namespace :deploy do
47
- desc "Deploys your project."
48
- task :default do
49
- unless multiple_hosts
50
- push
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
22
+ desc "setup", "Create the remote git repository and install push hooks for it"
23
+ method_option :shared, :aliases => '-g', :type => :boolean, :default => false
24
+ method_option :sudo, :aliases => '-s', :type => :boolean, :default => false
25
+ def setup
26
+ sudo = options.sudo? ? "#{sudo_cmd} " : ''
59
27
 
60
- task :push do
61
- system source.local.scm('push', remote, "#{revision}:#{branch}")
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
62
33
  end
63
34
 
64
- task :code do
65
- command = ["cd #{deploy_to}"]
66
- command << source.scm('fetch', remote, "+refs/heads/#{branch}:refs/remotes/origin/#{branch}")
67
- command << source.scm('reset', '--hard', "origin/#{branch}")
68
-
69
- run command.join(' && ')
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"
70
42
  end
71
43
 
72
- desc "Prepares servers for deployment."
73
- task :setup do
74
- shared = fetch(:group_writeable)
75
-
76
- command = ["#{try_sudo} mkdir -p #{deploy_to}"]
77
- command << "#{try_sudo} chown $USER #{deploy_to}" if :sudo == run_method
78
- command << "chmod g+ws #{deploy_to}" if shared
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
88
- end
44
+ invoke :hooks
45
+ end
89
46
 
90
- task :install_hooks do
91
- dir = File.dirname(__FILE__) + '/hooks'
92
- remote_dir = "#{deploy_to}/.git/hooks"
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"
93
51
 
94
- top.upload "#{dir}/post-receive.rb", "#{remote_dir}/post-receive"
95
- top.upload "#{dir}/post-reset.rb", "#{remote_dir}/post-reset"
96
- run "chmod +x #{remote_dir}/post-receive #{remote_dir}/post-reset"
97
- end
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
98
60
 
99
- desc "Restarts your Passenger application."
100
- task :restart, :roles => :app do
101
- run "touch #{deploy_to}/tmp/restart.txt"
102
- end
61
+ desc "rerun", "Runs the `deploy/after_push' callback as if a new revision was pushed via git"
62
+ def rerun
63
+ run <<-BASH, :echo => false
64
+ bash -e -c '
65
+ cd '#{deploy_to}'
66
+ declare -a revs=( $(git rev-parse HEAD@{1} HEAD) )
67
+ deploy/after_push ${revs[@]} 2>&1 | tee -a log/deploy.log
68
+ '
69
+ BASH
70
+ end
103
71
 
104
- desc <<-DESC
105
- Copy files to the currently deployed version. Use a comma-separated \
106
- list in FILES to specify which files to upload.
72
+ desc "rollback", "Rolls back the checkout to before the last push"
73
+ def rollback
74
+ run <<-BASH, :echo => false
75
+ bash -e -c '
76
+ cd '#{deploy_to}'
77
+ declare -a revs=( $(git rev-parse HEAD HEAD@{1}) )
78
+ git reset --hard ${revs[1]}
79
+ callback=after_push
80
+ [ -x deploy/rollback ] && callback=rollback
81
+ deploy/$callback ${revs[@]} 2>&1 | tee -a log/deploy.log
82
+ '
83
+ BASH
84
+ end
107
85
 
108
- Note that unversioned files on your server are likely to be \
109
- overwritten by the next push. Always persist your changes by committing.
86
+ desc "log", "Shows the last part of the deploy log on the server"
87
+ method_option :tail, :aliases => '-t', :type => :boolean, :default => false
88
+ method_option :lines, :aliases => '-l', :type => :numeric, :default => 20
89
+ def log(n = nil)
90
+ tail_args = options.tail? ? '-f' : "-n#{n || options.lines}"
91
+ run "tail #{tail_args} #{deploy_to}/log/deploy.log"
92
+ end
110
93
 
111
- $ cap deploy:upload FILES=templates,controller.rb
112
- $ cap deploy:upload FILES='config/apache/*.conf'
113
- DESC
114
- task :upload do
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?
94
+ desc "upload <files>", "Copy local files to the remote app"
95
+ def upload(*files)
96
+ files = files.map { |f| Dir[f.strip] }.flatten
97
+ abort "Error: Specify at least one file to upload" if files.empty?
117
98
 
118
- files.each { |file| top.upload(file, File.join(deploy_to, file)) }
119
- end
99
+ scp_upload files.inject({}) { |all, file|
100
+ all[file] = File.join(deploy_to, file)
101
+ all
102
+ }
120
103
  end
121
- end
104
+ end
@@ -0,0 +1,55 @@
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
+
47
+ # execute the one-time setup hook
48
+ [ -x deploy/setup ] && deploy/setup $oldrev $newrev 2>&1 | tee -a $logfile
49
+ else
50
+ # log timestamp
51
+ echo ==== $(date) ==== >> $logfile
52
+
53
+ # execute the main deploy hook
54
+ [ -x deploy/after_push ] && deploy/after_push $oldrev $newrev 2>&1 | tee -a $logfile
55
+ 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
- hash: 13
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 4
9
- - 1
10
- version: 0.4.1
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.6.0
11
6
  platform: ruby
12
- authors:
13
- - "Mislav Marohni\xC4\x87"
7
+ authors:
8
+ - Mislav Marohnić
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2010-10-08 00:00:00 +02:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
22
- name: capistrano
12
+ date: 2013-07-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
23
15
  prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
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
- hash: 9
30
- segments:
31
- - 2
32
- - 5
33
- - 9
34
- version: 2.5.9
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: *id001
37
- description: A tool to install useful git hooks on your remote repository to enable push-based, Heroku-like deployment on your host.
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
- files:
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.rb
48
- - lib/hooks/post-reset.rb
78
+ - lib/hooks/post-receive.sh
79
+ - spec/configuration_spec.rb
49
80
  - README.markdown
50
81
  - LICENSE
51
- has_rdoc: false
52
- homepage: http://github.com/mislav/git-deploy
53
- licenses: []
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
- hash: 3
66
- segments:
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
- hash: 3
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.3.7
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
-
@@ -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
@@ -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