divergence 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +111 -0
- data/Rakefile +18 -0
- data/bin/divergence +45 -0
- data/divergence.gemspec +26 -0
- data/generators/files/callbacks.rb +9 -0
- data/generators/files/config.rb +14 -0
- data/generators/files/config.ru +4 -0
- data/lib/divergence/config.rb +58 -0
- data/lib/divergence/git_manager.rb +150 -0
- data/lib/divergence/helpers.rb +34 -0
- data/lib/divergence/request_parser.rb +52 -0
- data/lib/divergence/respond.rb +71 -0
- data/lib/divergence/version.rb +3 -0
- data/lib/divergence/webhook.rb +31 -0
- data/lib/divergence.rb +54 -0
- data/lib/rack_ssl_hack.rb +14 -0
- data/log/.gitkeep +0 -0
- data/public/404.html +46 -0
- data/test/app_root/test.txt +1 -0
- data/test/config.rb +10 -0
- data/test/config_test.rb +18 -0
- data/test/git_test.rb +59 -0
- data/test/request_test.rb +23 -0
- data/test/test_helper.rb +72 -0
- data/test/webhook_test.rb +19 -0
- metadata +180 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
Copyright 2012 LayerVault Inc.
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
4
|
+
|
5
|
+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Ryan LeFevre
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# Divergence
|
2
|
+
|
3
|
+
Map subdomains to git branches for switching live codebases on the fly. It's a Rack application that acts as a HTTP proxy between you and your web application for rapid testing.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
First, you will need to install the gem:
|
8
|
+
|
9
|
+
```
|
10
|
+
gem install divergence
|
11
|
+
```
|
12
|
+
|
13
|
+
Then, since divergence is a rackup application, you will need to initialize it somewhere by running:
|
14
|
+
|
15
|
+
```
|
16
|
+
divergence init
|
17
|
+
```
|
18
|
+
|
19
|
+
This copies all of the necessary files into the current folder for you.
|
20
|
+
|
21
|
+
## Config
|
22
|
+
|
23
|
+
All configuration happens in `config/config.rb`. You must set the git repository root and the application root before using divergence.
|
24
|
+
|
25
|
+
You will probably want divergence to take over port 80 on your testing server, so you may have to update the forwarding host/port. Note, this is the address where your actual web application can be reached.
|
26
|
+
|
27
|
+
### Callbacks
|
28
|
+
|
29
|
+
Divergence lets you hook into various callbacks throughout the entire process. These are defined in `config/callbacks.rb`. Most callbacks automatically change the current working directory for you in order to make modifications as simple as possible.
|
30
|
+
|
31
|
+
The available callbacks are:
|
32
|
+
|
33
|
+
* before_swap
|
34
|
+
* Active dir: git repository
|
35
|
+
* after_swap
|
36
|
+
* Active dir: application
|
37
|
+
* before_pull
|
38
|
+
* Active dir: git repository
|
39
|
+
* Only executes if a git pull is required
|
40
|
+
* after_pull
|
41
|
+
* Active dir: git repository
|
42
|
+
* Only executes if the git pull succeeds
|
43
|
+
* on_pull_error
|
44
|
+
* Active dir: git repository
|
45
|
+
* Executes if there is a problem checking out and pulling a branch
|
46
|
+
* on_branch_discover
|
47
|
+
* Active dir: git repository
|
48
|
+
* Executes if the subdomain has a dash in the name. The subdomain name is passed to the callback in the options hash.
|
49
|
+
* If the callback returns nil, Divergence will try to auto-detect the branch name, otherwise it will use whatever you return.
|
50
|
+
|
51
|
+
There are also some built-in helper methods that are available inside callbacks. They are:
|
52
|
+
|
53
|
+
* bundle_install
|
54
|
+
* restart_passenger
|
55
|
+
|
56
|
+
### Github Service Hook
|
57
|
+
|
58
|
+
You can automatically keep the currently active branch up to date by using a Github service hook. In your repository on Github, go to Admin -> Service Hooks -> WebHook URLs. Add the url:
|
59
|
+
|
60
|
+
```
|
61
|
+
http://divergence.[your domain].com/update
|
62
|
+
```
|
63
|
+
|
64
|
+
Now, whenever you push code to your repository, divergence will automatically know and will update accordingly.
|
65
|
+
|
66
|
+
## Running
|
67
|
+
|
68
|
+
To start divergence, simply run in the divergence directory you initialized:
|
69
|
+
|
70
|
+
```
|
71
|
+
divergence start
|
72
|
+
```
|
73
|
+
|
74
|
+
This will start up divergence on port 9292 by default. If you'd like divergence to run on a different port, you can specify that as well:
|
75
|
+
|
76
|
+
```
|
77
|
+
divergence start --port=88
|
78
|
+
```
|
79
|
+
|
80
|
+
There is also a `--dev` flag that will run divergence in the foreground instead of daemonizing it.
|
81
|
+
|
82
|
+
### Port 80
|
83
|
+
|
84
|
+
On many systems, running on port 80 requires special permissions. If you try starting divergence, but get the error `TCPServer Error: Permission denied - bind(2)`, then you will need to run divergence with sudo (or as root). If you use RVM to manage multiple Ruby versions, then you can use `rvmsudo` instead.
|
85
|
+
|
86
|
+
Make sure, if you're using Git over SSH, that you have your repository's host added to your known hosts file for the root user.
|
87
|
+
|
88
|
+
### HTTPS
|
89
|
+
|
90
|
+
Divergence currently does not support HTTPS on its own; however, you can still use HTTPS in combination with a load balancer if you enable SSL termination.
|
91
|
+
|
92
|
+
## TODO
|
93
|
+
|
94
|
+
* Handle multiple users at the same time
|
95
|
+
* Build-in HTTPS support
|
96
|
+
|
97
|
+
## Contributing
|
98
|
+
|
99
|
+
1. Fork it
|
100
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
101
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
102
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
103
|
+
5. Create new Pull Request
|
104
|
+
|
105
|
+
## Authors
|
106
|
+
|
107
|
+
* [Ryan LeFevre](http://meltingice.net) - Project Creator
|
108
|
+
|
109
|
+
## License
|
110
|
+
|
111
|
+
Licensed under the Apache 2.0 License. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
task :docs do
|
5
|
+
`rdoc --main lib/divergence.rb lib`
|
6
|
+
end
|
7
|
+
|
8
|
+
namespace :test do
|
9
|
+
Rake::TestTask.new(:rack) do |t|
|
10
|
+
t.libs << "test"
|
11
|
+
t.pattern = "test/*_test.rb"
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
task :test do
|
17
|
+
Rake::Task["test:rack"].invoke
|
18
|
+
end
|
data/bin/divergence
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "thor/group"
|
5
|
+
|
6
|
+
module CLI
|
7
|
+
class Init < Thor::Group
|
8
|
+
include ::Thor::Actions
|
9
|
+
|
10
|
+
desc "Initializes a divergence application into the current directory"
|
11
|
+
|
12
|
+
def self.source_root
|
13
|
+
::File.expand_path('../../generators/files', __FILE__)
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_directories
|
17
|
+
empty_directory "config"
|
18
|
+
empty_directory "log"
|
19
|
+
end
|
20
|
+
|
21
|
+
def copy_templates
|
22
|
+
template "config.rb", "config/config.rb"
|
23
|
+
template "callbacks.rb", "config/callbacks.rb"
|
24
|
+
template "config.ru", "config.ru"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module CLI
|
30
|
+
class Base < Thor
|
31
|
+
register CLI::Init, "init", "init", "Initializes a divergence application into the current directory"
|
32
|
+
|
33
|
+
desc "start", "Start divergence"
|
34
|
+
method_options :port => :number, :dev => :boolean
|
35
|
+
def start
|
36
|
+
cmd = 'rackup'
|
37
|
+
cmd << " -p #{options[:port]}" if options[:port]
|
38
|
+
cmd << " -D" unless options[:dev]
|
39
|
+
|
40
|
+
exec cmd
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
CLI::Base.start
|
data/divergence.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'divergence/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "divergence"
|
8
|
+
gem.version = Divergence::VERSION
|
9
|
+
gem.authors = ["Ryan LeFevre"]
|
10
|
+
gem.email = ["ryan@layervault.com"]
|
11
|
+
gem.description = "Map subdomains to git branches for switching live codebases on the fly. It's a Rack application that acts as a HTTP proxy between you and your web application for rapid testing."
|
12
|
+
gem.summary = "Map virtual host subdomains to git branches for testing"
|
13
|
+
gem.homepage = "http://cosmos.layervault.com/divergence.html"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency "rack"
|
21
|
+
gem.add_dependency "rack-proxy"
|
22
|
+
gem.add_dependency "thor"
|
23
|
+
gem.add_dependency "git"
|
24
|
+
gem.add_dependency "json"
|
25
|
+
gem.add_development_dependency "rack-test"
|
26
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
Divergence::Application.configure do |config|
|
2
|
+
config.callbacks :after_swap do
|
3
|
+
# Run anything after the swap finishes
|
4
|
+
#
|
5
|
+
# after_swap changes to the app directory for you, so
|
6
|
+
# you can simply run any commands you want. There are
|
7
|
+
# some built-in helpers too.
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.expand_path('../callbacks', __FILE__)
|
2
|
+
|
3
|
+
Divergence::Application.configure do |config|
|
4
|
+
config.git_path = nil # Change this to the git repository path
|
5
|
+
config.app_path = nil # and this to your application's path.
|
6
|
+
|
7
|
+
# Where should we proxy this request to? Normally you can leave
|
8
|
+
# the host as 'localhost', but if you are using virtual hosts in
|
9
|
+
# your web server setup, you may need to be more specific. You
|
10
|
+
# will probably want Divergence to take over port 80 as well,
|
11
|
+
# so update your web application to run on a different port.
|
12
|
+
config.forward_host = 'localhost'
|
13
|
+
config.forward_port = 80
|
14
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Divergence
|
2
|
+
class Configuration
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_accessor :app_path, :git_path
|
6
|
+
attr_accessor :forward_host, :forward_port
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@git_path = nil
|
10
|
+
@app_path = nil
|
11
|
+
@forward_host = 'localhost'
|
12
|
+
@forward_port = 80
|
13
|
+
|
14
|
+
@callback_store = {}
|
15
|
+
@helpers = Divergence::Helpers.new(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Might get rid of realpath in the future because it
|
19
|
+
# resolves symlinks and that could be problematic
|
20
|
+
# with capistrano in case someone accidentally deploys.
|
21
|
+
def app_path=(p)
|
22
|
+
@app_path = File.realpath(p)
|
23
|
+
end
|
24
|
+
|
25
|
+
def git_path=(p)
|
26
|
+
@git_path = File.realpath(p)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Lets a user define a callback for a specific event
|
30
|
+
def callbacks(name, &block)
|
31
|
+
unless @callback_store.has_key?(name)
|
32
|
+
@callback_store[name] = []
|
33
|
+
end
|
34
|
+
|
35
|
+
@callback_store[name].push block
|
36
|
+
end
|
37
|
+
|
38
|
+
def callback(name, args = {})
|
39
|
+
return unless @callback_store.has_key?(name)
|
40
|
+
|
41
|
+
Application.log.debug "Execute callback: #{name.to_s}"
|
42
|
+
|
43
|
+
@callback_store[name].each do |cb|
|
44
|
+
@helpers.execute cb, args
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def each(&block)
|
49
|
+
instance_variables.each do |key|
|
50
|
+
if block_given?
|
51
|
+
block.call key, instance_variable_get(key)
|
52
|
+
else
|
53
|
+
yield instance_variable_get(key)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Divergence
|
2
|
+
class GitManager
|
3
|
+
attr_reader :current_branch
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@config = config
|
7
|
+
@app_path = config.app_path
|
8
|
+
@git_path = config.git_path
|
9
|
+
|
10
|
+
@log = Logger.new('./log/git.log')
|
11
|
+
@git = Git.open(@git_path, :log => @log)
|
12
|
+
|
13
|
+
@current_branch = @git.branch
|
14
|
+
@new_branch = false
|
15
|
+
end
|
16
|
+
|
17
|
+
def prepare_directory(branch, force=false)
|
18
|
+
return if is_current?(branch) and !force
|
19
|
+
pull branch
|
20
|
+
end
|
21
|
+
|
22
|
+
# Performs the swap between the git directory and the working
|
23
|
+
# app directory. We want to copy the files without copying
|
24
|
+
# the .git directory, but this is a temporary dumb solution.
|
25
|
+
#
|
26
|
+
# Future idea: try the capistrano route and simply symlink
|
27
|
+
# to the git directory instead of copying files.
|
28
|
+
#
|
29
|
+
# TODO: make this more ruby-like.
|
30
|
+
def swap!
|
31
|
+
return unless @new_branch
|
32
|
+
|
33
|
+
Dir.chdir @config.git_path do
|
34
|
+
@config.callback :before_swap
|
35
|
+
end
|
36
|
+
|
37
|
+
Application.log.info "Swap: #{@git_path} -> #{@app_path}"
|
38
|
+
`rsync -a --delete --exclude=.git #{@git_path}/* #{@app_path}`
|
39
|
+
@new_branch = false
|
40
|
+
|
41
|
+
Dir.chdir @config.app_path do
|
42
|
+
@config.callback :after_swap
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Since underscores are technically not allowed in URLs,
|
47
|
+
# but they are allowed in Git branch names, we have to do
|
48
|
+
# some magic to possibly convert dashes to underscores
|
49
|
+
# so we can load the right branch.
|
50
|
+
#
|
51
|
+
# Another possible thing to explore is converting all
|
52
|
+
# dashes in the URL to a regex search against all branches
|
53
|
+
# in this repository to avoid the current brute-force
|
54
|
+
# solution we're using.
|
55
|
+
def discover(branch)
|
56
|
+
return branch if is_branch?(branch)
|
57
|
+
|
58
|
+
Dir.chdir @git_path do
|
59
|
+
resp = @config.callback :on_branch_discover, branch
|
60
|
+
|
61
|
+
unless resp.nil?
|
62
|
+
return resp
|
63
|
+
end
|
64
|
+
|
65
|
+
local_search = "^" + branch.gsub(/-/, ".") + "$"
|
66
|
+
remote_search = "^remotes/origin/(" + branch.gsub(/-/, ".") + ")$"
|
67
|
+
local_r = Regexp.new(local_search, Regexp::IGNORECASE)
|
68
|
+
remote_r = Regexp.new(remote_search, Regexp::IGNORECASE)
|
69
|
+
|
70
|
+
`git branch -a`.split("\n").each do |b|
|
71
|
+
b = b.gsub('*', '').strip
|
72
|
+
|
73
|
+
return b if local_r.match(b)
|
74
|
+
if remote_r.match(b)
|
75
|
+
return remote_r.match(b)[1]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
raise "Unable to automatically detect branch. Given = #{branch}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def is_current?(branch)
|
84
|
+
@current_branch.to_s == branch
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def is_branch?(branch)
|
90
|
+
Dir.chdir @git_path do
|
91
|
+
# This is fast, but only works on locally checked out branches
|
92
|
+
`git show-ref --verify --quiet 'refs/heads/#{branch}'`
|
93
|
+
return true if $?.exitstatus == 0
|
94
|
+
|
95
|
+
# This is slow and will only get called for remote branches.
|
96
|
+
result = `git ls-remote --heads origin 'refs/heads/#{branch}'`
|
97
|
+
return result.strip.length != 0
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def pull(branch)
|
102
|
+
if checkout(branch)
|
103
|
+
Dir.chdir @config.git_path do
|
104
|
+
@config.callback :before_pull
|
105
|
+
end
|
106
|
+
|
107
|
+
#@git.pull 'origin', branch
|
108
|
+
@git.chdir do
|
109
|
+
# For some reason, I'm having issues with the pull
|
110
|
+
# that's built into the library. Doing this manually
|
111
|
+
# for now.
|
112
|
+
@log.info "git pull origin #{branch} 2>&1"
|
113
|
+
`git pull origin #{branch} 2>&1`
|
114
|
+
end
|
115
|
+
|
116
|
+
Dir.chdir @config.git_path do
|
117
|
+
@config.callback :after_pull
|
118
|
+
end
|
119
|
+
else
|
120
|
+
Dir.chdir @config.git_path do
|
121
|
+
@config.callback :on_pull_error
|
122
|
+
end
|
123
|
+
|
124
|
+
return false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def checkout(branch)
|
129
|
+
fetch
|
130
|
+
reset
|
131
|
+
|
132
|
+
begin
|
133
|
+
@git.checkout branch, :force => true
|
134
|
+
@current_branch = branch
|
135
|
+
@new_branch = true
|
136
|
+
rescue
|
137
|
+
return false
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def reset
|
142
|
+
@git.reset_hard('HEAD')
|
143
|
+
end
|
144
|
+
|
145
|
+
# Fetch all remote branch information
|
146
|
+
def fetch
|
147
|
+
@git.fetch
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Divergence
|
4
|
+
class Helpers
|
5
|
+
def initialize(config)
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute(block, opts={})
|
10
|
+
self.instance_exec opts, &block
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def bundle_install
|
16
|
+
Application.log.debug "bundle install"
|
17
|
+
|
18
|
+
Dir.chdir @config.app_path do
|
19
|
+
`bundle install`
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def restart_passenger
|
24
|
+
Application.log.debug "Restarting passenger..."
|
25
|
+
|
26
|
+
Dir.chdir @config.app_path do
|
27
|
+
begin
|
28
|
+
FileUtils.touch 'tmp/restart.txt'
|
29
|
+
rescue
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Divergence
|
2
|
+
class RequestParser
|
3
|
+
def initialize(env, git)
|
4
|
+
@req = Rack::Request.new(env)
|
5
|
+
@git = git
|
6
|
+
end
|
7
|
+
|
8
|
+
def raw
|
9
|
+
@req
|
10
|
+
end
|
11
|
+
|
12
|
+
def is_webhook?
|
13
|
+
subdomain == "divergence" and
|
14
|
+
@req.env['PATH_INFO'] == "/update" and
|
15
|
+
@req.post?
|
16
|
+
end
|
17
|
+
|
18
|
+
def host_parts
|
19
|
+
@req.host.split(".")
|
20
|
+
end
|
21
|
+
|
22
|
+
def has_subdomain?
|
23
|
+
host_parts.length > 2
|
24
|
+
end
|
25
|
+
|
26
|
+
def subdomain
|
27
|
+
if has_subdomain?
|
28
|
+
host_parts.shift
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def branch
|
35
|
+
if has_subdomain?
|
36
|
+
branch = subdomain
|
37
|
+
|
38
|
+
if branch['-']
|
39
|
+
@git.discover(branch)
|
40
|
+
else
|
41
|
+
branch
|
42
|
+
end
|
43
|
+
else
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def method_missing(meth, *args, &block)
|
49
|
+
raw.send(meth, *args)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Divergence
|
2
|
+
class Application < Rack::Proxy
|
3
|
+
# The main entry point for the application. This is caled
|
4
|
+
# by Rack.
|
5
|
+
def call(env)
|
6
|
+
@req = RequestParser.new(env, @g)
|
7
|
+
|
8
|
+
# First, lets find out what subdomain/git branch
|
9
|
+
# we're dealing with (if any).
|
10
|
+
unless @req.has_subdomain?
|
11
|
+
# No subdomain, simply proxy the request.
|
12
|
+
return proxy(env)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Handle webhooks from Github for updating the current
|
16
|
+
# branch if necessary.
|
17
|
+
if @req.is_webhook?
|
18
|
+
return Webhook.handle @g, @req
|
19
|
+
end
|
20
|
+
|
21
|
+
# Ask our GitManager to prepare the directory
|
22
|
+
# for the given branch.
|
23
|
+
result = @g.prepare_directory @req.branch
|
24
|
+
if result === false
|
25
|
+
return error!
|
26
|
+
end
|
27
|
+
|
28
|
+
# And then perform the codebase swap
|
29
|
+
@g.swap!
|
30
|
+
|
31
|
+
# We're finished, pass the request through.
|
32
|
+
proxy(env)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def proxy(env)
|
38
|
+
fix_environment!(env)
|
39
|
+
|
40
|
+
status, header, body = perform_request(env)
|
41
|
+
|
42
|
+
# This is super weird. Not sure why there is a status
|
43
|
+
# header coming through, but Rack::Lint complains about
|
44
|
+
# it, so we just remove it. I think this might be coming
|
45
|
+
# from Cloudfront (if you use it).
|
46
|
+
if header.has_key?('Status')
|
47
|
+
header.delete 'Status'
|
48
|
+
end
|
49
|
+
|
50
|
+
[status, header, body]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Sets the forwarding host for the request. This is where
|
54
|
+
# the proxy comes in.
|
55
|
+
def fix_environment!(env)
|
56
|
+
env["HTTP_HOST"] = "#{config.forward_host}:#{config.forward_port}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def error!
|
60
|
+
Application.log.error "Branch #{@req.branch} does not exist"
|
61
|
+
Application.log.error @req.raw
|
62
|
+
|
63
|
+
public_path = File.expand_path('../../../public', __FILE__)
|
64
|
+
file = File.open("#{public_path}/404.html", "r")
|
65
|
+
contents = file.read
|
66
|
+
file.close
|
67
|
+
|
68
|
+
[404, {"Content-Type" => "text/html"}, [contents]]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Divergence
|
2
|
+
class Webhook
|
3
|
+
def self.handle(git, req)
|
4
|
+
hook = JSON.parse(req['payload'])
|
5
|
+
branch = hook["ref"].split("/").last.strip
|
6
|
+
|
7
|
+
Application.log.info "Webhook: received for #{branch} branch"
|
8
|
+
|
9
|
+
# If the webhook is for the currently active branch,
|
10
|
+
# then we perform a pull and a swap.
|
11
|
+
if git.is_current?(branch)
|
12
|
+
Application.log.info "Webhook: updating #{branch}"
|
13
|
+
|
14
|
+
git.prepare_directory(branch, true)
|
15
|
+
git.swap!
|
16
|
+
|
17
|
+
ok
|
18
|
+
else
|
19
|
+
ignore
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.ok
|
24
|
+
[200, {"Content-Type" => "text/html"}, ["OK"]]
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.ignore
|
28
|
+
[200, {"Content-Type" => "text/html"}, ["IGNORE"]]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|