├── .emacs-project ├── .gitignore ├── .rspec ├── LICENSE ├── README.rdoc ├── Rakefile ├── UPGRADING.rdoc ├── VERSION.yml ├── bin └── giternal ├── features ├── checking_out_externals.feature ├── freeze_externals.feature ├── steps │ └── repository_steps.rb └── unfreeze_externals.feature ├── giternal.gemspec ├── lib ├── giternal.rb └── giternal │ ├── app.rb │ ├── repository.rb │ ├── version.rb │ └── yaml_config.rb ├── spec ├── giternal │ ├── app_spec.rb │ ├── repository_spec.rb │ └── yaml_config_spec.rb ├── giternal_helper.rb └── spec_helper.rb └── test_trackers.rb /.emacs-project: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patmaddox/giternal/1858347d5fdf1f5043821620afa9efd5498503ac/.emacs-project -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | spec/test_repos 2 | pkg 3 | features/tmp 4 | test_repos 5 | .idea 6 | .rvmrc 7 | *.gem 8 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Pat Maddox 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = giternal is *unmaintained* 2 | 3 | If you want the functionality that giternal provides, but in a tool that's being maintained, you should consider: 4 | 5 | * http://myrepos.branchable.com 6 | * https://github.com/RichiH/vcsh 7 | * forking it 8 | 9 | Thanks to @jwhitley for the suggested alternatives. 10 | 11 | = Installation 12 | 13 | `gem install giternal` 14 | 15 | = Why giternal 16 | 17 | There are a couple tools out there that keep track of git externals 18 | for you. Git submodules are built in, and braid is a different 19 | project. They both have problems that prevent them from using those 20 | externals collaboratively. 21 | 22 | In a nutshell, git submodules keep a reference to the head of each 23 | external project. This means that if Joe and Sarah each make 24 | non-conflicting changes to their externals, and push the external 25 | reference in the main project, one of them will get a conflict on 26 | update. Braid doesn't treat the externals as being separate from the 27 | main project, so any commits you make will go to the parent instead of 28 | the external. 29 | 30 | In order to demonstrate these issues more concretely, I've written a 31 | script that will simulate the workflow of making changes to an 32 | external, pushing it upstream, and pulling it into another project. 33 | You'll notice in the submodule example that there's a conflict when 34 | updating the main repo, and files are missing in the local external 35 | after update. In the braid example, the changes never make it 36 | upstream. This script checks to see if a tool allows you to not only 37 | track dependencies but collaborate as well. To execute it, run 38 | 39 | ruby test_trackers.rb giternal|submodules|braid 40 | 41 | = Using it 42 | 43 | Put a file in your project named .giternal.yml or config/giternal.yml, 44 | that looks like this: 45 | 46 | local_dir_name: 47 | repo: git://path/to/repo.git 48 | path: local/sub/dir 49 | 50 | As an example, here's how you'd track rspec: 51 | 52 | rspec: 53 | repo: git://github.com/dchelimsky/rspec.git 54 | path: vendor/plugins 55 | 56 | To pull the externals into your workspace, run "giternal update". You 57 | should add vendor/plugins/rspec to .gitignore to keep the files from 58 | being added to your main repo. 59 | 60 | = Deploying externals 61 | 62 | I frequently use a cap task that changes to deploy_root and runs 63 | "giternal update" to pull all the externals. The downside with this 64 | approach is that you'll get the bleeding edge of your external, and 65 | you may want to use a particular version that you've tested and know 66 | works with your app. Enter freezing. 67 | 68 | Make sure your working dir is clean and then run "giternal freeze". 69 | The externals are no longer separate repos - the history was zipped up 70 | so that the external can be unfrozen later. Each external is added to 71 | the git index, so all you have to do is commit. You've got a 72 | self-contained external that is frozen to a working version, suitable 73 | for deploy. 74 | 75 | After you've tagged your release, you can unfreeze the giternal with 76 | "giternal unfreeze" and get back to development. 77 | 78 | = How I want to work with externals 79 | 80 | When tracking externals, the most important thing is knowing my 81 | libraries work together, and second I can try to stay up to date. 82 | When I update an external, I would run rake. If it passes, keep that 83 | version. If not, look at what it would take to take for my code and 84 | the library code to work. If I feel like I can do it, I do, otherwise 85 | I unfreeze and stay on the older working version. 86 | 87 | = THANK YOUs 88 | 89 | Rollcall of awesome people who have contributed to giternal: 90 | 91 | Brian Takita & Honkster Team - ability to specify individual repos for 92 | freeze/unfreeze/update 93 | 94 | 95 | == Copyright 96 | 97 | Copyright (c) 2009-2011 Pat Maddox. See LICENSE for details. 98 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "#{ENV["GEM_PREFIX"]}giternal" 8 | gem.summary = %Q{Non-sucky git externals} 9 | gem.description = %Q{Giternal provides dead-simple management of external git dependencies. It only stores a small bit of metadata, letting you actively develop in any of the repos. Come deploy time, you can easily freeze freeze all the dependencies to particular versions} 10 | gem.email = "pat.maddox@gmail.com" 11 | gem.homepage = "http://github.com/patmaddox/giternal" 12 | gem.authors = ["Pat Maddox"] 13 | gem.add_development_dependency "rspec", "~> 2" 14 | gem.add_development_dependency "cucumber", "~> 1" 15 | gem.post_install_message = "** IMPORTANT - Please see UPGRADING.rdoc for important changes **" 16 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 17 | end 18 | rescue LoadError 19 | puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" 20 | end 21 | 22 | require 'rspec/core/rake_task' 23 | RSpec::Core::RakeTask.new(:spec) 24 | 25 | task :spec => :check_dependencies 26 | 27 | begin 28 | require 'cucumber/rake/task' 29 | Cucumber::Rake::Task.new(:features) 30 | 31 | task :features => :check_dependencies 32 | rescue LoadError 33 | task :features do 34 | abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber" 35 | end 36 | end 37 | 38 | task :default => [:spec, :features] 39 | 40 | #Rake::RDocTask.new do |rdoc| 41 | # if File.exist?('VERSION') 42 | # version = File.read('VERSION') 43 | # else 44 | # version = "" 45 | # end 46 | # 47 | # rdoc.rdoc_dir = 'rdoc' 48 | # rdoc.title = "giternal #{version}" 49 | # rdoc.rdoc_files.include('README*') 50 | # rdoc.rdoc_files.include('lib/**/*.rb') 51 | #end 52 | -------------------------------------------------------------------------------- /UPGRADING.rdoc: -------------------------------------------------------------------------------- 1 | # Upgrading giternal 2 | 3 | ## 0.1.0 -> * 4 | 5 | 0.1.0 has a nasty bug that corrupts a checked out external's .git 6 | repository. This happens because giternal sorts the files in .git before piping 7 | them to tar, in order to minimize repo bloat. Unfortunately, tar doesn't seem to 8 | like that, and so when unfreezing the repository it loses many of the files in 9 | .git/objects. Instant repository corruption. 10 | 11 | ### What you can do 12 | 13 | The best thing to do is probably to wipe your external dir and do a giternal 14 | update. This will check out a fresh copy of the dependency, along with its 15 | uncorrupted git repo. 16 | 17 | I don't actually think there's anything else you can do. Sorry :( -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :major: 0 3 | :minor: 1 4 | :patch: 1 5 | :build: 6 | -------------------------------------------------------------------------------- /bin/giternal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | if File.exist?(File.dirname(__FILE__) + '/../lib/giternal.rb') 4 | $:.unshift(File.dirname(__FILE__) + '/../lib') 5 | end 6 | require 'giternal' 7 | 8 | action = ARGV[0] 9 | available_actions = %w(update freeze unfreeze) 10 | unless available_actions.include?(action) 11 | puts "Usage: giternal (#{available_actions.join(':')})" 12 | puts "" 13 | puts File.read(File.dirname(__FILE__) + '/../README.rdoc') 14 | exit 1 15 | end 16 | 17 | Giternal::Repository.verbose = true 18 | app = Giternal::App.new(FileUtils.pwd) 19 | app.run(*ARGV) 20 | -------------------------------------------------------------------------------- /features/checking_out_externals.feature: -------------------------------------------------------------------------------- 1 | Feature: Checking out and updating externals 2 | As a developer 3 | I want to check out and update external projects via git 4 | So that I can add functionality to my app with little effort 5 | 6 | Scenario: Repository is not yet checked out 7 | Given an external repository named 'first_external' 8 | And 'first_external' is not yet checked out 9 | When I update the externals 10 | Then 'first_external' should be checked out 11 | 12 | Scenario: Multiple externals 13 | Given an external repository named 'first_external' 14 | And an external repository named 'second_external' 15 | When I update the externals 16 | Then 'first_external' should be checked out 17 | And 'second_external' should be checked out 18 | 19 | Scenario: Repository checked out then updated 20 | Given an external repository named 'first_external' 21 | And the externals are up to date 22 | And content is added to 'first_external' 23 | Then 'first_external' should not be up to date 24 | When I update the externals 25 | Then 'first_external' should be up to date 26 | 27 | Scenario: Two Repositories checked out and one updated 28 | Given an external repository named 'first_external' 29 | And an external repository named 'second_external' 30 | And the externals are up to date 31 | And content is added to 'first_external' 32 | And content is added to 'second_external' 33 | Then 'first_external' should not be up to date 34 | And 'second_external' should not be up to date 35 | When I update the external 'second_external' 36 | Then 'first_external' should not be up to date 37 | And 'second_external' should be up to date 38 | 39 | Scenario: One repo frozen, one repo unfrozen, and all updated 40 | Given an external repository named 'first_external' 41 | And an external repository named 'second_external' 42 | And the externals are up to date 43 | And the externals are frozen 44 | And content is added to 'first_external' 45 | And content is added to 'second_external' 46 | Then 'first_external' should not be up to date 47 | And 'second_external' should not be up to date 48 | When I unfreeze the external 'second_external' 49 | And I update the externals 50 | Then 'first_external' should not be up to date 51 | And 'second_external' should be up to date 52 | -------------------------------------------------------------------------------- /features/freeze_externals.feature: -------------------------------------------------------------------------------- 1 | Feature: Freeze externals 2 | As a developer 3 | I want to freeze externals 4 | So that I can test and deploy my app with no worries 5 | 6 | Scenario: Main project has one external 7 | Given an external repository named 'first_external' 8 | And the externals are up to date 9 | When I freeze the externals 10 | Then 'first_external' should no longer be a git repo 11 | And 'first_external' should be added to the commit index 12 | 13 | Scenario: External has been added to .gitignore 14 | Given an external repository named 'first_external' 15 | And the external 'first_external' has been added to .gitignore 16 | And the externals are up to date 17 | When I freeze the externals 18 | Then 'first_external' should be added to the commit index 19 | 20 | Scenario: Main project has two externals 21 | Given an external repository named 'first_external' 22 | And an external repository named 'second_external' 23 | And the externals are up to date 24 | When I freeze the external 'second_external' 25 | Then 'second_external' should be added to the commit index 26 | And 'first_external' should be removed from the commit index 27 | -------------------------------------------------------------------------------- /features/steps/repository_steps.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | $:.unshift(File.dirname(__FILE__) + '/../../lib') 3 | require 'giternal' 4 | $:.unshift(File.dirname(__FILE__) + '/../../spec') 5 | require 'giternal_helper' 6 | 7 | RSpec::Matchers.define :be_up_to_date do 8 | match do |actual_repo_name| 9 | File.directory?(GiternalHelper.checked_out_path(actual_repo_name)) && 10 | GiternalHelper.repo_contents(GiternalHelper.checked_out_path(actual_repo_name)) == 11 | GiternalHelper.repo_contents(GiternalHelper.external_path(actual_repo_name)) 12 | end 13 | end 14 | 15 | RSpec::Matchers.define :be_a_git_repo do 16 | match do |actual_repo_name| 17 | File.directory?(GiternalHelper.checked_out_path(actual_repo_name) + '/.git') 18 | end 19 | end 20 | 21 | RSpec::Matchers.define :be_added_to_commit_index do 22 | match do |actual_repo_name| 23 | Dir.chdir(GiternalHelper.tmp_path + '/main_repo') do 24 | status = `git status` 25 | flattened_status = status.split("\n").join(" ") 26 | to_be_committed_regex = /new file:\W+dependencies\/#{actual_repo_name}/ 27 | untracked_files_regex = /Untracked files:.*#{actual_repo_name}/ 28 | status =~ to_be_committed_regex && !(flattened_status =~ untracked_files_regex) 29 | end 30 | end 31 | end 32 | 33 | Before do 34 | GiternalHelper.clean! 35 | GiternalHelper.create_main_repo 36 | end 37 | 38 | After do 39 | GiternalHelper.clean! 40 | end 41 | 42 | Given /an external repository named '(.*)'/ do |repo_name| 43 | GiternalHelper.create_repo repo_name 44 | GiternalHelper.add_content repo_name 45 | end 46 | 47 | Given /'(.*)' is not yet checked out/ do |repo_name| 48 | # TODO: Figure out why I can't use should be_false here 49 | File.directory?(GiternalHelper.checked_out_path(repo_name)).should == false 50 | end 51 | 52 | Given "the externals are up to date" do 53 | GiternalHelper.update_externals 54 | end 55 | 56 | Given "the externals are frozen" do 57 | GiternalHelper.freeze_externals 58 | end 59 | 60 | Given /content is added to '(.*)'/ do |repo_name| 61 | GiternalHelper.add_content(repo_name) 62 | end 63 | 64 | Given /^the external '(.*)' has been added to \.gitignore$/ do |repo_name| 65 | GiternalHelper.add_external_to_ignore(repo_name) 66 | end 67 | 68 | When "I update the externals" do 69 | GiternalHelper.update_externals 70 | end 71 | 72 | When /I update the external '(.*)'/ do |external_name| 73 | GiternalHelper.update_externals("dependencies/#{external_name}") 74 | end 75 | 76 | When "I freeze the externals" do 77 | GiternalHelper.freeze_externals 78 | end 79 | 80 | When /I freeze the external '(.*)'/ do |external_name| 81 | GiternalHelper.freeze_externals("dependencies/#{external_name}") 82 | end 83 | 84 | When "I unfreeze the externals" do 85 | GiternalHelper.unfreeze_externals 86 | end 87 | 88 | When /I unfreeze the external '(.*)'/ do |external_name| 89 | GiternalHelper.unfreeze_externals("dependencies/#{external_name}") 90 | end 91 | 92 | Then /'(.*)' should be checked out/ do |repo_name| 93 | repo_name.should be_up_to_date 94 | end 95 | 96 | Then /'(.*)' should be up to date/ do |repo_name| 97 | repo_name.should be_up_to_date 98 | end 99 | 100 | Then /'(.*)' should not be up to date/ do |repo_name| 101 | repo_name.should_not be_up_to_date 102 | end 103 | 104 | Then /'(.*)' should no longer be a git repo/ do |repo_name| 105 | repo_name.should_not be_a_git_repo 106 | end 107 | 108 | Then /'(.*)' should be a git repo/ do |repo_name| 109 | repo_name.should be_a_git_repo 110 | end 111 | 112 | Then /'(.*)' should be added to the commit index/ do |repo_name| 113 | repo_name.should be_added_to_commit_index 114 | end 115 | 116 | Then /'(.*)' should be removed from the commit index/ do |repo_name| 117 | repo_name.should_not be_added_to_commit_index 118 | end 119 | -------------------------------------------------------------------------------- /features/unfreeze_externals.feature: -------------------------------------------------------------------------------- 1 | Feature: Unfreeze externals 2 | As a developer 3 | I want to unfreeze externals 4 | So that I can continue to update and develop on them 5 | 6 | Scenario: Main project has one frozen external 7 | Given an external repository named 'first_external' 8 | And the externals are up to date 9 | And the externals are frozen 10 | When I unfreeze the externals 11 | Then 'first_external' should be a git repo 12 | And 'first_external' should be removed from the commit index 13 | 14 | Scenario: Main project has two frozen externals 15 | Given an external repository named 'first_external' 16 | And an external repository named 'second_external' 17 | And the externals are up to date 18 | And the externals are frozen 19 | When I unfreeze the external 'second_external' 20 | Then 'second_external' should be a git repo 21 | And 'second_external' should be removed from the commit index 22 | And 'first_external' should no longer be a git repo 23 | And 'first_external' should be added to the commit index 24 | -------------------------------------------------------------------------------- /giternal.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{giternal} 8 | s.version = "0.1.1" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Pat Maddox"] 12 | s.date = %q{2011-06-24} 13 | s.description = %q{Giternal provides dead-simple management of external git dependencies. It only stores a small bit of metadata, letting you actively develop in any of the repos. Come deploy time, you can easily freeze freeze all the dependencies to particular versions} 14 | s.email = %q{pat.maddox@gmail.com} 15 | s.executables = ["giternal"] 16 | s.extra_rdoc_files = [ 17 | "LICENSE", 18 | "README.rdoc" 19 | ] 20 | s.files = [ 21 | ".emacs-project", 22 | ".rspec", 23 | "LICENSE", 24 | "README.rdoc", 25 | "Rakefile", 26 | "UPGRADING.rdoc", 27 | "VERSION.yml", 28 | "bin/giternal", 29 | "features/checking_out_externals.feature", 30 | "features/freeze_externals.feature", 31 | "features/steps/repository_steps.rb", 32 | "features/unfreeze_externals.feature", 33 | "giternal.gemspec", 34 | "lib/giternal.rb", 35 | "lib/giternal/app.rb", 36 | "lib/giternal/repository.rb", 37 | "lib/giternal/version.rb", 38 | "lib/giternal/yaml_config.rb", 39 | "spec/giternal/app_spec.rb", 40 | "spec/giternal/repository_spec.rb", 41 | "spec/giternal/yaml_config_spec.rb", 42 | "spec/giternal_helper.rb", 43 | "spec/spec_helper.rb", 44 | "test_trackers.rb" 45 | ] 46 | s.homepage = %q{http://github.com/patmaddox/giternal} 47 | s.post_install_message = %q{** IMPORTANT - Please see UPGRADING.rdoc for important changes **} 48 | s.require_paths = ["lib"] 49 | s.rubygems_version = %q{1.7.2} 50 | s.summary = %q{Non-sucky git externals} 51 | 52 | if s.respond_to? :specification_version then 53 | s.specification_version = 3 54 | 55 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 56 | s.add_development_dependency(%q, ["~> 2"]) 57 | s.add_development_dependency(%q, ["~> 1"]) 58 | else 59 | s.add_dependency(%q, ["~> 2"]) 60 | s.add_dependency(%q, ["~> 1"]) 61 | end 62 | else 63 | s.add_dependency(%q, ["~> 2"]) 64 | s.add_dependency(%q, ["~> 1"]) 65 | end 66 | end 67 | 68 | -------------------------------------------------------------------------------- /lib/giternal.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__)) unless 2 | $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) 3 | 4 | module Giternal 5 | 6 | end 7 | 8 | require 'giternal/repository' 9 | require 'giternal/yaml_config' 10 | require 'giternal/app' 11 | -------------------------------------------------------------------------------- /lib/giternal/app.rb: -------------------------------------------------------------------------------- 1 | module Giternal 2 | class App 3 | def initialize(base_dir) 4 | @base_dir = base_dir 5 | end 6 | 7 | def update(*dirs) 8 | if dirs.empty? 9 | config.each_repo {|r| r.update } 10 | else 11 | dirs.each do |dir| 12 | if repo = config.find_repo(dir) 13 | repo.update 14 | end 15 | end 16 | end 17 | end 18 | 19 | def freezify(*dirs) 20 | if dirs.empty? 21 | config.each_repo {|r| r.freezify } 22 | else 23 | dirs.each do |dir| 24 | if repo = config.find_repo(dir) 25 | repo.freezify 26 | end 27 | end 28 | end 29 | end 30 | 31 | def unfreezify(*dirs) 32 | if dirs.empty? 33 | config.each_repo {|r| r.unfreezify } 34 | else 35 | dirs.each do |dir| 36 | if repo = config.find_repo(dir) 37 | repo.unfreezify 38 | end 39 | end 40 | end 41 | end 42 | 43 | def run(action, *args) 44 | case action 45 | when "freeze" 46 | freezify(*args) 47 | when "unfreeze" 48 | unfreezify(*args) 49 | else 50 | send(action, *args) 51 | end 52 | end 53 | 54 | def config 55 | return @config if @config 56 | 57 | config_file = ['config/giternal.yml', '.giternal.yml'].detect do |file| 58 | File.file? File.expand_path(@base_dir + '/' + file) 59 | end 60 | 61 | if config_file.nil? 62 | $stderr.puts "config/giternal.yml is missing" 63 | exit 1 64 | end 65 | 66 | @config = YamlConfig.new(@base_dir, File.read(config_file)) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/giternal/repository.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Giternal 4 | class Repository 5 | class << self 6 | attr_accessor :verbose 7 | end 8 | attr_accessor :verbose 9 | 10 | def initialize(base_dir, name, repo_url, rel_path) 11 | @base_dir = base_dir 12 | @name = name 13 | @repo_url = repo_url 14 | @rel_path = rel_path 15 | @verbose = self.class.verbose 16 | end 17 | 18 | def update 19 | git_ignore_self 20 | 21 | return true if frozen? 22 | FileUtils.mkdir_p checkout_path unless File.exist?(checkout_path) 23 | if checked_out? 24 | if !File.exist?(repo_path + '/.git') 25 | raise "Directory '#{@name}' exists but is not a git repository" 26 | else 27 | update_output { `cd #{repo_path} && git pull 2>&1` } 28 | end 29 | else 30 | update_output { `cd #{checkout_path} && git clone #{@repo_url} #{@name}` } 31 | end 32 | true 33 | end 34 | 35 | def freezify 36 | return true if frozen? || !checked_out? 37 | 38 | Dir.chdir(repo_path) do 39 | `tar czf .git.frozen.tgz .git` 40 | FileUtils.rm_r('.git') 41 | end 42 | `cd #{@base_dir} && git add -f #{rel_repo_path}` 43 | true 44 | end 45 | 46 | def unfreezify 47 | return true unless frozen? 48 | 49 | Dir.chdir(repo_path) do 50 | `tar xzf .git.frozen.tgz` 51 | FileUtils.rm('.git.frozen.tgz') 52 | end 53 | `cd #{@base_dir} && git rm -r --cached #{rel_repo_path}` 54 | true 55 | end 56 | 57 | def frozen? 58 | File.exist?(repo_path + '/.git.frozen.tgz') 59 | end 60 | 61 | def checked_out? 62 | File.exist?(repo_path) 63 | end 64 | 65 | private 66 | def checkout_path 67 | File.expand_path(File.join(@base_dir, @rel_path)) 68 | end 69 | 70 | def repo_path 71 | File.expand_path(checkout_path + '/' + @name) 72 | end 73 | 74 | def rel_repo_path 75 | @rel_path + '/' + @name 76 | end 77 | 78 | def update_output(&block) 79 | puts "Updating #{@name}" if verbose 80 | block.call 81 | puts " ..updated\n" if verbose 82 | end 83 | 84 | def git_ignore_self 85 | Dir.chdir(@base_dir) do 86 | contents = File.read('.gitignore') if File.exist?('.gitignore') 87 | 88 | unless contents.to_s.include?(rel_repo_path) 89 | File.open('.gitignore', 'w') do |file| 90 | if contents 91 | file << contents 92 | file << "\n" unless contents[-1] == 10 # ascii code for \n 93 | end 94 | file << rel_repo_path << "\n" 95 | end 96 | end 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/giternal/version.rb: -------------------------------------------------------------------------------- 1 | module Giternal 2 | module VERSION #:nodoc: 3 | MAJOR = 0 4 | MINOR = 0 5 | TINY = 2 6 | 7 | STRING = [MAJOR, MINOR, TINY].join('.') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/giternal/yaml_config.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Giternal 4 | class YamlConfig 5 | def initialize(base_dir, yaml_string) 6 | @base_dir = base_dir 7 | @config_hash = YAML.load yaml_string 8 | end 9 | 10 | def each_repo 11 | repositories.each { |r| yield(r) if block_given? } 12 | end 13 | 14 | def find_repo(path) 15 | @config_hash.each do |name, attributes| 16 | if path == File.join(attributes["path"], name) 17 | return Repository.new(@base_dir, name, attributes["repo"], attributes["path"]) 18 | end 19 | end 20 | return nil 21 | end 22 | 23 | private 24 | def repositories 25 | @config_hash.map do |name, attributes| 26 | Repository.new(@base_dir, name, attributes["repo"], attributes["path"]) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/giternal/app_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper")) 2 | 3 | module Giternal 4 | describe App do 5 | before(:each) do 6 | @app = App.new("some_fake_dir") 7 | @mock_config = stub("config").as_null_object 8 | end 9 | 10 | describe "loading the config file" do 11 | before(:each) do 12 | File.stub!(:file?).and_return true 13 | File.stub!(:read).and_return "yaml config" 14 | YamlConfig.stub!(:new).and_return @mock_config 15 | end 16 | 17 | it "should look for config/giternal.yml" do 18 | File.should_receive(:file?).with(/some_fake_dir\/config\/giternal\.yml/) 19 | @app.config 20 | end 21 | 22 | it "should look for .giternal.yml if giternal.yml does not exist" do 23 | File.should_receive(:file?).with(/some_fake_dir\/config\/giternal\.yml/).and_return false 24 | File.should_receive(:file?).with(/some_fake_dir\/\.giternal\.yml/).and_return true 25 | @app.config 26 | end 27 | 28 | it "should exit with an error when no config file exists" do 29 | File.stub!(:file?).and_return false 30 | $stderr.should_receive(:puts) 31 | @app.should_receive(:exit).with(1) 32 | @app.config 33 | end 34 | 35 | it "should create a config from the config file" do 36 | YamlConfig.should_receive(:new).with('some_fake_dir', "yaml config").and_return @mock_config 37 | @app.config 38 | end 39 | end 40 | 41 | describe "app actions" do 42 | before(:each) do 43 | @app.stub!(:config).and_return @mock_config 44 | @mock_repo = mock("repo") 45 | @mock_config.stub!(:each_repo).and_yield(@mock_repo) 46 | end 47 | 48 | it "should update each of the repositories" do 49 | @mock_repo.should_receive(:update) 50 | @app.update 51 | end 52 | 53 | it "should freeze each of the repositories" do 54 | @mock_repo.should_receive(:freezify) 55 | @app.freezify 56 | end 57 | 58 | it "should unfreeze each of the repositories" do 59 | @mock_repo.should_receive(:unfreezify) 60 | @app.unfreezify 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/giternal/repository_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper")) 2 | 3 | module Giternal 4 | describe Repository do 5 | before(:each) do 6 | GiternalHelper.create_main_repo 7 | GiternalHelper.create_repo 'foo' 8 | @repository = Repository.new(GiternalHelper.base_project_dir, "foo", 9 | GiternalHelper.external_path('foo'), 10 | 'dependencies') 11 | end 12 | 13 | it "should check itself out to a dir" do 14 | @repository.update 15 | File.file?(GiternalHelper.checked_out_path('foo/foo')).should be_true 16 | File.read(GiternalHelper.checked_out_path('foo/foo')).strip. 17 | should == 'foo' 18 | end 19 | 20 | it "should be ignored from git" do 21 | @repository.update 22 | Dir.chdir(GiternalHelper.base_project_dir) do 23 | # TODO: What I really want is to say it shouldn't include 'foo' 24 | `git status`.should_not include('dependencies') 25 | File.read('.gitignore').should == "dependencies/foo\n" 26 | end 27 | end 28 | 29 | it "should only add itself to .gitignore if it's not already there" do 30 | Dir.chdir(GiternalHelper.base_project_dir) do 31 | File.open('.gitignore', 'w') {|f| f << "dependencies/foo\n" } 32 | end 33 | 34 | @repository.update 35 | 36 | Dir.chdir(GiternalHelper.base_project_dir) do 37 | File.read('.gitignore').should == "dependencies/foo\n" 38 | end 39 | end 40 | 41 | it "adds a newline if it needs to" do 42 | Dir.chdir(GiternalHelper.base_project_dir) do 43 | File.open('.gitignore', 'w') {|f| f << "something/else" } 44 | end 45 | 46 | @repository.update 47 | 48 | Dir.chdir(GiternalHelper.base_project_dir) do 49 | File.read('.gitignore').should == "something/else\ndependencies/foo\n" 50 | end 51 | end 52 | 53 | it "should not show any output when verbose mode is off" do 54 | @repository.verbose = false 55 | @repository.should_not_receive(:puts) 56 | @repository.update 57 | end 58 | 59 | it "should not show output when verbose mode is on" do 60 | @repository.verbose = true 61 | @repository.should_receive(:puts).any_number_of_times 62 | @repository.update 63 | end 64 | 65 | it "should update the repo when it's already been checked out" do 66 | @repository.update 67 | GiternalHelper.add_content 'foo', 'newfile' 68 | @repository.update 69 | File.file?(GiternalHelper.checked_out_path('foo/newfile')).should be_true 70 | File.read(GiternalHelper.checked_out_path('foo/newfile')).strip. 71 | should == 'newfile' 72 | end 73 | 74 | it "should raise an error if the directory exists but there's no .git dir" do 75 | FileUtils.mkdir_p(GiternalHelper.checked_out_path('foo')) 76 | lambda { 77 | @repository.update 78 | }.should raise_error(/Directory 'foo' exists but is not a git repository/) 79 | end 80 | 81 | describe "freezify" do 82 | before(:each) do 83 | GiternalHelper.create_repo('external') 84 | @repository = Repository.new(GiternalHelper.base_project_dir, 'external', 85 | GiternalHelper.external_path('external'), 86 | 'dependencies') 87 | @repository.update 88 | end 89 | 90 | it "should archive the .git dir" do 91 | @repository.freezify 92 | File.file?(GiternalHelper.checked_out_path('external/.git.frozen.tgz')).should be_true 93 | end 94 | 95 | it "should get rid of the .git dir" do 96 | File.directory?(GiternalHelper.checked_out_path('external/.git')).should be_true 97 | @repository.freezify 98 | File.directory?(GiternalHelper.checked_out_path('external/.git')).should be_false 99 | end 100 | end 101 | 102 | it "should simply return if updated when frozen" do 103 | @repository.update 104 | @repository.freezify 105 | lambda { @repository.update }.should_not raise_error 106 | end 107 | 108 | it "should simply return when made to freeze when already frozen" do 109 | @repository.update 110 | @repository.freezify 111 | lambda { @repository.freezify }.should_not raise_error 112 | end 113 | 114 | it "should simply return when made to freeze before checked out" do 115 | lambda { @repository.freezify }.should_not raise_error 116 | end 117 | 118 | it "should simply return when made to unfreeze before checked out" do 119 | lambda { @repository.unfreezify }.should_not raise_error 120 | end 121 | 122 | it "should simply return when made to unfreeze when already unfrozen" do 123 | @repository.update 124 | lambda { @repository.unfreezify }.should_not raise_error 125 | end 126 | 127 | describe "unfreezify" do 128 | before(:each) do 129 | GiternalHelper.create_repo('main') 130 | GiternalHelper.create_repo('external') 131 | @repository = Repository.new(GiternalHelper.base_project_dir, 'external', 132 | GiternalHelper.external_path('external'), 133 | 'dependencies') 134 | @repository.update 135 | end 136 | 137 | it "should unarchive the .git dir" do 138 | @repository.freezify 139 | @repository.unfreezify 140 | File.directory?(GiternalHelper.checked_out_path('external/.git')).should be_true 141 | end 142 | 143 | it "should remove the archived file" do 144 | @repository.freezify 145 | @repository.unfreezify 146 | File.file?(GiternalHelper.checked_out_path('external/.git.frozen.tgz')).should be_false 147 | end 148 | 149 | it "leaves the .git directory exactly how it found it" do 150 | expect { 151 | @repository.freezify 152 | @repository.unfreezify 153 | }.to_not change { Dir[GiternalHelper.checked_out_path('external/.git/**/**')] } 154 | end 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /spec/giternal/yaml_config_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper")) 2 | 3 | module Giternal 4 | describe YamlConfig do 5 | it "should create repositories from the config" do 6 | config = YamlConfig.new('base_dir', 7 | "rspec:\n repo: git://rspec\n path: vendor/plugins\n" + 8 | "foo:\n repo: git://at/foo\n path: path/to/foo\n") 9 | Repository.should_receive(:new).with('base_dir', "rspec", "git://rspec", "vendor/plugins").and_return :a_repo 10 | Repository.should_receive(:new).with('base_dir', "foo", "git://at/foo", "path/to/foo").and_return :a_repo 11 | config.each_repo {|r| r.should == :a_repo} 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/giternal_helper.rb: -------------------------------------------------------------------------------- 1 | class GiternalHelper 2 | @@giternal_base ||= File.expand_path(File.dirname(__FILE__) + '/..') 3 | 4 | def self.create_main_repo 5 | FileUtils.mkdir_p tmp_path 6 | Dir.chdir(tmp_path) do 7 | FileUtils.mkdir "main_repo" 8 | Dir.chdir('main_repo') do 9 | `git init` 10 | `echo 'first content' > starter_repo` 11 | `git add starter_repo` 12 | `git commit -m "starter repo"` 13 | end 14 | end 15 | end 16 | 17 | def self.tmp_path 18 | "/tmp/giternal_test" 19 | end 20 | 21 | def self.giternal_base 22 | @@giternal_base 23 | end 24 | 25 | def self.base_project_dir 26 | tmp_path + '/main_repo' 27 | end 28 | 29 | def self.run(*args) 30 | `#{giternal_base}/bin/giternal #{args.join(' ')}` 31 | end 32 | 33 | def self.create_repo(repo_name) 34 | Dir.chdir(tmp_path) do 35 | FileUtils.mkdir_p "externals/#{repo_name}" 36 | `cd externals/#{repo_name} && git init` 37 | end 38 | add_content repo_name 39 | add_to_config_file repo_name 40 | end 41 | 42 | def self.add_to_config_file(repo_name) 43 | config_dir = tmp_path + '/main_repo/config' 44 | FileUtils.mkdir(config_dir) unless File.directory?(config_dir) 45 | Dir.chdir(config_dir) do 46 | `echo #{repo_name}: >> giternal.yml` 47 | `echo ' repo: #{external_path(repo_name)}' >> giternal.yml` 48 | `echo ' path: dependencies' >> giternal.yml` 49 | end 50 | end 51 | 52 | def self.add_content(repo_name, content=repo_name) 53 | Dir.chdir(tmp_path + "/externals/#{repo_name}") do 54 | `echo #{content} >> #{content} && git add #{content}` 55 | `git commit #{content} -m "added content to #{content}"` 56 | end 57 | end 58 | 59 | def self.external_path(repo_name) 60 | File.expand_path(tmp_path + "/externals/#{repo_name}") 61 | end 62 | 63 | def self.checked_out_path(repo_name) 64 | File.expand_path(tmp_path + "/main_repo/dependencies/#{repo_name}") 65 | end 66 | 67 | def self.clean! 68 | FileUtils.rm_rf tmp_path 69 | %w(GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE).each {|var| ENV[var] = nil } 70 | end 71 | 72 | def self.update_externals(*args) 73 | Dir.chdir(tmp_path + '/main_repo') do 74 | GiternalHelper.run('update', *args) 75 | end 76 | end 77 | 78 | def self.freeze_externals(*args) 79 | Dir.chdir(tmp_path + '/main_repo') do 80 | GiternalHelper.run("freeze", *args) 81 | end 82 | end 83 | 84 | def self.unfreeze_externals(*args) 85 | Dir.chdir(tmp_path + '/main_repo') do 86 | GiternalHelper.run("unfreeze", *args) 87 | end 88 | end 89 | 90 | def self.repo_contents(path) 91 | Dir.chdir(path) do 92 | contents = `git cat-file -p HEAD` 93 | unless contents.include?('tree') && contents.include?('author') 94 | raise "something is wrong with the repo, output doesn't contain expected git elements:\n\n #{contents}" 95 | end 96 | contents 97 | end 98 | end 99 | 100 | def self.add_external_to_ignore(repo_name) 101 | Dir.chdir(tmp_path + '/main_repo') do 102 | `echo 'dependencies/#{repo_name}' >> .gitignore` 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'rspec' 3 | rescue LoadError 4 | require 'rubygems' 5 | require 'rspec' 6 | end 7 | 8 | $:.unshift(File.dirname(__FILE__) + '/../lib') 9 | require 'giternal' 10 | require 'fileutils' 11 | require 'giternal_helper' 12 | 13 | RSpec.configure do |config| 14 | config.before { GiternalHelper.clean! } 15 | config.after { GiternalHelper.clean! } 16 | end 17 | -------------------------------------------------------------------------------- /test_trackers.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | module Repomanipulator 3 | def repo_path(path) 4 | File.expand_path File.join(File.dirname(__FILE__), "test_repos", path) 5 | end 6 | 7 | def create_repo(name) 8 | path = repo_path(name) 9 | FileUtils.mkdir_p(path) 10 | Dir.chdir(path) do 11 | `git init` 12 | `echo first_file > first_file` 13 | end 14 | commit_all path, "created repo" 15 | end 16 | 17 | def commit_all(path, message) 18 | Dir.chdir(path) do 19 | `git add .` 20 | puts `git commit -m "#{message}"` 21 | end 22 | end 23 | 24 | def clone_repo(from, to, options=nil) 25 | Dir.chdir(repo_path("")) { `git clone #{options} #{from} #{to}` } 26 | end 27 | 28 | def add_content_to(file, content) 29 | path = repo_path file 30 | FileUtils.touch path 31 | File.open(path, 'w') {|f| f << content } 32 | `cd #{File.dirname(path)} && git checkout master` 33 | commit_all File.dirname(repo_path(file)), "added content to #{file}" 34 | end 35 | 36 | def push(repo) 37 | Dir.chdir(repo_path(repo)) { `git push origin master` } 38 | end 39 | 40 | def pull(repo) 41 | Dir.chdir(repo_path(repo)) { puts `git pull` } 42 | end 43 | 44 | def verify_repo_has_files(repo, *files) 45 | Dir.chdir(repo_path(repo)) do 46 | missing_files = files.select {|f| !File.exist?(f) } 47 | raise "Expected files: [#{missing_files.join(', ')}] to exist in #{repo} but are missing" unless missing_files.empty? 48 | end 49 | end 50 | end 51 | include Repomanipulator 52 | 53 | module GitSubmodules 54 | def add_external(base, external) 55 | external_path = repo_path(external) 56 | Dir.chdir(repo_path(base)) { puts `git submodule add #{external_path} #{external}` } 57 | commit_all repo_path(base), "committed external #{external}" 58 | end 59 | 60 | def update_externals(repo) 61 | Dir.chdir(repo_path(repo)) do 62 | puts `git submodule init` 63 | puts `git submodule update` 64 | end 65 | commit_all repo_path(repo), "updated externals" 66 | end 67 | end 68 | 69 | module Giternal 70 | def add_external(base, external) 71 | external_path = repo_path external 72 | Dir.chdir(repo_path(base)) do 73 | FileUtils.touch ".giternal.yml" 74 | File.open(".giternal.yml", 'w') do |f| 75 | f << external << ":\n" 76 | f << " repo: #{external_path}\n" 77 | f << " path: ." 78 | end 79 | `echo #{external} >> .gitignore` 80 | end 81 | commit_all repo_path(base), "added #{external}" 82 | end 83 | 84 | def update_externals(base) 85 | Dir.chdir(repo_path(base)) { puts `giternal update` } 86 | end 87 | end 88 | 89 | module Braid 90 | def add_external(base, external) 91 | external_path = repo_path external 92 | Dir.chdir(repo_path(base)) { puts `braid add --type git #{external_path} #{external}` } 93 | end 94 | 95 | def update_externals(base) 96 | Dir.chdir(repo_path(base)) { puts `braid update` } 97 | end 98 | end 99 | 100 | 101 | drivers = {"submodules" => GitSubmodules, "giternal" => Giternal, "braid" => Braid} 102 | unless drivers.keys.include?(ARGV.first) 103 | puts "Run with: ruby test_tracking.rb #{drivers.keys.join('|')}" 104 | exit 105 | end 106 | include drivers[ARGV.first] 107 | 108 | FileUtils.rm_rf repo_path("") 109 | 110 | def ALERT(message) 111 | puts 112 | puts "*** " + message 113 | end 114 | 115 | ALERT "the project has will_paginate as an external" 116 | create_repo "will_paginate-full" 117 | clone_repo "will_paginate-full", "will_paginate", "--bare" 118 | 119 | create_repo "base-full" 120 | add_external "base-full", "will_paginate" 121 | clone_repo "base-full", "base", "--bare" 122 | 123 | ALERT "Joe and Sarah clone copies of the project" 124 | clone_repo "base", "joe" 125 | update_externals "joe" 126 | 127 | clone_repo "base", "sarah" 128 | update_externals "sarah" 129 | 130 | ALERT "Joe adds README to will_paginate and commits" 131 | add_content_to "joe/will_paginate/README", "some content" 132 | 133 | ALERT "Joe commits a change to the base project" 134 | add_content_to "joe/foo", "in the base project" 135 | 136 | ALERT "Joe pushes his code" 137 | push "joe" 138 | push "joe/will_paginate" 139 | 140 | ALERT "Sarah adds LICENSE to her repo and commits" 141 | add_content_to "sarah/will_paginate/LICENSE", "gnu baby" 142 | 143 | ALERT "Sarah commits a change to the base project" 144 | add_content_to "sarah/bar", "in the base project" 145 | 146 | ALERT "Sarah pulls from base project and updates externals" 147 | pull "sarah" 148 | update_externals "sarah" 149 | push "sarah/will_paginate" 150 | 151 | ALERT "Does the cloned project have all the files we expect?" 152 | verify_repo_has_files("sarah", "foo", "bar", "will_paginate/README", "will_paginate/LICENSE") 153 | 154 | ALERT "Does the upstream external have all the files we expect?" 155 | clone_repo "will_paginate", "will_paginate_clone" 156 | verify_repo_has_files "will_paginate_clone", "README", "LICENSE" 157 | puts "woooo! Collaboration ftw" 158 | --------------------------------------------------------------------------------