capistrano-patch 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.rvmrc +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +29 -0
- data/README.md +162 -0
- data/Rakefile +1 -0
- data/capistrano-patch.gemspec +22 -0
- data/lib/capistrano/patch.rb +44 -0
- data/lib/capistrano/patch/strategy.rb +18 -0
- data/lib/capistrano/patch/strategy/base.rb +94 -0
- data/lib/capistrano/patch/strategy/git.rb +57 -0
- data/lib/capistrano/patch/strategy/git_server.rb +40 -0
- data/lib/capistrano/patch/version.rb +5 -0
- metadata +93 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm ruby-1.8.7
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
capistrano-patch (0.0.1)
|
5
|
+
capistrano (>= 2.5.5)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
capistrano (2.9.0)
|
11
|
+
highline
|
12
|
+
net-scp (>= 1.0.0)
|
13
|
+
net-sftp (>= 2.0.0)
|
14
|
+
net-ssh (>= 2.0.14)
|
15
|
+
net-ssh-gateway (>= 1.1.0)
|
16
|
+
highline (1.6.2)
|
17
|
+
net-scp (1.0.4)
|
18
|
+
net-ssh (>= 1.99.1)
|
19
|
+
net-sftp (2.0.5)
|
20
|
+
net-ssh (>= 2.0.9)
|
21
|
+
net-ssh (2.2.1)
|
22
|
+
net-ssh-gateway (1.1.0)
|
23
|
+
net-ssh (>= 1.99.1)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
capistrano-patch!
|
data/README.md
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
# Capistrano patch recipes
|
2
|
+
|
3
|
+
## Concept
|
4
|
+
|
5
|
+
Usual deployment procedure is pretty good.
|
6
|
+
But real project are big and offen have several megabytes of code. So standard deployment is not so fast.
|
7
|
+
You need chekout new code, prepare all symplinks, etc ... It takes the time.
|
8
|
+
|
9
|
+
Sometimes you need to *quickly* apply hostfix that fixes critical bug.
|
10
|
+
Usually such fix is small and may contain only several lines of code.
|
11
|
+
Doing it manual it's not good from any point of view.
|
12
|
+
|
13
|
+
So that's why *capistrano patch recipes* appears. They are extracted from real big project.
|
14
|
+
|
15
|
+
It's automated solution for safely code patching on any stage.
|
16
|
+
|
17
|
+
Depending on patch strategy `cap patch` :
|
18
|
+
|
19
|
+
* creates patch file
|
20
|
+
* delivers patch file to all application servers
|
21
|
+
* checks patch
|
22
|
+
* applies patch
|
23
|
+
* updates REVISION
|
24
|
+
|
25
|
+
If something went wrong your also able to quickly revert patch with `cap patch:revert`. See recipes below.
|
26
|
+
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
gem install capistrano-patch
|
31
|
+
|
32
|
+
Add to `Capfile`
|
33
|
+
|
34
|
+
# load standard capistrano recipes
|
35
|
+
load 'deploy'
|
36
|
+
|
37
|
+
# load patch recipes
|
38
|
+
require 'capistrano/patch'
|
39
|
+
|
40
|
+
## Configuration
|
41
|
+
|
42
|
+
* *:patch_via* - patch strategy (default `:git`)
|
43
|
+
* *:patch_repository* - SCM repository that will be using for generating diffs (default `.git`)
|
44
|
+
* *:patch_directory* - directory for storing generated patches (default `.`)
|
45
|
+
* *:patch_base_url* - base url for patch download ( default `nil`)
|
46
|
+
* *:patch_server_host* - server host for patch generating ( default `nil`)
|
47
|
+
* *:patch_server_options* -server host options ( default `{}`)
|
48
|
+
|
49
|
+
## Patch strategies
|
50
|
+
|
51
|
+
Patch strategy is about how to process code patching on stage. It also depends on patch source (or scm).
|
52
|
+
|
53
|
+
Currently implemented patch strategies:
|
54
|
+
|
55
|
+
* git
|
56
|
+
* git_server
|
57
|
+
|
58
|
+
## Git strategy
|
59
|
+
|
60
|
+
It's simple strategy that is suitable for almost all git projects.
|
61
|
+
|
62
|
+
### Integrated deployment
|
63
|
+
|
64
|
+
When you use capistrano directly into your application you need zero configuration.
|
65
|
+
|
66
|
+
### Separated deployment
|
67
|
+
|
68
|
+
When you have separated deployment repository you need to specify patch repository and directory.
|
69
|
+
|
70
|
+
Example:
|
71
|
+
|
72
|
+
set(:patch_repository) { "../#{application}.git" }
|
73
|
+
set(:patch_directory) { "/tmp/patches/#{application}" }
|
74
|
+
|
75
|
+
## GitServer strategy
|
76
|
+
|
77
|
+
For big projects offen is required to generate patch using some certain or origin repository and store generated patch in some directory there. Thus patch can be accessible from directory server via http and delivered to application servers from the same host where repository is located.
|
78
|
+
|
79
|
+
Example:
|
80
|
+
|
81
|
+
set :patch_via, :git_server
|
82
|
+
|
83
|
+
set(:patch_repository) { "/var/git/repositories/#{application}.git" }
|
84
|
+
set(:patch_directory) { "/var/www/patches/#{application}" }
|
85
|
+
set(:patch_base_url) { "http://scm.example.com/patches/#{application}" }
|
86
|
+
|
87
|
+
set :patch_server_host, "scm.example.com"
|
88
|
+
|
89
|
+
### Extra options
|
90
|
+
|
91
|
+
Patch server is not in global capistrano servers list.
|
92
|
+
So any other task will not affect your server. And this is good.
|
93
|
+
But offen you need to specify different options to patch server instead of global values.
|
94
|
+
|
95
|
+
When you want to specify just another `user` or `port` you can do it in `patch_server_host` variable:
|
96
|
+
|
97
|
+
set :patch_server_host, "USER@scm.example.com:PORT"
|
98
|
+
|
99
|
+
or using `patch_server_options` variable:
|
100
|
+
|
101
|
+
set :patch_server_options, {
|
102
|
+
:user => 'patcher',
|
103
|
+
:port => 2222
|
104
|
+
}
|
105
|
+
|
106
|
+
Also you are able to specify another ssh keys:
|
107
|
+
|
108
|
+
set :patch_server_options, {
|
109
|
+
:ssh_options => { :auth_methods => %w(publickey), :keys => %w(keys/patcher.pem) }
|
110
|
+
}
|
111
|
+
|
112
|
+
## Recipes
|
113
|
+
|
114
|
+
### Create deliver and apply patch
|
115
|
+
|
116
|
+
$ cap patch FROM=v0.0.1 TO=v0.0.2
|
117
|
+
|
118
|
+
or
|
119
|
+
|
120
|
+
$ cap patch PATCH=v0.0.1-v0.0.2
|
121
|
+
|
122
|
+
or
|
123
|
+
|
124
|
+
$ cap patch FROM=development TO=master
|
125
|
+
|
126
|
+
|
127
|
+
* It will create patch file like `sha1-sha2.patch`
|
128
|
+
* It will deliver patch file to all application servers
|
129
|
+
* It will check patch before applying
|
130
|
+
* It will apply check
|
131
|
+
* It will update REVISION after applying
|
132
|
+
|
133
|
+
|
134
|
+
### Create patch
|
135
|
+
|
136
|
+
$ cap patch:create FROM=v0.0.1 TO=v0.0.2
|
137
|
+
|
138
|
+
Create file like: `6c5923863a288b089a6a2bc11e560f0d28dabfb6-92e018dbda8a91c442e40257b81097ceeb150876.patch`
|
139
|
+
|
140
|
+
### Deliver patch
|
141
|
+
|
142
|
+
If patch file is missing on some server you can manualy deliver it with
|
143
|
+
|
144
|
+
$ cap patch:deliver FROM=v0.0.1 TO=v0.0.2 HOSTFILTER='app1'
|
145
|
+
|
146
|
+
### Apply patch
|
147
|
+
|
148
|
+
If may manualy apply patch on some server:
|
149
|
+
|
150
|
+
$ cap patch:apply FROM=v0.0.1 TO=v0.0.2 HOSTFILTER='app1'
|
151
|
+
|
152
|
+
### Revert patch
|
153
|
+
|
154
|
+
|
155
|
+
If something went bad you may revert patch
|
156
|
+
|
157
|
+
$ cap patch:revert FROM=v0.0.1 TO=v0.0.2
|
158
|
+
# or
|
159
|
+
$ cap patch:revert PATCH=v0.0.1-v0.0.2
|
160
|
+
# or
|
161
|
+
$ cap patch:revert PATCH=6c5923863a288b089a6a2bc11e560f0d28dabfb6-92e018dbda8a91c442e40257b81097ceeb150876.patch
|
162
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
require "capistrano/patch/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "capistrano-patch"
|
8
|
+
s.version = Capistrano::Patch::VERSION
|
9
|
+
s.authors = ["Andriy Yanko"]
|
10
|
+
s.email = ["andriy.yanko@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/railsware/capistrano-patch"
|
12
|
+
s.summary = %q{Capistrano patch recipes}
|
13
|
+
|
14
|
+
s.rubyforge_project = "capistrano-patch"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_runtime_dependency "capistrano", ">=2.5.5"
|
22
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'capistrano/patch/strategy'
|
2
|
+
|
3
|
+
Capistrano::Configuration.instance.load do
|
4
|
+
|
5
|
+
set(:patch_strategy) { Capistrano::Patch::Strategy.new(fetch(:patch_via, scm), self) }
|
6
|
+
|
7
|
+
namespace :patch do
|
8
|
+
|
9
|
+
desc "Create, deliver and apply patch"
|
10
|
+
task :default, :except => { :no_release => true } do
|
11
|
+
top.patch.create
|
12
|
+
top.patch.deliver
|
13
|
+
top.patch.apply
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Create patch"
|
17
|
+
task :create, :except => { :no_release => true } do
|
18
|
+
patch_strategy.create
|
19
|
+
Capistrano::CLI.ui.say "Patch location: #{patch_strategy.patch_location}"
|
20
|
+
abort unless Capistrano::CLI.ui.ask("Is created patch ok? (y/n)") == 'y'
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Deliver patch"
|
24
|
+
task :deliver, :except => { :no_release => true } do
|
25
|
+
patch_strategy.deliver
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Apply patch"
|
29
|
+
task :apply, :except => { :no_release => true } do
|
30
|
+
abort unless Capistrano::CLI.ui.ask("Apply #{patch_strategy.patch_file} ? (y/n)") == 'y'
|
31
|
+
patch_strategy.check_apply
|
32
|
+
patch_strategy.apply
|
33
|
+
patch_strategy.mark_apply
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "Revert patch"
|
37
|
+
task :revert, :except => { :no_release => true } do
|
38
|
+
abort unless Capistrano::CLI.ui.ask("Revert #{patch_strategy.patch_file} ? (y/n)") == 'y'
|
39
|
+
patch_strategy.check_revert
|
40
|
+
patch_strategy.revert
|
41
|
+
patch_strategy.mark_revert
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Capistrano
|
2
|
+
module Patch
|
3
|
+
module Strategy
|
4
|
+
|
5
|
+
def self.new(strategy, config = {})
|
6
|
+
require "capistrano/patch/strategy/#{strategy}"
|
7
|
+
|
8
|
+
strategy_const = strategy.to_s.capitalize.gsub(/_(.)/) { $1.upcase }
|
9
|
+
|
10
|
+
const_get(strategy_const).new(config)
|
11
|
+
|
12
|
+
rescue LoadError
|
13
|
+
raise Capistrano::Error, "could not find any strategy named `#{strategy}'"
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'capistrano/recipes/deploy/strategy/base'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Patch
|
5
|
+
module Strategy
|
6
|
+
class Base < Deploy::Strategy::Base
|
7
|
+
|
8
|
+
def initialize(*)
|
9
|
+
super
|
10
|
+
|
11
|
+
if ENV['PATCH']
|
12
|
+
ENV['FROM'], ENV['TO'] = File.basename(ENV['PATCH'], '.patch').split('-', 2)
|
13
|
+
end
|
14
|
+
|
15
|
+
ENV['FROM'] or abort "Please specify FROM revision environment variable"
|
16
|
+
ENV['TO'] or abort "Please specify TO revision environment variable"
|
17
|
+
|
18
|
+
@revision_from = normalize_revision(ENV['FROM'])
|
19
|
+
@revision_to = normalize_revision(ENV['TO'])
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Configuration
|
24
|
+
#
|
25
|
+
|
26
|
+
attr_reader :revision_from, :revision_to
|
27
|
+
|
28
|
+
def patch_file
|
29
|
+
@patch_file ||= "#{revision_from}-#{revision_to}.patch"
|
30
|
+
end
|
31
|
+
|
32
|
+
def patch_directory
|
33
|
+
raise NotImplementedError, "`patch_directory' is not implemented by #{self.class.name}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def patch_path
|
37
|
+
File.join patch_directory, patch_file
|
38
|
+
end
|
39
|
+
|
40
|
+
def patch_location
|
41
|
+
raise NotImplementedError, "`patch_location' is not implemented by #{self.class.name}"
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Actions
|
46
|
+
#
|
47
|
+
|
48
|
+
def create
|
49
|
+
raise NotImplementedError, "`create' is not implemented by #{self.class.name}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def deliver
|
53
|
+
raise NotImplementedError, "`deliver' is not implemented by #{self.class.name}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def check_apply
|
57
|
+
raise NotImplementedError, "`check_apply' is not implemented by #{self.class.name}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def check_revert
|
61
|
+
raise NotImplementedError, "`check_revert' is not implemented by #{self.class.name}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def apply
|
65
|
+
raise NotImplementedError, "`apply' is not implemented by #{self.class.name}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def revert
|
69
|
+
raise NotImplementedError, "`revert' is not implemented by #{self.class.name}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def mark_apply
|
73
|
+
mark(revision_to)
|
74
|
+
end
|
75
|
+
|
76
|
+
def mark_revert
|
77
|
+
mark(revision_from)
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def normalize_revision(revision)
|
83
|
+
source.local.query_revision(revision) { |cmd| run_locally(cmd) }
|
84
|
+
end
|
85
|
+
|
86
|
+
def mark(revision)
|
87
|
+
command = "echo #{revision} > #{configuration.current_path}/REVISION"
|
88
|
+
run(command)
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'capistrano/patch/strategy/base'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Patch
|
5
|
+
module Strategy
|
6
|
+
class Git < Base
|
7
|
+
|
8
|
+
def patch_repository
|
9
|
+
configuration.fetch(:patch_repository, '.git')
|
10
|
+
end
|
11
|
+
|
12
|
+
def patch_directory
|
13
|
+
configuration.fetch(:patch_directory, '.')
|
14
|
+
end
|
15
|
+
|
16
|
+
def patch_location
|
17
|
+
patch_path
|
18
|
+
end
|
19
|
+
|
20
|
+
def create
|
21
|
+
command = "env GIT_DIR=#{patch_repository} "
|
22
|
+
command << source.local.scm('diff-tree', "--binary #{revision_from}..#{revision_to} > #{patch_path}")
|
23
|
+
system(command)
|
24
|
+
end
|
25
|
+
|
26
|
+
def deliver
|
27
|
+
top.upload patch_path, "#{configuration.current_path}/#{patch_file}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def check_apply
|
31
|
+
command = "cd #{configuration.current_path} && "
|
32
|
+
command << source.scm('apply', "--binary --check #{patch_file}")
|
33
|
+
run(command)
|
34
|
+
end
|
35
|
+
|
36
|
+
def check_revert
|
37
|
+
command = "cd #{configuration.current_path} && "
|
38
|
+
command << source.scm('apply', "-R --binary --check #{patch_file}")
|
39
|
+
run(command)
|
40
|
+
end
|
41
|
+
|
42
|
+
def apply
|
43
|
+
command = "cd #{configuration.current_path} && "
|
44
|
+
command << source.scm('apply', "--binary #{patch_file}")
|
45
|
+
run(command)
|
46
|
+
end
|
47
|
+
|
48
|
+
def revert
|
49
|
+
command = "cd #{configuration.current_path} && "
|
50
|
+
command << source.scm('apply', "-R --binary #{patch_file}")
|
51
|
+
run(command)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'capistrano/patch/strategy/git'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Patch
|
5
|
+
module Strategy
|
6
|
+
class GitServer < Git
|
7
|
+
|
8
|
+
def patch_location
|
9
|
+
File.join configuration.fetch(:patch_base_url), patch_file
|
10
|
+
end
|
11
|
+
|
12
|
+
def create
|
13
|
+
command = "mkdir -p #{patch_directory} && "
|
14
|
+
command << source.scm('diff-tree', "--binary #{revision_from}..#{revision_to} > #{patch_path}")
|
15
|
+
|
16
|
+
run(command, {
|
17
|
+
:hosts => [server],
|
18
|
+
:env => { 'GIT_DIR' => patch_repository }
|
19
|
+
})
|
20
|
+
end
|
21
|
+
|
22
|
+
def deliver
|
23
|
+
command = "cd #{current_path} && "
|
24
|
+
command << "wget -N -q #{patch_location}"
|
25
|
+
run(command)
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def server
|
31
|
+
ServerDefinition.new(
|
32
|
+
configuration.fetch(:patch_server_host),
|
33
|
+
configuration.fetch(:patch_server_options, {})
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: capistrano-patch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Andriy Yanko
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-10-22 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: capistrano
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 17
|
29
|
+
segments:
|
30
|
+
- 2
|
31
|
+
- 5
|
32
|
+
- 5
|
33
|
+
version: 2.5.5
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
description:
|
37
|
+
email:
|
38
|
+
- andriy.yanko@gmail.com
|
39
|
+
executables: []
|
40
|
+
|
41
|
+
extensions: []
|
42
|
+
|
43
|
+
extra_rdoc_files: []
|
44
|
+
|
45
|
+
files:
|
46
|
+
- .gitignore
|
47
|
+
- .rvmrc
|
48
|
+
- Gemfile
|
49
|
+
- Gemfile.lock
|
50
|
+
- README.md
|
51
|
+
- Rakefile
|
52
|
+
- capistrano-patch.gemspec
|
53
|
+
- lib/capistrano/patch.rb
|
54
|
+
- lib/capistrano/patch/strategy.rb
|
55
|
+
- lib/capistrano/patch/strategy/base.rb
|
56
|
+
- lib/capistrano/patch/strategy/git.rb
|
57
|
+
- lib/capistrano/patch/strategy/git_server.rb
|
58
|
+
- lib/capistrano/patch/version.rb
|
59
|
+
homepage: https://github.com/railsware/capistrano-patch
|
60
|
+
licenses: []
|
61
|
+
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
hash: 3
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
hash: 3
|
82
|
+
segments:
|
83
|
+
- 0
|
84
|
+
version: "0"
|
85
|
+
requirements: []
|
86
|
+
|
87
|
+
rubyforge_project: capistrano-patch
|
88
|
+
rubygems_version: 1.8.6
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: Capistrano patch recipes
|
92
|
+
test_files: []
|
93
|
+
|