Skip to content
This repository has been archived by the owner on Jul 14, 2020. It is now read-only.

Commit

Permalink
Merge pull request #16 from gjtorikian/unbreak
Browse files Browse the repository at this point in the history
Unrbreak things
  • Loading branch information
benbalter committed Jan 28, 2015
2 parents cb808b4 + 28119bb commit f581bb2
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 179 deletions.
27 changes: 12 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,14 @@ Next, you'll need to set a few environment variables:
| `MACHINE_USER_EMAIL` | **Required**. The Git email address of your machine user.
| `MACHINE_USER_NAME` | **Required**. The Git author name of your machine user.

On your private repository, set a webhook to point to the `/sync?squash` endpoint.
Pass in one more parameter, `dest_repo`, the name of the public repository to update. It might look like `http://repository-sync.someserver.com/sync?squash&dest_repo=ourorg/public`. Don't forget to fill out the **Secret** field with your secret token!

On your private repository, set a webhook to point to the `/update_public` endpoint.
Pass in just one parameter, `dest_repo`, the name of the public repository to update. It might look like `http://repository-sync.someserver.com?dest_repo=ourorg/public`. Don't forget to fill out the **Secret** field with your secret token!
On your public repository, set a webhook to point to the `/sync` endpoint.
Pass in just one parameter, `dest_repo`, the name of the private repository to update. It might look like `http://repository-sync.someserver.com/sync?dest_repo=ourorg/private`. Don't forget to fill out the **Secret** field with your secret token!

On your public repository, set a webhook to point to the `/update_private` endpoint.
Pass in just one parameter, `dest_repo`, the name of the private repository to update. It might look like `http://repository-sync.someserver.com?dest_repo=ourorg/private`. Don't forget to fill out the **Secret** field with your secret token!

You'll notice these two are practically the same. They are! The only difference is
that, whilst updating a public repository, this tool will `--squash merge` to hide
the commit history.
You'll notice these two endpoints are practically the same. They are! The only difference is
that, when hitting an endpoint with the `squash` path parameter, this tool will perform a `--squash merge` to hide the commit history.

### Between a github.com repository and a GitHub Enterprise repository

Expand All @@ -45,20 +43,19 @@ Next, you'll need to set a few environment variables:
| `MACHINE_USER_EMAIL` | **Required**. The Git email address of your machine user.
| `MACHINE_USER_NAME` | **Required**. The Git author name of your machine user.

On your private repository, set a webhook to point to the `/update_public` endpoint.
Pass in just one parameter, `dest_repo`, the name of the public repository to update. It might look like `http://repository-sync.someserver.com?dest_repo=ourorg/public`. Don't forget to fill out the **Secret** field with your secret token!
On your private repository, set a webhook to point to the `/sync` endpoint.
Pass in just one parameter, `dest_repo`, the name of the public repository to update. It might look like `http://repository-sync.someserver.com/sync?squash&dest_repo=ourorg/public`. Don't forget to fill out the **Secret** field with your secret token!

On your public repository, set a webhook to point to the `/update_private` endpoint.
On your public repository, set a webhook to point to the `/sync` endpoint.
Pass in two parameters:

* `dest_repo`, the name of the private repository to update
* `hostname`, the hostname of your GitHub Enterprise installation

It might look like `http://repository-sync.someserver.com?dest_repo=ourorg/private&hostname=our.ghe.io`. Don't forget to fill out the **Secret** field with your secret token!
It might look like `http://repository-sync.someserver.com/sync?dest_repo=ourorg/private&hostname=our.ghe.io`. Don't forget to fill out the **Secret** field with your secret token!

You'll notice these two are practically the same. They are! The only difference is
that, whilst updating a public repository, this tool will `--squash merge` to hide
the commit history.
You'll notice these two endpoints are practically the same. They are! The only difference is
that, when hitting an endpoint with the `squash` path parameter, this tool will perform a `--squash merge` to hide the commit history.

## Customizing messaging

Expand Down
23 changes: 8 additions & 15 deletions lib/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ class RepositorySync < Sinatra::Base
configure_redis
end

before do
get '/' do
'You\'ll want to make a POST to /sync. Check the documentation for more info.'
end

post '/sync' do
# trim trailing slashes
request.path_info.sub!(/\/$/, '')
pass unless %w(update_public update_private).include? request.path_info.split('/')[1]

# ensure there's a payload
request.body.rewind
Expand All @@ -41,23 +44,13 @@ class RepositorySync < Sinatra::Base
@payload = JSON.parse(payload_body)
halt 202, "Payload was not for master, was for #{@payload['ref']}, aborting." unless master_branch?(@payload)

@squash = params[:squash]

# keep some important vars
process_payload(@payload)
@destination_hostname = params[:destination_hostname] || 'github.com'
end

get '/' do
'I think you misunderstand how to use this.'
end

post '/update_public' do
do_the_work(true)
'Processing...'
end

post '/update_private' do
do_the_work(false)
'Processing...'
Resque.enqueue(CloneJob, @after_sha, @destination_hostname, @destination_repo, @originating_hostname, @originating_repo, @squash)
end

helpers Helpers
Expand Down
7 changes: 2 additions & 5 deletions lib/clone_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,15 @@
class CloneJob
@queue = :default

def self.perform(tmpdir, after_sha, dotcom_token, ghe_token, destination_hostname, destination_repo, originating_hostname, originating_repo, is_public)
def self.perform(after_sha, destination_hostname, destination_repo, originating_hostname, originating_repo, squash)

cloner = Cloner.new({
:tmpdir => tmpdir,
:after_sha => after_sha,
:dotcom_token => dotcom_token,
:ghe_token => ghe_token,
:destination_hostname => destination_hostname,
:destination_repo => destination_repo,
:originating_hostname => originating_hostname,
:originating_repo => originating_repo,
:public => is_public
:squash => squash
})

cloner.clone
Expand Down
137 changes: 89 additions & 48 deletions lib/cloner.rb
Original file line number Diff line number Diff line change
@@ -1,51 +1,76 @@
require "open3"
require 'open3'

class Cloner
GITHUB_DOMAIN = 'github.com'

DEFAULTS = {
:tmpdir => Dir.mktmpdir("repository-sync"),
:tmpdir => nil,
:after_sha => nil,
:dotcom_token => nil,
:ghe_token => nil,
:destination_hostname => "github.com",
:squash => nil,
:destination_hostname => GITHUB_DOMAIN,
:destination_repo => nil,
:originating_hostname => "github.com",
:originating_hostname => GITHUB_DOMAIN,
:originating_repo => nil,
:public => false,
:git => nil
}

attr_accessor :tmpdir, :after_sha, :dotcom_token, :ghe_token, :destination_hostname
attr_accessor :destination_repo, :originating_hostname, :originating_repo, :public
alias_method :public?, :public
attr_accessor :tmpdir, :after_sha, :destination_hostname, :destination_repo
attr_accessor :originating_hostname, :originating_repo, :squash
alias_method :squash?, :squash

def initialize(options)
logger.level = Logger::WARN if ENV['RACK_ENV'] == 'test'
logger.info 'New Cloner instance initialized'

DEFAULTS.each { |key,value| instance_variable_set("@#{key}", options[key] || value) }
@tmpdir ||= Dir.mktmpdir("repository-sync")

if destination_hostname != 'github.com'
if destination_hostname != GITHUB_DOMAIN
Octokit.configure do |c|
c.api_endpoint = "https://#{destination_hostname}/api/v3/"
c.web_endpoint = "https://#{destination_hostname}"
end
end

git.config('user.name', ENV['MACHINE_USER_NAME'])
git.config('user.email', ENV['MACHINE_USER_EMAIL'])
git_init

DEFAULTS.each { |key, _| logger.info " * #{key}: #{instance_variable_get("@#{key}")}" }
end

def clone
Dir.chdir "#{tmpdir}/#{destination_repo}" do
add_remote
fetch
merge
push
create_pull_request
delete_branch
Bundler.with_clean_env do
Dir.chdir "#{tmpdir}/#{destination_repo}" do
add_remote
fetch
merge
push
create_pull_request
delete_branch
logger.info 'fin'
end
end
rescue StandardError => e
logger.warn e
raise
ensure
FileUtils.rm_rf(tmpdir)
logger.info "Cleaning up #{tmpdir}"
end

def originating_token
@originating_token ||= (originating_hostname == GITHUB_DOMAIN ? dotcom_token : ghe_token)
end

def token
@token ||= (destination_hostname == 'github.com' ? dotcom_token : ghe_token)
def destination_token
@destination_token ||= (destination_hostname == GITHUB_DOMAIN ? dotcom_token : ghe_token)
end

def dotcom_token
ENV['DOTCOM_MACHINE_USER_TOKEN']
end

def ghe_token
ENV['GHE_MACHINE_USER_TOKEN']
end

def remote_name
Expand All @@ -68,22 +93,33 @@ def files
@files ||= client.compare(destination_repo, 'master', branch_name)['files']
end

def clone_url_with_token
@clone_url_with_token ||= "https://#{token}:x-oauth-basic@#{originating_hostname}/#{originating_repo}.git"
def url_with_token(remote = :destination)
token = (remote == :destination) ? destination_token : originating_token
hostname = (remote == :destination) ? destination_hostname : originating_hostname
repo = (remote == :destination) ? destination_repo : originating_repo
"https://#{token}:x-oauth-basic@#{hostname}/#{repo}.git"
end

def originating_url_with_token
@originating_url_with_token ||= url_with_token(:originating)
end

def destination_url_with_token
@destination_url_with_token ||= url_with_token(:destination)
end

def pull_request_title
if files.count == 1
"#{files.first["status"].capitalize} #{files.first["filename"]}"
"#{files.first['status'].capitalize} #{files.first['filename']}"
else
ENV["#{safe_destination_repo}_PR_TITLE"] || 'Sync changes from upstream repository'
end
end

def pull_request_body
return ENV["#{safe_destination_repo}_PR_BODY"] if ENV["#{safe_destination_repo}_PR_BODY"]
body = ""
["added", "removed", "unchanged"].each do |type|
body = ''
%w(added removed unchanged).each do |type|
filenames = files.select { |f| f['status'] == type }.map { |f| f['filename'] }
body << "### #{type.capitalize} files: \n\n* #{filenames.join("\n* ")}\n\n" unless filenames.empty?
end
Expand All @@ -97,23 +133,26 @@ def logger
end

def client
@client ||= Octokit::Client.new(:access_token => token)
@client ||= Octokit::Client.new(:access_token => destination_token)
end

def git
@git ||= begin
logger.info "Cloning #{destination_repo} from #{destination_hostname}..."
Git.clone(clone_url_with_token, "#{tmpdir}/#{destination_repo}")
Git.clone(destination_url_with_token, "#{tmpdir}/#{destination_repo}")
end
end

def run_command(*args)
logger.info "Running command #{args.join(" ")}"
logger.info "Running command #{args.join(' ')}"
output = status = nil
output, status = Open3.capture2e(*args)
output = output.gsub(/#{dotcom_token}/, '<TOKEN>') if dotcom_token
output = output.gsub(/#{ghe_token}/, '<TOKEN>') if ghe_token
logger.info "Result: #{output}"
if status != 0
report_error(output)
raise "Command `#{args.join(" ")}` failed: #{output}"
fail "Command `#{args.join(' ')}` failed: #{output}"
end
output
end
Expand All @@ -127,46 +166,48 @@ def report_error(command_output)
body << "\n```\n"
body << "You'll have to resolve this problem manually, I'm afraid.\n"
body << "![I'm so sorry](http://media.giphy.com/media/NxKcqJI6MdIgo/giphy.gif)"
client.create_issue originating_repo, "Merge conflict detected", body
client.create_issue originating_repo, 'Merge conflict detected', body
end

# Methods that perform sync actions, in order

def git_init
git.config('user.name', ENV['MACHINE_USER_NAME'])
git.config('user.email', ENV['MACHINE_USER_EMAIL'])
end

def add_remote
logger.info "Adding remote for #{originating_repo} on #{originating_hostname}..."
git.add_remote(remote_name, clone_url_with_token)
git.add_remote(remote_name, originating_url_with_token)
end

def fetch
logger.info "Fetching #{originating_repo}..."
git.remote(remote_name).fetch
git.branch(branch_name).checkout
end

def merge
public_note = public? ? '(is public)' : ''
logger.info "Merging #{originating_repo}/master into #{remote_name} #{public_note}..."
if public?
output = run_command('git', 'merge', '--squash', "#{remote_name}/master")
logger.info "Checking out #{branch_name}"
git.branch(branch_name).checkout

logger.info "Merging #{originating_repo}/master into #{branch_name}..."
if squash?
logger.info 'Squashing!'
run_command('git', 'merge', '--squash', "#{remote_name}/master")
git.commit(commit_message)
else
output = run_command('git', 'merge', "#{remote_name}/master")
end
rescue Git::GitExecuteError => e
if e.message =~ /nothing to commit/
return nil, "#{e.message}"
else
raise
logger.info 'Not squashing!'
run_command('git', 'merge', "#{remote_name}/master")
end
end

def push
logger.info "Pushing to origin..."
run_command(['git', 'push', 'origin', branch_name])
logger.info 'Pushing to origin...'
run_command('git', 'push', 'origin', branch_name)
end

def create_pull_request
return logger.warn "No files have changed" if files.empty?
return logger.warn 'No files have changed' if files.empty?

pr = client.create_pull_request(
destination_repo,
Expand Down
23 changes: 0 additions & 23 deletions lib/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,7 @@ def process_payload(payload)
@after_sha = payload['after']
end

def dotcom_token
ENV['DOTCOM_MACHINE_USER_TOKEN']
end

def ghe_token
ENV['GHE_MACHINE_USER_TOKEN']
end

def master_branch?(payload)
payload['ref'] == 'refs/heads/master'
end

def do_the_work(is_public)
in_tmpdir do |tmpdir|
Resque.enqueue(CloneJob, tmpdir, @after_sha, dotcom_token, ghe_token, @destination_hostname, @destination_repo, @originating_hostname, @originating_repo, is_public)
end
end

def in_tmpdir
path = File.expand_path "#{Dir.tmpdir}/repository-sync/repos/#{Time.now.to_i}#{rand(1000)}/"
FileUtils.mkdir_p path
puts "Directory created at: #{path}"
yield path
ensure
FileUtils.rm_rf(path) if File.exist?(path) && !Sinatra::Base.development?
end
end
Loading

0 comments on commit f581bb2

Please sign in to comment.