sunshine 1.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +237 -0
- data/Manifest.txt +70 -0
- data/README.txt +277 -0
- data/Rakefile +46 -0
- data/bin/sunshine +5 -0
- data/examples/deploy.rb +61 -0
- data/examples/deploy_tasks.rake +112 -0
- data/examples/standalone_deploy.rb +31 -0
- data/lib/commands/add.rb +96 -0
- data/lib/commands/default.rb +169 -0
- data/lib/commands/list.rb +322 -0
- data/lib/commands/restart.rb +62 -0
- data/lib/commands/rm.rb +83 -0
- data/lib/commands/run.rb +151 -0
- data/lib/commands/start.rb +72 -0
- data/lib/commands/stop.rb +61 -0
- data/lib/sunshine/app.rb +876 -0
- data/lib/sunshine/binder.rb +70 -0
- data/lib/sunshine/crontab.rb +143 -0
- data/lib/sunshine/daemon.rb +380 -0
- data/lib/sunshine/daemons/ar_sendmail.rb +28 -0
- data/lib/sunshine/daemons/delayed_job.rb +30 -0
- data/lib/sunshine/daemons/nginx.rb +104 -0
- data/lib/sunshine/daemons/rainbows.rb +35 -0
- data/lib/sunshine/daemons/server.rb +66 -0
- data/lib/sunshine/daemons/unicorn.rb +26 -0
- data/lib/sunshine/dependencies.rb +103 -0
- data/lib/sunshine/dependency_lib.rb +200 -0
- data/lib/sunshine/exceptions.rb +54 -0
- data/lib/sunshine/healthcheck.rb +83 -0
- data/lib/sunshine/output.rb +131 -0
- data/lib/sunshine/package_managers/apt.rb +48 -0
- data/lib/sunshine/package_managers/dependency.rb +349 -0
- data/lib/sunshine/package_managers/gem.rb +54 -0
- data/lib/sunshine/package_managers/yum.rb +62 -0
- data/lib/sunshine/remote_shell.rb +241 -0
- data/lib/sunshine/repo.rb +128 -0
- data/lib/sunshine/repos/git_repo.rb +122 -0
- data/lib/sunshine/repos/rsync_repo.rb +29 -0
- data/lib/sunshine/repos/svn_repo.rb +78 -0
- data/lib/sunshine/server_app.rb +554 -0
- data/lib/sunshine/shell.rb +384 -0
- data/lib/sunshine.rb +391 -0
- data/templates/logrotate/logrotate.conf.erb +11 -0
- data/templates/nginx/nginx.conf.erb +109 -0
- data/templates/nginx/nginx_optimize.conf +23 -0
- data/templates/nginx/nginx_proxy.conf +13 -0
- data/templates/rainbows/rainbows.conf.erb +18 -0
- data/templates/tasks/sunshine.rake +114 -0
- data/templates/unicorn/unicorn.conf.erb +6 -0
- data/test/fixtures/app_configs/test_app.yml +11 -0
- data/test/fixtures/sunshine_test/test_upload +0 -0
- data/test/mocks/mock_object.rb +179 -0
- data/test/mocks/mock_open4.rb +117 -0
- data/test/test_helper.rb +188 -0
- data/test/unit/test_app.rb +489 -0
- data/test/unit/test_binder.rb +20 -0
- data/test/unit/test_crontab.rb +128 -0
- data/test/unit/test_git_repo.rb +26 -0
- data/test/unit/test_healthcheck.rb +70 -0
- data/test/unit/test_nginx.rb +107 -0
- data/test/unit/test_rainbows.rb +26 -0
- data/test/unit/test_remote_shell.rb +102 -0
- data/test/unit/test_repo.rb +42 -0
- data/test/unit/test_server.rb +324 -0
- data/test/unit/test_server_app.rb +425 -0
- data/test/unit/test_shell.rb +97 -0
- data/test/unit/test_sunshine.rb +157 -0
- data/test/unit/test_svn_repo.rb +55 -0
- data/test/unit/test_unicorn.rb +22 -0
- metadata +217 -0
@@ -0,0 +1,241 @@
|
|
1
|
+
module Sunshine
|
2
|
+
|
3
|
+
##
|
4
|
+
# Keeps an SSH connection open to a server the app will be deployed to.
|
5
|
+
# Deploy servers use the ssh command and support any ssh feature.
|
6
|
+
# By default, deploy servers use the ControlMaster feature to share
|
7
|
+
# socket connections, with the ControlPath = ~/.ssh/sunshine-%r%h:%p
|
8
|
+
#
|
9
|
+
# Setting session-persistant environment variables is supported by
|
10
|
+
# accessing the @env attribute.
|
11
|
+
|
12
|
+
class RemoteShell < Shell
|
13
|
+
|
14
|
+
class ConnectionError < FatalDeployError; end
|
15
|
+
|
16
|
+
##
|
17
|
+
# The loop to keep the ssh connection open.
|
18
|
+
LOGIN_LOOP = "echo connected; echo ready; for (( ; ; )); do sleep 10; done"
|
19
|
+
|
20
|
+
LOGIN_TIMEOUT = 30
|
21
|
+
|
22
|
+
|
23
|
+
##
|
24
|
+
# Closes all remote shell connections.
|
25
|
+
|
26
|
+
def self.disconnect_all
|
27
|
+
return unless defined?(@remote_shells)
|
28
|
+
@remote_shells.each{|rs| rs.disconnect}
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
##
|
33
|
+
# Registers a remote shell for global access from the class.
|
34
|
+
# Handled automatically on initialization.
|
35
|
+
|
36
|
+
def self.register remote_shell
|
37
|
+
(@remote_shells ||= []) << remote_shell
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
attr_reader :host, :user
|
42
|
+
attr_accessor :ssh_flags, :rsync_flags
|
43
|
+
|
44
|
+
|
45
|
+
##
|
46
|
+
# Remote shells essentially need a host and optional user.
|
47
|
+
# Typical instantiation is done through either of these methods:
|
48
|
+
# RemoteShell.new "user@host"
|
49
|
+
# RemoteShell.new "host", :user => "user"
|
50
|
+
#
|
51
|
+
# The constructor also supports the following options:
|
52
|
+
# :env:: hash - hash of environment variables to set for the ssh session
|
53
|
+
# :password:: string - password for ssh login; if missing the deploy server
|
54
|
+
# will attempt to prompt the user for a password.
|
55
|
+
|
56
|
+
def initialize host, options={}
|
57
|
+
super $stdout, options
|
58
|
+
|
59
|
+
@host, @user = host.split("@").reverse
|
60
|
+
|
61
|
+
@user ||= options[:user]
|
62
|
+
|
63
|
+
@rsync_flags = ["-azP"]
|
64
|
+
@rsync_flags.concat [*options[:rsync_flags]] if options[:rsync_flags]
|
65
|
+
|
66
|
+
@ssh_flags = [
|
67
|
+
"-o ControlMaster=auto",
|
68
|
+
"-o ControlPath=~/.ssh/sunshine-%r@%h:%p"
|
69
|
+
]
|
70
|
+
@ssh_flags.concat ["-l", @user] if @user
|
71
|
+
@ssh_flags.concat [*options[:ssh_flags]] if options[:ssh_flags]
|
72
|
+
|
73
|
+
@pid, @inn, @out, @err = nil
|
74
|
+
|
75
|
+
self.class.register self
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
##
|
80
|
+
# Runs a command via SSH. Optional block is passed the
|
81
|
+
# stream(stderr, stdout) and string data
|
82
|
+
|
83
|
+
def call command_str, options={}, &block
|
84
|
+
Sunshine.logger.info @host, "Running: #{command_str}" do
|
85
|
+
execute ssh_cmd(command_str, options), &block
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
##
|
91
|
+
# Connect to host via SSH and return process pid
|
92
|
+
|
93
|
+
def connect
|
94
|
+
return @pid if connected?
|
95
|
+
|
96
|
+
cmd = ssh_cmd LOGIN_LOOP, :sudo => false
|
97
|
+
|
98
|
+
@pid, @inn, @out, @err = popen4(cmd.join(" "))
|
99
|
+
@inn.sync = true
|
100
|
+
|
101
|
+
data = ""
|
102
|
+
ready = nil
|
103
|
+
start_time = Time.now.to_i
|
104
|
+
|
105
|
+
until ready || @out.eof?
|
106
|
+
data << @out.readpartial(1024)
|
107
|
+
ready = data =~ /ready/
|
108
|
+
|
109
|
+
raise TimeoutError if timed_out?(start_time, LOGIN_TIMEOUT)
|
110
|
+
end
|
111
|
+
|
112
|
+
unless connected?
|
113
|
+
disconnect
|
114
|
+
host_info = [@user, @host].compact.join("@")
|
115
|
+
raise ConnectionError, "Can't connect to #{host_info}"
|
116
|
+
end
|
117
|
+
|
118
|
+
@inn.close
|
119
|
+
@pid
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
##
|
124
|
+
# Check if SSH session is open and returns process pid
|
125
|
+
|
126
|
+
def connected?
|
127
|
+
Process.kill(0, @pid) && @pid rescue false
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
##
|
132
|
+
# Disconnect from host
|
133
|
+
|
134
|
+
def disconnect
|
135
|
+
return unless connected?
|
136
|
+
|
137
|
+
@inn.close rescue nil
|
138
|
+
@out.close rescue nil
|
139
|
+
@err.close rescue nil
|
140
|
+
|
141
|
+
kill_process @pid, "HUP"
|
142
|
+
|
143
|
+
@pid = nil
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
##
|
148
|
+
# Download a file via rsync
|
149
|
+
|
150
|
+
def download from_path, to_path, options={}, &block
|
151
|
+
from_path = "#{@host}:#{from_path}"
|
152
|
+
Sunshine.logger.info @host, "Downloading #{from_path} -> #{to_path}" do
|
153
|
+
execute rsync_cmd(from_path, to_path, options), &block
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
##
|
159
|
+
# Expand a path:
|
160
|
+
# shell.expand_path "~user/thing"
|
161
|
+
# #=> "/home/user/thing"
|
162
|
+
|
163
|
+
def expand_path path
|
164
|
+
dir = File.dirname path
|
165
|
+
full_dir = call "cd #{dir} && pwd"
|
166
|
+
File.join full_dir, File.basename(path)
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
##
|
171
|
+
# Checks if the given file exists
|
172
|
+
|
173
|
+
def file? filepath
|
174
|
+
call("test -f #{filepath}") && true rescue false
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
##
|
179
|
+
# Create a file remotely
|
180
|
+
|
181
|
+
def make_file filepath, content, options={}
|
182
|
+
|
183
|
+
temp_filepath =
|
184
|
+
"#{TMP_DIR}/#{File.basename(filepath)}_#{Time.now.to_i}#{rand(10000)}"
|
185
|
+
|
186
|
+
File.open(temp_filepath, "w+"){|f| f.write(content)}
|
187
|
+
|
188
|
+
self.upload temp_filepath, filepath, options
|
189
|
+
|
190
|
+
File.delete(temp_filepath)
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
##
|
195
|
+
# Uploads a file via rsync
|
196
|
+
|
197
|
+
def upload from_path, to_path, options={}, &block
|
198
|
+
to_path = "#{@host}:#{to_path}"
|
199
|
+
Sunshine.logger.info @host, "Uploading #{from_path} -> #{to_path}" do
|
200
|
+
execute rsync_cmd(from_path, to_path, options), &block
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
def build_rsync_flags options
|
208
|
+
flags = @rsync_flags.dup
|
209
|
+
|
210
|
+
remote_rsync = 'rsync'
|
211
|
+
rsync_sudo = sudo_cmd remote_rsync, options
|
212
|
+
|
213
|
+
unless rsync_sudo == remote_rsync
|
214
|
+
flags << "--rsync-path='#{ rsync_sudo.join(" ") }'"
|
215
|
+
end
|
216
|
+
|
217
|
+
flags << "-e \"ssh #{@ssh_flags.join(' ')}\"" if @ssh_flags
|
218
|
+
|
219
|
+
flags.concat [*options[:flags]] if options[:flags]
|
220
|
+
|
221
|
+
flags
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
def rsync_cmd from_path, to_path, options={}
|
226
|
+
cmd = ["rsync", build_rsync_flags(options), from_path, to_path]
|
227
|
+
cmd.flatten.compact.join(" ")
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
def ssh_cmd string, options={}
|
232
|
+
cmd = sh_cmd string
|
233
|
+
cmd = env_cmd cmd
|
234
|
+
cmd = sudo_cmd cmd, options
|
235
|
+
|
236
|
+
flags = [*options[:flags]].concat @ssh_flags
|
237
|
+
|
238
|
+
["ssh", flags, @host, cmd].flatten.compact
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module Sunshine
|
2
|
+
|
3
|
+
class RepoError < Exception; end
|
4
|
+
|
5
|
+
##
|
6
|
+
# An abstract class to wrap simple basic scm features. The primary function
|
7
|
+
# of repo objects is to get information about the scm branch that is being
|
8
|
+
# deployed and to check it out on remote deploy servers:
|
9
|
+
# svn = SvnRepo.new "svn://path/to/repo", :flags => "--ignore-externals"
|
10
|
+
#
|
11
|
+
# The :flags option can be a String or an Array and supports any scm
|
12
|
+
# checkout (or clone for git) options.
|
13
|
+
|
14
|
+
class Repo
|
15
|
+
|
16
|
+
##
|
17
|
+
# Creates a new repo subclass object:
|
18
|
+
# Repo.new_of_type :svn, "https://path/to/repo/tags/releasetag"
|
19
|
+
# Repo.new_of_type :git, "user@gitbox.com:repo/path"
|
20
|
+
|
21
|
+
def self.new_of_type repo_type, url, options={}
|
22
|
+
repo = "#{repo_type.to_s.capitalize}Repo"
|
23
|
+
Sunshine.const_get(repo).new(url, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
##
|
28
|
+
# Looks for .git and .svn directories and determines if the passed path
|
29
|
+
# is a recognized repo. Does not check for RsyncRepo since it's a
|
30
|
+
# special case. Returns the appropriate repo object:
|
31
|
+
# Repo.detect "path/to/svn/repo/dir"
|
32
|
+
# #=> <SvnRepo @url="svn://url/of/checked/out/repo">
|
33
|
+
# Repo.detect "path/to/git/repo/dir"
|
34
|
+
# #=> <GitRepo, @url="git://url/of/git/repo", @branch="master">
|
35
|
+
# Repo.detect "invalid/repo/path"
|
36
|
+
# #=> nil
|
37
|
+
|
38
|
+
def self.detect path=".", shell=nil
|
39
|
+
|
40
|
+
if SvnRepo.valid? path, shell
|
41
|
+
info = SvnRepo.get_info path, shell
|
42
|
+
SvnRepo.new info[:url], info
|
43
|
+
|
44
|
+
elsif GitRepo.valid? path, shell
|
45
|
+
info = GitRepo.get_info path, shell
|
46
|
+
GitRepo.new info[:url], info
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
##
|
52
|
+
# Gets repo information for the specified dir - Implemented by subclass
|
53
|
+
|
54
|
+
def self.get_info path=".", shell=nil
|
55
|
+
raise RepoError,
|
56
|
+
"The 'get_info' method must be implemented by child classes"
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
attr_reader :url
|
61
|
+
|
62
|
+
def initialize url, options={}
|
63
|
+
@scm = self.class.name.split("::").last.sub('Repo', '').downcase
|
64
|
+
|
65
|
+
@url = url
|
66
|
+
@flags = [*options[:flags]].compact
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
##
|
71
|
+
# Checkout code to a shell and return an info log hash:
|
72
|
+
# repo.chekout_to server, "some/path"
|
73
|
+
# #=> {:revision => 123, :committer => 'someone', :date => time_obj ...}
|
74
|
+
|
75
|
+
def checkout_to path, shell=nil
|
76
|
+
shell ||= Sunshine.shell
|
77
|
+
|
78
|
+
Sunshine.logger.info @scm,
|
79
|
+
"Checking out to #{shell.host} #{path}" do
|
80
|
+
|
81
|
+
Sunshine.dependencies.install @scm, :call => shell if
|
82
|
+
Sunshine.dependencies.exist? @scm
|
83
|
+
|
84
|
+
shell.call "test -d #{path} && rm -rf #{path} || echo false"
|
85
|
+
shell.call "mkdir -p #{path}"
|
86
|
+
|
87
|
+
do_checkout path, shell
|
88
|
+
get_repo_info path, shell
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
##
|
94
|
+
# Checkout the repo - implemented by subclass
|
95
|
+
|
96
|
+
def do_checkout path, shell
|
97
|
+
raise RepoError,
|
98
|
+
"The 'do_checkout' method must be implemented by child classes"
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
##
|
103
|
+
# Get the name of the specified repo - implemented by subclass
|
104
|
+
|
105
|
+
def name
|
106
|
+
raise RepoError,
|
107
|
+
"The 'name' method must be implemented by child classes"
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
##
|
112
|
+
# Returns the set scm flags as a string
|
113
|
+
|
114
|
+
def scm_flags
|
115
|
+
@flags.join(" ")
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
##
|
120
|
+
# Returns the repo information as a hash.
|
121
|
+
|
122
|
+
def get_repo_info path=".", shell=nil
|
123
|
+
defaults = {:type => @scm, :url => @url, :path => path}
|
124
|
+
|
125
|
+
defaults.merge self.class.get_info(path, shell)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Sunshine
|
2
|
+
|
3
|
+
##
|
4
|
+
# Simple wrapper for git repos. Constructor supports :tree option to
|
5
|
+
# specify either a branch or tree-ish to checkout:
|
6
|
+
# git = GitRepo.new "git://mygitrepo.git", :tree => "tags/release001"
|
7
|
+
|
8
|
+
class GitRepo < Repo
|
9
|
+
|
10
|
+
LOG_FORMAT = [
|
11
|
+
":revision: %H",
|
12
|
+
":committer: %cn",
|
13
|
+
":date: %cd",
|
14
|
+
":message: %s",
|
15
|
+
":refs: '%d'",
|
16
|
+
":tree: %t"
|
17
|
+
].join("%n")
|
18
|
+
|
19
|
+
|
20
|
+
##
|
21
|
+
# Check if this is an svn repo
|
22
|
+
|
23
|
+
def self.valid? path="."
|
24
|
+
File.exist? File.join(path, ".git")
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
##
|
29
|
+
# Get the repo info from the path to a checked out git repo
|
30
|
+
|
31
|
+
def self.get_info path=".", shell=nil
|
32
|
+
shell ||= Sunshine.shell
|
33
|
+
|
34
|
+
info = YAML.load git_log(path, shell)
|
35
|
+
|
36
|
+
info[:date] = Time.parse info[:date]
|
37
|
+
info[:branch] = parse_branch info
|
38
|
+
info[:url] = git_origin path, shell
|
39
|
+
|
40
|
+
info
|
41
|
+
rescue => e
|
42
|
+
raise RepoError, e
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
##
|
47
|
+
# Returns the git logs for a path, formatted as yaml.
|
48
|
+
|
49
|
+
def self.git_log path, shell
|
50
|
+
git_options = "-1 --no-color --format=\"#{LOG_FORMAT}\""
|
51
|
+
shell.call "cd #{path} && git log #{git_options}"
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
##
|
56
|
+
# Returns the fetch origin of the current git repo. Returns the path to a
|
57
|
+
# public git repo by default:
|
58
|
+
# GitRepo.git_origin "/some/path", Sunshine.shell
|
59
|
+
# #=> "git://myrepo/path/to/repo.git"
|
60
|
+
# GitRepo.git_origin "/some/path", Sunshine.shell, false
|
61
|
+
# #=> "user@myrepo:path/to/repo.git"
|
62
|
+
|
63
|
+
def self.git_origin path, shell, public_url=true
|
64
|
+
get_origin_cmd = "cd #{path} && git remote -v | grep \\(fetch\\)"
|
65
|
+
|
66
|
+
origin = shell.call get_origin_cmd
|
67
|
+
origin = origin.split(/\t|\s/)[1]
|
68
|
+
|
69
|
+
origin = make_public_url origin if public_url
|
70
|
+
|
71
|
+
origin
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
##
|
76
|
+
# Returns the git url for a public checkout
|
77
|
+
|
78
|
+
def self.make_public_url git_url
|
79
|
+
url, protocol = git_url.split("://").reverse
|
80
|
+
url, user = url.split("@").reverse
|
81
|
+
|
82
|
+
url.gsub!(":", "/") if !protocol
|
83
|
+
|
84
|
+
"git://#{url}"
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
attr_accessor :tree
|
89
|
+
|
90
|
+
def initialize url, options={}
|
91
|
+
super
|
92
|
+
@tree = options[:branch] || options[:tree] || "master"
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def do_checkout path, shell
|
97
|
+
cmd = "cd #{path} && git clone #{@url} #{scm_flags} . && "+
|
98
|
+
"git checkout #{@tree}"
|
99
|
+
shell.call cmd
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
NAME_MATCH = /\/([^\/]+)\.git/
|
104
|
+
|
105
|
+
def name
|
106
|
+
@url.match(NAME_MATCH)[1]
|
107
|
+
rescue
|
108
|
+
raise RepoError, "Git url must match #{NAME_MATCH.inspect}"
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def self.parse_branch response
|
115
|
+
refs = response[:refs]
|
116
|
+
return response[:tree] unless refs && !refs.strip.empty?
|
117
|
+
|
118
|
+
ref_names = refs.delete('()').gsub('/', '_')
|
119
|
+
ref_names.split(',').last.strip
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Sunshine
|
2
|
+
|
3
|
+
##
|
4
|
+
# Allows uploading code directly using rsync, instead of a scm.
|
5
|
+
|
6
|
+
class RsyncRepo < Repo
|
7
|
+
|
8
|
+
def self.get_info path=".", shell=nil
|
9
|
+
{}
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def initialize url, options={}
|
14
|
+
super
|
15
|
+
@flags << "-r"
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def do_checkout path, shell
|
20
|
+
shell.upload @url, path, :flags => @flags
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def name
|
25
|
+
File.basename @url
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Sunshine
|
2
|
+
|
3
|
+
##
|
4
|
+
# Simple scm wrapper for subversion control.
|
5
|
+
|
6
|
+
class SvnRepo < Repo
|
7
|
+
|
8
|
+
##
|
9
|
+
# Check if this is an svn repo
|
10
|
+
|
11
|
+
def self.valid? path="."
|
12
|
+
git_svn?(path) || File.exist?(File.join(path, ".svn"))
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
# Get the repo info from the path to a checked out svn repo
|
18
|
+
|
19
|
+
def self.get_info path=".", shell=nil
|
20
|
+
shell ||= Sunshine.shell
|
21
|
+
|
22
|
+
svn_url = get_svn_url path, shell
|
23
|
+
response = svn_log svn_url, shell
|
24
|
+
|
25
|
+
info = {}
|
26
|
+
|
27
|
+
info[:url] = svn_url
|
28
|
+
info[:revision] = response.match(/revision="(.*)">/)[1]
|
29
|
+
info[:committer] = response.match(/<author>(.*)<\/author>/)[1]
|
30
|
+
info[:date] = Time.parse response.match(/<date>(.*)<\/date>/)[1]
|
31
|
+
info[:message] = response.match(/<msg>(.*)<\/msg>/m)[1]
|
32
|
+
info[:branch] = svn_url.split("/").last
|
33
|
+
|
34
|
+
info
|
35
|
+
rescue => e
|
36
|
+
raise RepoError, e
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
##
|
41
|
+
# Returns the svn logs as xml.
|
42
|
+
|
43
|
+
def self.svn_log path, shell
|
44
|
+
shell.call "svn log #{path} --limit 1 --xml"
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
##
|
49
|
+
# Check if this is a git-svn repo.
|
50
|
+
|
51
|
+
def self.git_svn? path="."
|
52
|
+
File.exist? File.join(path, ".git/svn")
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
##
|
57
|
+
# Get the svn url from a svn or git-svn checkout.
|
58
|
+
|
59
|
+
def self.get_svn_url path, shell
|
60
|
+
cmd = git_svn?(path) ? "git svn" : "svn"
|
61
|
+
shell.call("cd #{path} && #{cmd} info | grep ^URL:").split(" ")[1]
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def do_checkout path, shell
|
66
|
+
shell.call "svn checkout #{scm_flags} #{@url} #{path}"
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
NAME_MATCH = /([^\/]+\/)+([^\/]+)\/(trunk|branches|tags)/
|
71
|
+
|
72
|
+
def name
|
73
|
+
@url.match(NAME_MATCH)[2]
|
74
|
+
rescue
|
75
|
+
raise RepoError, "Svn url must match #{NAME_MATCH.inspect}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|