sunshine 1.0.0.pre
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/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
|