├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md └── hubsync.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /lib/bundler/man/ 26 | 27 | # for a library or gem, you might want to ignore these files since the code is 28 | # intended to run in multiple environments; otherwise, check them in: 29 | # Gemfile.lock 30 | # .ruby-version 31 | # .ruby-gemset 32 | 33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 34 | .rvmrc 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'git', '~> 1.3.0' 4 | gem 'octokit', '~> 4.3.0' 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.4.0) 5 | faraday (0.9.2) 6 | multipart-post (>= 1.2, < 3) 7 | git (1.3.0) 8 | multipart-post (2.0.0) 9 | octokit (4.3.0) 10 | sawyer (~> 0.7.0, >= 0.5.3) 11 | sawyer (0.7.0) 12 | addressable (>= 2.3.5, < 2.5) 13 | faraday (~> 0.8, < 0.10) 14 | 15 | PLATFORMS 16 | ruby 17 | 18 | DEPENDENCIES 19 | git (~> 1.3.0) 20 | octokit (~> 4.3.0) 21 | 22 | BUNDLED WITH 23 | 1.12.3 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Lars Schneider 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hubsync 2 | ======= 3 | 4 | Syncs all repositories of a user/organization on github.com to a user/organization of a GitHub Enterprise instance. 5 | 6 | ## Installation 7 | 8 | gem install bundler 9 | bundle install 10 | 11 | 12 | ## Usage 13 | 14 | ./hubsync.rb \ 15 | \ 16 | \ 17 | \ 18 | \ 19 | 20 | 21 | ## License 22 | 23 | hubsync is available under the MIT license. See the LICENSE file for more info. 24 | -------------------------------------------------------------------------------- /hubsync.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Syncs all repositories of a user/organization on github.com to a user/organization of a GitHub Enterprise instance. 4 | # 5 | # Usage: 6 | # ./hubsync.rb \ 7 | # \ 8 | # \ 9 | # \ 10 | # \ 11 | # \ 12 | # [] 13 | # 14 | # Note: 15 | # can be the name of one repository or a collection of repositories separated by "," 16 | # 17 | 18 | require 'rubygems' 19 | require 'bundler/setup' 20 | require 'octokit' 21 | require 'git' 22 | require 'fileutils' 23 | require 'timeout' 24 | 25 | 26 | module Git 27 | 28 | class Lib 29 | def clone(repository, name, opts = {}) 30 | @path = opts[:path] || '.' 31 | clone_dir = opts[:path] ? File.join(@path, name) : name 32 | 33 | arr_opts = [] 34 | arr_opts << "--bare" if opts[:bare] 35 | arr_opts << "--mirror" if opts[:mirror] 36 | arr_opts << "--recursive" if opts[:recursive] 37 | arr_opts << "-o" << opts[:remote] if opts[:remote] 38 | arr_opts << "--depth" << opts[:depth].to_i if opts[:depth] && opts[:depth].to_i > 0 39 | arr_opts << "--config" << opts[:config] if opts[:config] 40 | 41 | arr_opts << '--' 42 | arr_opts << repository 43 | arr_opts << clone_dir 44 | 45 | command('clone', arr_opts) 46 | 47 | opts[:bare] or opts[:mirror] ? {:repository => clone_dir} : {:working_directory => clone_dir} 48 | end 49 | 50 | def push(remote, branch = 'master', opts = {}) 51 | # Small hack to keep backwards compatibility with the 'push(remote, branch, tags)' method signature. 52 | opts = {:tags => opts} if [true, false].include?(opts) 53 | 54 | arr_opts = [] 55 | arr_opts << '--mirror' if opts[:mirror] 56 | arr_opts << '--force' if opts[:force] || opts[:f] 57 | arr_opts << remote 58 | 59 | if opts[:mirror] 60 | command('push', arr_opts) 61 | else 62 | command('push', arr_opts + [branch]) 63 | command('push', ['--tags'] + arr_opts) if opts[:tags] 64 | end 65 | end 66 | 67 | def remote_set_url(name, url, opts = {}) 68 | arr_opts = ['set-url'] 69 | arr_opts << '--push' if opts[:push] 70 | arr_opts << '--' 71 | arr_opts << name 72 | arr_opts << url 73 | 74 | command('remote', arr_opts) 75 | end 76 | end 77 | 78 | 79 | class Base 80 | def remote_set_url(name, url, opts = {}) 81 | url = url.repo.path if url.is_a?(Git::Base) 82 | self.lib.remote_set_url(name, url, opts) 83 | Git::Remote.new(self, name) 84 | end 85 | end 86 | end 87 | 88 | 89 | def init_github_clients(dotcom_token, enterprise_token, enterprise_url) 90 | clients = {} 91 | clients[:githubcom] = Octokit::Client.new(:access_token => dotcom_token, :auto_paginate => true) 92 | 93 | Octokit.configure do |c| 94 | c.api_endpoint = "#{enterprise_url}/api/v3" 95 | c.web_endpoint = "#{enterprise_url}" 96 | end 97 | 98 | clients[:enterprise] = Octokit::Client.new(:access_token => enterprise_token, :auto_paginate => true) 99 | return clients 100 | end 101 | 102 | 103 | def create_internal_repository(repo_dotcom, github, organization) 104 | puts "Repository `#{repo_dotcom.name}` not found on internal Github. Creating repository..." 105 | return github.create_repository( 106 | repo_dotcom.name, 107 | :organization => organization, 108 | :description => "This repository is automatically synced. Please push changes to #{repo_dotcom.clone_url}", 109 | :homepage => 'https://larsxschneider.github.io/2014/08/04/hubsync/', 110 | :has_issues => false, 111 | :has_wiki => false, 112 | :has_downloads => false, 113 | :default_branch => repo_dotcom.default_branch 114 | ) 115 | end 116 | 117 | 118 | def init_enterprise_repository(repo_dotcom, github, organization) 119 | repo_int_url = "#{organization}/#{repo_dotcom.name}" 120 | if github.repository? repo_int_url 121 | return github.repository(repo_int_url) 122 | else 123 | return create_internal_repository(repo_dotcom, github, organization) 124 | end 125 | end 126 | 127 | 128 | def init_local_repository(cache_path, repo_dotcom, repo_enterprise) 129 | FileUtils::mkdir_p cache_path 130 | repo_local_dir = "#{cache_path}/#{repo_enterprise.name}" 131 | 132 | if File.directory? repo_local_dir 133 | repo_local = Git.bare(repo_local_dir) 134 | else 135 | puts "Cloning `#{repo_dotcom.name}`..." 136 | 137 | repo_local = Git.clone( 138 | repo_dotcom.clone_url, 139 | repo_dotcom.name, 140 | :path => cache_path, 141 | :mirror => true 142 | ) 143 | repo_local.remote_set_url('origin', repo_enterprise.clone_url, :push => true) 144 | end 145 | return repo_local 146 | end 147 | 148 | 149 | # GitHub automatically creates special read only refs. They need to be removed to perform a successful push. 150 | # c.f. https://github.com/rtyley/bfg-repo-cleaner/issues/36 151 | def remove_github_readonly_refs(repo_local) 152 | file_lines = '' 153 | 154 | FileUtils.rm_rf(File.join(repo_local.repo.path, 'refs', 'pull')) 155 | 156 | IO.readlines(File.join(repo_local.repo.path, 'packed-refs')).map do |line| 157 | file_lines += line unless !(line =~ /^[0-9a-fA-F]{40} refs\/pull\/[0-9]+\/(head|pull|merge)/).nil? 158 | end 159 | 160 | File.open(File.join(repo_local.repo.path, 'packed-refs'), 'w') do |file| 161 | file.puts file_lines 162 | end 163 | end 164 | 165 | 166 | def sync(clients, dotcom_organization, enterprise_organization, repo_name, cache_path) 167 | clients[:githubcom].organization_repositories(dotcom_organization).each do |repo_dotcom| 168 | begin 169 | if (repo_name.nil? || (repo_name.split(",").include? repo_dotcom.name)) 170 | # The sync of each repository must not take longer than 15 min 171 | Timeout.timeout(60*15) do 172 | repo_enterprise = init_enterprise_repository(repo_dotcom, clients[:enterprise], enterprise_organization) 173 | 174 | puts "Syncing #{repo_dotcom.name}..." 175 | puts " Source: #{repo_dotcom.clone_url}" 176 | puts " Target: #{repo_enterprise.clone_url}" 177 | puts 178 | 179 | repo_enterprise.clone_url = repo_enterprise.clone_url.sub( 180 | 'https://', 181 | "https://#{clients[:enterprise].access_token}:x-oauth-basic@" 182 | ) 183 | repo_local = init_local_repository(cache_path, repo_dotcom, repo_enterprise) 184 | 185 | repo_local.remote('origin').fetch(:tags => true, :prune => true) 186 | remove_github_readonly_refs(repo_local) 187 | repo_local.push('origin', repo_dotcom.default_branch, :force => true, :mirror => true) 188 | end 189 | end 190 | rescue StandardError => e 191 | puts "Syncing #{repo_dotcom.name} FAILED!" 192 | puts e.message 193 | puts e.backtrace.inspect 194 | end 195 | end 196 | end 197 | 198 | 199 | if $0 == __FILE__ 200 | dotcom_organization = ARGV[0] 201 | dotcom_token = ARGV[1] 202 | enterprise_url = ARGV[2] 203 | enterprise_organization = ARGV[3] 204 | enterprise_token = ARGV[4] 205 | cache_path = ARGV[5] 206 | repo_name = ARGV[6] 207 | 208 | clients = init_github_clients(dotcom_token, enterprise_token, enterprise_url) 209 | while true do 210 | sleep(1) 211 | begin 212 | sync(clients, dotcom_organization, enterprise_organization, repo_name, cache_path) 213 | rescue SystemExit, Interrupt 214 | raise 215 | rescue Exception => e 216 | puts "Syncing FAILED!" 217 | puts e.message 218 | puts e.backtrace.inspect 219 | end 220 | end 221 | end 222 | --------------------------------------------------------------------------------