├── .gitignore ├── .rspec ├── Manifest ├── Gemfile ├── demo └── overview.md ├── test ├── blob │ └── test_load.rb ├── gash │ └── test_new.rb └── tree │ ├── test_delete.rb │ ├── test_to_hash.rb │ ├── test_update.rb │ ├── test_fetch.rb │ ├── test_store.rb │ └── test_retrieve.rb ├── spec ├── spec_helper.rb ├── gash_spec.rb └── support │ └── helper.rb ├── HISTORY ├── README.rdoc ├── Indexfile ├── .ruby ├── LICENSE ├── Rakefile ├── .gemspec └── lib └── gash.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | log 3 | doc 4 | pkg 5 | tmp 6 | web 7 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -fs 3 | -Ilib 4 | -Ispec 5 | --require spec_helper -------------------------------------------------------------------------------- /Manifest: -------------------------------------------------------------------------------- 1 | .ruby 2 | HISTORY 3 | README.rdoc 4 | LICENSE 5 | Manifest 6 | lib/gash.rb 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec 3 | 4 | group :test do 5 | gem "rspec-core" 6 | gem "rspec" 7 | end -------------------------------------------------------------------------------- /demo/overview.md: -------------------------------------------------------------------------------- 1 | # Gash Overview 2 | 3 | Gash allows us to work with a git repository in much the same manner 4 | as we work with an ordinary hash. 5 | 6 | -------------------------------------------------------------------------------- /test/blob/test_load.rb: -------------------------------------------------------------------------------- 1 | covers "gash" 2 | 3 | test_class Gash::Blob do 4 | 5 | method :load! do 6 | 7 | test "" do 8 | end 9 | 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "rspec" 2 | require "gash" 3 | require "./spec/support/helper" 4 | 5 | RSpec.configure do |config| 6 | config.mock_with :rspec 7 | config.include Helper 8 | config.before(:each) { setup } 9 | config.after(:each) { teardown } 10 | end -------------------------------------------------------------------------------- /test/gash/test_new.rb: -------------------------------------------------------------------------------- 1 | covers 'gash' 2 | 3 | test_class Gash do 4 | 5 | class_method :new do 6 | 7 | test "without a path defaults to the current working path" do 8 | repo = Gash.new 9 | repo.directory.assert == Dir.pwd 10 | end 11 | 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /HISTORY: -------------------------------------------------------------------------------- 1 | = Release History 2 | 3 | == v0.1.4 4 | * Fix for 1.9 escaping issue. (#5) 5 | 6 | == v0.1.3 7 | * Removed nasty eval. Switching to open4. 8 | 9 | == v0.1.2 10 | * Working correctly on empty branches. 11 | 12 | == v0.1.1 13 | * Tree got more Hash-methods. Better documentation. 14 | 15 | == v0.1.0 16 | * First version. 17 | -------------------------------------------------------------------------------- /test/tree/test_delete.rb: -------------------------------------------------------------------------------- 1 | cover 'gash' 2 | 3 | test_class Gash::Tree do 4 | 5 | method :delete do 6 | 7 | concern "tree of workiong git repository" do 8 | @repo = Gash.new(:branch=>'test') 9 | @tree = Tree.new(:parent => @repo) 10 | end 11 | 12 | test "removes an object from the tree" do 13 | @tree.delete('foo.rb') 14 | @tree['foo.rb'].assert.nil? 15 | end 16 | 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /test/tree/test_to_hash.rb: -------------------------------------------------------------------------------- 1 | cover 'gash' 2 | 3 | test_class Gash::Tree do 4 | 5 | method :to_hash do 6 | 7 | concern "tree of workiong git repository" do 8 | @repo = Gash.new(:branch=>'test') 9 | @tree = Tree.new(:parent => @repo) 10 | end 11 | 12 | test "convert to ordinary hash object" do 13 | hash = @tree.to_hash 14 | hash.class.assert == Hash 15 | # TODO: add some additional detailed assertions 16 | end 17 | 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Gash, Git + Hash 2 | 3 | == Description 4 | 5 | Gash is a simple Ruby API for interfacing with a Git repo. While not as 6 | feature rich as say {grit}[https://github.com/mojombo/grit], it is easier 7 | to use b/c it effectively allows you to work with the files in the repo much 8 | like you would any Ruby Hash. 9 | 10 | == Documentation 11 | 12 | Please see http://judofyr.github.com/gash or the documentation in lib/gash.rb! 13 | 14 | == Copyrights 15 | 16 | (MIT License) 17 | 18 | Copyright 2008 Magnus Holm 19 | 20 | Copyright 2008 Michael Siebert 21 | 22 | See LICENSE file for license details. 23 | 24 | -------------------------------------------------------------------------------- /test/tree/test_update.rb: -------------------------------------------------------------------------------- 1 | cover 'gash' 2 | 3 | test_class Gash::Tree do 4 | 5 | method :update do 6 | 7 | concern "tree of workiong git repository" do 8 | @repo = Gash.new(:branch=>'test') 9 | @tree = Tree.new(:parent => @repo) 10 | end 11 | 12 | test "update takes another hash object and update the tree" do 13 | hsh = { 14 | 'rabbit.txt' => "Rabbit fell.", 15 | 'alice.txt' => "Alice fell." 16 | } 17 | @tree.update(hsh) 18 | 19 | @tree['rabbit.txt'].assert == "Rabbit fell." 20 | @tree['alice.txt'].assert == "Alice fell." 21 | end 22 | 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /Indexfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | name "gash" 4 | version "0.1.4" 5 | 6 | title "Gash" 7 | summary "Git + Hash" 8 | description "Gash = Git + Hash" 9 | created "2008-08-30" 10 | 11 | requirements ["open4 >=0.9.6", 12 | "hanna-nouveau (document)", 13 | "lemon (test)"] 14 | 15 | resources "home" => "http://dojo.rubyforge.org/gash", 16 | "code" => "http://github.com/judofyr/gash" 17 | 18 | repositories "upstream" => "git@github.com:judofyr/gash.git" 19 | 20 | authors ["Magnus Holm ", 21 | "Michael Siebert"] 22 | 23 | organization "dojo" 24 | 25 | copyrights ["2008 Magnus Holm (MIT)", 26 | "2008 Michael Siebert (MIT)"] 27 | 28 | alternatives ["grit"] 29 | 30 | -------------------------------------------------------------------------------- /test/tree/test_fetch.rb: -------------------------------------------------------------------------------- 1 | covers 'gash' 2 | 3 | test_class Gash::Tree do 4 | 5 | method :fetch do 6 | 7 | concern "tree of workiong git repository" do 8 | @repo = Gash.new(:branch=>'test') 9 | @tree = Tree.new(:parent => @repo) 10 | end 11 | 12 | test "return a Blob if the repository object is a file" do 13 | obj = @tree.fetch('foo.rb') 14 | obj.assert.is_a? == Gash::Blob 15 | end 16 | 17 | test "return a Tree if the repository object is a subdirectory" do 18 | obj = @tree.fetch('bar') 19 | obj.assert.is_a? == Gash::Tree 20 | end 21 | 22 | test "raise an IndexError if a repository object is not found" do 23 | expect IndexError do 24 | @tree.fetch('snafu') 25 | end 26 | end 27 | 28 | test "return default if given instead of raising an IndexError" do 29 | @tree.fetch('snafu', nil).assert == nil 30 | end 31 | 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /test/tree/test_store.rb: -------------------------------------------------------------------------------- 1 | covers 'gash' 2 | 3 | test_class Gash::Tree do 4 | 5 | method :store do 6 | 7 | concern "tree of workiong git repository" do 8 | @repo = Gash.new(:branch=>'test') 9 | @tree = Tree.new(:parent => @repo) 10 | end 11 | 12 | test "create a Blob if the object is a string" do 13 | obj = @tree.store('story.txt', "Once upon a time...") 14 | obj.assert.is_a? == Gash::Blob 15 | end 16 | 17 | test "create a Tree if the object is a hash" do 18 | hsh = { 19 | 'rabbit.txt' => "Rabbit fell.", 20 | 'alice.txt' => "Alice fell." 21 | } 22 | obj = @tree.store('hole', hsh) 23 | obj.assert.is_a? == Gash::Tree 24 | 25 | @tree['hole'].assert == obj 26 | @tree['hole']['alice.txt'].assert == "Alice fell." 27 | end 28 | 29 | test "the #[]= method is an alias of #store" do 30 | # TODO: 31 | end 32 | 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /.ruby: -------------------------------------------------------------------------------- 1 | --- 2 | source: 3 | - Profile 4 | authors: 5 | - name: Magnus Holm 6 | email: judofyr@gmail.com 7 | - name: Michael Siebert 8 | copyrights: 9 | - holder: Magnus Holm 10 | year: '2008' 11 | license: MIT 12 | - holder: Michael Siebert 13 | year: '2008' 14 | license: MIT 15 | requirements: 16 | - name: open4 17 | version: ! '>=0.9.6' 18 | - name: hanna-nouveau 19 | groups: 20 | - document 21 | development: true 22 | dependencies: [] 23 | alternatives: 24 | - grit 25 | conflicts: [] 26 | repositories: 27 | - uri: git@github.com:judofyr/gash.git 28 | scm: git 29 | name: upstream 30 | resources: 31 | home: http://dojo.rubyforge.org/gash 32 | code: http://github.com/judofyr/gash 33 | extra: {} 34 | load_path: 35 | - lib 36 | revision: 0 37 | name: gash 38 | title: Gash 39 | version: 0.1.4 40 | summary: Git + Hash 41 | description: Gash = Git + Hash 42 | created: '2008-08-30' 43 | organization: dojo 44 | date: '2012-02-24' 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Magnus Holm and Michael Siebert 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | gem 'rdoc' 2 | 3 | require 'yaml' 4 | require 'rdoc/task' 5 | 6 | metadata = YAML.load_file('.ruby') 7 | version = metadata['version'] 8 | 9 | # DEPRECATED: Use gh-pages instead 10 | #Rake::Task[:publish_docs].instance_eval do 11 | # @actions.clear 12 | # enhance do 13 | # sh("rsync -avc --delete doc/* judofyr@rubyforge.org:/var/www/gforge-projects/dojo/gash/") 14 | # end 15 | #end 16 | 17 | RDoc::Task.new do |rdoc| 18 | rdoc.generator = 'hanna' 19 | rdoc.main = 'README.rdoc' 20 | rdoc.rdoc_dir = 'web' 21 | rdoc.title = "Gash #{version} Documentation" 22 | rdoc.rdoc_files.include('README.rdoc') 23 | rdoc.rdoc_files.include('lib/**/*.rb') 24 | end 25 | 26 | desc "build gem" 27 | task :gem do 28 | sh "gem build .gemspec" 29 | end 30 | 31 | desc "push gem to server" 32 | task :push do 33 | file = Dir["*-#{version}.gem"].first 34 | abort "No gem!" unless file 35 | sh "gem push #{file}" 36 | end 37 | 38 | #desc "tag version" 39 | #task :tag do 40 | # sh "git tag -a" 41 | #end 42 | 43 | desc "release package" # and tag" 44 | task :release => [:gem, :push] do 45 | puts "Don't forget to tag the version!" 46 | end 47 | 48 | -------------------------------------------------------------------------------- /test/tree/test_retrieve.rb: -------------------------------------------------------------------------------- 1 | covers 'gash' 2 | 3 | test_class Gash::Tree do 4 | 5 | method :retrieve do 6 | 7 | concern "tree of workiong git repository" do 8 | @repo = Gash.new(:branch=>'test') 9 | @tree = Tree.new(:parent => @repo) 10 | end 11 | 12 | test "return a Blob if the repository object is a file" do 13 | obj = @tree.retreive('foo.rb') 14 | obj.assert.is_a? == Gash::Blob 15 | end 16 | 17 | test "return a Tree if the repository object is a subdirectory" do 18 | obj = @tree.retrieve('bar') 19 | obj.assert.is_a? == Gash::Tree 20 | end 21 | 22 | test "return nil if a repository object is not found" do 23 | @tree.retrieve('snafu').assert == nil 24 | end 25 | 26 | test "passing lazy flag prevents the object from being loaded immediately" do 27 | # TODO: how do we test? 28 | @tree.retrieve('baz.rb', true) 29 | end 30 | 31 | test "the #[] method is an alias of #retrieve" do 32 | @tree['foo.rb'] == @tree.retrieve['foo.rb'] 33 | end 34 | 35 | test "the #/ method is also an alias of #retrieve" do 36 | (@tree / 'foo.rb') == @tree.retrieve['foo.rb'] 37 | end 38 | 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /spec/gash_spec.rb: -------------------------------------------------------------------------------- 1 | describe Gash do 2 | let(:gash) { Gash.new(path) } 3 | 4 | it "starts with an empty directory" do 5 | gash.should be_empty 6 | end 7 | 8 | it "should not be empty after a file has been added" do 9 | gash["File"] = "data" 10 | gash.should_not be_empty 11 | end 12 | 13 | it "commits files when #commit are applied to gash object" do 14 | gash["File"] = "data" 15 | hash = gash.commit("My commit message") 16 | list_files(hash).should include("File") 17 | end 18 | 19 | it "can override files" do 20 | gash["File"] = "data" 21 | hash = gash.commit("My commit message") 22 | list_files(hash).should include("File") 23 | gash["File"] = "other" 24 | hash = gash.commit("My commit message 2") 25 | content.should match(/other/) 26 | end 27 | 28 | it "can create a tree" do 29 | gash["my-folder/file"] = "content" 30 | hash = gash.commit("My commit message") 31 | content.should match(/content/) 32 | raw_commit.should match(%r{A\s+my-folder/file}) 33 | end 34 | 35 | it "should be possible to pass a blob to path" do 36 | gash["file1"] = "content" 37 | gash["file2/a"] = gash["file1"] 38 | gash.commit("Commit message") 39 | raw_commit.should match(%r{A\s+file1}) 40 | raw_commit.should match(%r{A\s+file2/a}) 41 | end 42 | end -------------------------------------------------------------------------------- /spec/support/helper.rb: -------------------------------------------------------------------------------- 1 | require "tmpdir" 2 | module Helper 3 | def path 4 | @path ||= Dir.mktmpdir 5 | end 6 | 7 | def setup 8 | `cd #{path} && git init` 9 | end 10 | 11 | def teardown 12 | `rm -rf #{path}` 13 | end 14 | 15 | # 16 | # @hash String A commit hash 17 | # @return Array A list of files 18 | # 19 | def list_files(hash) 20 | `cd #{path} && git show --pretty='format:' --name-only #{hash}`.strip.split("\n") 21 | end 22 | 23 | # 24 | # @return String Diff content for last commit 25 | # 26 | def content 27 | `cd #{path} && git show HEAD` 28 | end 29 | 30 | # 31 | # @dir String Folder to retrive 32 | # @return String 33 | # Example: 040000 tree d91d06157bdc633d25f970b9cc54d0eb74fb850f my-folder 34 | # 35 | def folder(dir) 36 | tree = `cd #{path} && git cat-file -p HEAD`.split(" ")[1] 37 | `cd #{path} && git cat-file -p #{tree}`.split("\n").select do |row| 38 | row.match(/#{dir}/) and row.match(/tree/) 39 | end.first 40 | end 41 | 42 | # 43 | # @return String The last commit message 44 | # 45 | def last_commit_message 46 | `cd #{path} && git log --pretty='format:%s' -n 1` 47 | end 48 | 49 | # 50 | # @return String 51 | # @example 52 | # commit 5b580afc95c32721d35a7d659abce1e3845635a9 53 | # Author: Linus Oleander 54 | # AuthorDate: Mon Feb 27 23:16:59 2012 +0100 55 | # Commit: Linus Oleander 56 | # CommitDate: Mon Feb 27 23:16:59 2012 +0100 57 | # 58 | # Add last_commit_message helper 59 | # 60 | # M spec/support/helper.rb 61 | # 62 | def raw_commit 63 | `cd #{path} && git show --name-status --format=fuller` 64 | end 65 | end -------------------------------------------------------------------------------- /.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | 5 | module DotRuby 6 | 7 | # 8 | class GemSpec 9 | 10 | # For which revision of .ruby is this gemspec intended? 11 | REVISION = 0 unless defined?(REVISION) 12 | 13 | # 14 | PATTERNS = { 15 | :bin_files => 'bin/*', 16 | :lib_files => 'lib/{**/}*.rb', 17 | :ext_files => 'ext/{**/}extconf.rb', 18 | :doc_files => '*.{txt,rdoc,md,markdown,tt,textile}', 19 | :test_files => '{test/{**/}*_test.rb,spec/{**/}*_spec.rb}' 20 | } unless defined?(PATTERNS) 21 | 22 | # 23 | def self.instance 24 | new.to_gemspec 25 | end 26 | 27 | attr :metadata 28 | 29 | attr :manifest 30 | 31 | # 32 | def initialize 33 | @metadata = YAML.load_file('.ruby') 34 | @manifest = Dir.glob('manifest{,.txt}', File::FNM_CASEFOLD).first 35 | 36 | if @metadata['revision'].to_i != REVISION 37 | warn "You have the wrong revision. Trying anyway..." 38 | end 39 | end 40 | 41 | # 42 | def scm 43 | @scm ||= \ 44 | case 45 | when File.directory?('.git') 46 | :git 47 | end 48 | end 49 | 50 | # 51 | def files 52 | @files ||= \ 53 | #glob_files[patterns[:files]] 54 | case 55 | when manifest 56 | File.readlines(manifest). 57 | map{ |line| line.strip }. 58 | reject{ |line| line.empty? || line[0,1] == '#' } 59 | when scm == :git 60 | `git ls-files -z`.split("\0") 61 | else 62 | Dir.glob('{**/}{.*,*}') # TODO: be more specific using standard locations ? 63 | end.select{ |path| File.file?(path) } 64 | end 65 | 66 | # 67 | def glob_files(pattern) 68 | Dir.glob(pattern).select { |path| 69 | File.file?(path) && files.include?(path) 70 | } 71 | end 72 | 73 | # 74 | def patterns 75 | PATTERNS 76 | end 77 | 78 | # 79 | def executables 80 | @executables ||= \ 81 | glob_files(patterns[:bin_files]).map do |path| 82 | File.basename(path) 83 | end 84 | end 85 | 86 | def extensions 87 | @extensions ||= \ 88 | glob_files(patterns[:ext_files]).map do |path| 89 | File.basename(path) 90 | end 91 | end 92 | 93 | # 94 | def name 95 | metadata['name'] || metadata['title'].downcase.gsub(/\W+/,'_') 96 | end 97 | 98 | # 99 | def to_gemspec 100 | Gem::Specification.new do |gemspec| 101 | gemspec.name = name 102 | gemspec.version = metadata['version'] 103 | gemspec.summary = metadata['summary'] 104 | gemspec.description = metadata['description'] 105 | 106 | metadata['authors'].each do |author| 107 | gemspec.authors << author['name'] 108 | 109 | if author.has_key?('email') 110 | if gemspec.email 111 | gemspec.email << author['email'] 112 | else 113 | gemspec.email = [author['email']] 114 | end 115 | end 116 | end 117 | 118 | gemspec.licenses = metadata['copyrights'].map{ |c| c['license'] }.compact 119 | 120 | metadata['requirements'].each do |req| 121 | name = req['name'] 122 | version = req['version'] 123 | groups = req['groups'] || [] 124 | 125 | case version 126 | when /^(.*?)\+$/ 127 | version = ">= #{$1}" 128 | when /^(.*?)\-$/ 129 | version = "< #{$1}" 130 | when /^(.*?)\~$/ 131 | version = "~> #{$1}" 132 | end 133 | 134 | if groups.empty? or groups.include?('runtime') 135 | # populate runtime dependencies 136 | if gemspec.respond_to?(:add_runtime_dependency) 137 | gemspec.add_runtime_dependency(name,*version) 138 | else 139 | gemspec.add_dependency(name,*version) 140 | end 141 | else 142 | # populate development dependencies 143 | if gemspec.respond_to?(:add_development_dependency) 144 | gemspec.add_development_dependency(name,*version) 145 | else 146 | gemspec.add_dependency(name,*version) 147 | end 148 | end 149 | end 150 | 151 | # convert external dependencies into a requirements 152 | if metadata['external_dependencies'] 153 | ##gemspec.requirements = [] unless metadata['external_dependencies'].empty? 154 | metadata['external_dependencies'].each do |req| 155 | gemspec.requirements << req.to_s 156 | end 157 | end 158 | 159 | # determine homepage from resources 160 | homepage = metadata['resources'].find{ |key, url| key =~ /^home/ } 161 | gemspec.homepage = homepage.last if homepage 162 | 163 | gemspec.require_paths = metadata['load_path'] || ['lib'] 164 | gemspec.post_install_message = metadata['install_message'] 165 | 166 | # RubyGems specific metadata 167 | gemspec.files = files 168 | gemspec.extensions = extensions 169 | gemspec.executables = executables 170 | 171 | if Gem::VERSION < '1.7.' 172 | gemspec.default_executable = gemspec.executables.first 173 | end 174 | 175 | gemspec.test_files = glob_files(patterns[:test_files]) 176 | 177 | unless gemspec.files.include?('.document') 178 | gemspec.extra_rdoc_files = glob_files(patterns[:doc_files]) 179 | end 180 | end 181 | end 182 | 183 | end #class GemSpec 184 | 185 | end 186 | 187 | DotRuby::GemSpec.instance 188 | -------------------------------------------------------------------------------- /lib/gash.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | require 'open4' 3 | 4 | # == What is Gash? 5 | # 6 | # * Gash lets you access a Git-repo as a Hash. 7 | # * Gash doesn't touch your working directory 8 | # * Gash only cares about the data, not the commits. 9 | # * Gash only cares about the _latest_ data. 10 | # * Gash can commit. 11 | # * Gash will automatically create branches if they don't exist. 12 | # * Gash only loads what it needs, so it handles large repos well. 13 | # * Gash got {pretty good documentation}[http://dojo.rubyforge.org/gash]. 14 | # * Gash got {a bug tracker}[http://dojo.lighthouseapp.com/projects/17529-gash]. 15 | # * Gash is being {developed at GitHub}[http://github.com/judofyr/gash]. 16 | # 17 | # Some of these "rules" might change it the future. 18 | # 19 | # == How do you install it? 20 | # 21 | # The stable version can installed through RubyGems: 22 | # 23 | # sudo gem install gash 24 | # 25 | # The unstable version can be checked out {through Git at GitHub}[http://github.com/judofyr/gash], 26 | # and installed through this command: 27 | # 28 | # rake install 29 | # 30 | # == How do you use it? 31 | # 32 | # gash = Gash.new 33 | # gash["README"] = "new content" 34 | # gash.commit("Some changes...") 35 | # 36 | # It's also important to remember that a Gash is simply a Tree, so you can 37 | # also call those methods. 38 | # 39 | # See also: #new, #commit, Tree 40 | # 41 | # == Credits 42 | # 43 | # This code is based upon git-shelve[https://github.com/siebertm/git-shelve], 44 | # created by Michael Siebert, which is released under LGPL. However, 45 | # Michael has allowed me to release this under the MIT-license as long as 46 | # I keep his name here. 47 | # 48 | # And, in fact: I could never create this without the code written by Michael. 49 | # You should really thank him! 50 | # 51 | # Older versions of Gash, which doesn't include this section or the MIT-license, 52 | # is still licensed under LGPL. 53 | class Gash < SimpleDelegator 54 | module Errors 55 | # This error is raised when the Git-command fails. 56 | class Git < StandardError; end 57 | class NoGitRepo < StandardError; end 58 | end 59 | 60 | # Some common methods used by both Tree and Blob. 61 | module Helpers 62 | attr_accessor :sha1, :mode, :parent 63 | 64 | # Sets the accessors using a Hash: 65 | # 66 | # tree = Gash::Tree.new(:sha1 => "some thing", :mode => "some thing", 67 | # :parent => "some parent") 68 | # tree.sha1 == "some thing" 69 | # tree.mode == "some thing" 70 | # tree.parent == "some parent" 71 | def initialize(opts = {}) 72 | opts.each do |key, value| 73 | send("#{key}=", value) 74 | end 75 | end 76 | 77 | # Checks if this is a Blob. 78 | def blob?; self.class == Gash::Blob end 79 | # Checks if this is a Tree. 80 | def tree?; self.class == Gash::Tree end 81 | # Checks if this object has been changed (since last commit). 82 | def changed?; !@sha1 end 83 | # Mark this, and all parents as changed. 84 | def changed!; @sha1 = nil;parent.changed! if parent and not parent == self end 85 | # Returns the Gash-object (top-parent). 86 | def gash; parent.gash if parent end 87 | 88 | # Converts the +value+ to a Tree or a Blob, using some rules: 89 | # 90 | # ==== If +value+ is already a Tree or a Blob: 91 | # 92 | # * If +value+ comes from another repo, we load it and return a deep copy. 93 | # * If +value+ got no parent, we simply return the same tree. 94 | # * If +value+'s parent is +self+, we also return the same tree. 95 | # * If +value+'s parent is something else, we return a duplicated tree. 96 | # 97 | # ==== If it's something else: 98 | # 99 | # * If +value+ is a Hash, we create a Tree from it. 100 | # * If it's not any of the former rules, we turn it into a string and create a Blob from it. 101 | def normalize(value) 102 | case value 103 | when Tree, Blob, Gash 104 | if value.parent && value.parent != self 105 | if (g = value.gash) && self.gash == g 106 | value.dup 107 | else 108 | normalize(value.tree? ? value.to_hash : value.to_s) 109 | end 110 | else 111 | value 112 | end 113 | when Hash 114 | Tree[value] 115 | else 116 | Blob.new(:content => value.to_s) 117 | end 118 | end 119 | end 120 | 121 | # A Tree is a Hash which can store other instances of Tree and Blob. 122 | # 123 | # == Special methods 124 | # 125 | # Internally, a tree is being stored like this: 126 | # 127 | # { 128 | # "README" => blob, 129 | # "examples" => { 130 | # "test.rb" => blob, 131 | # "yay.rb" => blob 132 | # } 133 | # } 134 | # 135 | # So you have to do tree["examples"].delete("test.rb") instead 136 | # of tree.delete("examples/test.rb"). However, there are some 137 | # methods which supports the slash. All of these will work: 138 | # 139 | # tree["examples/test.rb"] 140 | # tree.fetch("examples/test.rb") 141 | # tree["examples/another.rb"] = "Content" 142 | # tree.store("examples/another.rb", "Content") # Exactly the same as above. 143 | # 144 | # tree["examples"]["test.rb"] # Or, you could use this 145 | # 146 | # == Documentation 147 | # 148 | # The point of Tree is that it should be as close to Hash as possible. 149 | # Therefore, methods which behaves exactly equally in Gash and Hash will 150 | # not be documentated below. Please see the Ruby documentation if you 151 | # wonder what you can do. 152 | # 153 | # See also: Helpers, Blob 154 | class Tree < Hash 155 | include Helpers 156 | 157 | # Retrieves the _value_ stored as +key+: 158 | # 159 | # tree["FILE"] == File.read("FILE") 160 | # tree["DIR/FILE"] == tree["DIR"]["FILE"] == File.read("DIR/FILE") 161 | # 162 | # ==== Lazy loading 163 | # 164 | # By default, this method will automatically load the blob/tree from 165 | # the repo. If you rather want to load it later, simply set +lazy+ to 166 | # +true+: 167 | # 168 | # blob = tree["FILE", true] 169 | # # do some other stuff... 170 | # blob.load! # Load it now! 171 | # 172 | def retrieve(key, lazy = nil) 173 | ret = fetch(key, default) 174 | ensure 175 | ret.load! if ret.respond_to?(:load!) && !lazy 176 | end 177 | 178 | alias [] retrieve 179 | alias / retrieve 180 | 181 | # Stores the given _value_ at +key+: 182 | # 183 | # tree["FILE"] = "Content" 184 | # 185 | # It uses Helpers#normalize in order convert it to a blob/tree, and will 186 | # always set the parent to itself: 187 | # 188 | # tree["FILE"] = "Content" 189 | # # is the same as: 190 | # tree["FILE"] = Gash::Blob.new(:content => "Content", :parent => tree) 191 | # 192 | # ==== Mark as changed 193 | # 194 | # By default, the object will be marked as changed (using 195 | # Helpers#changed!). If this is not what you want, simply set 196 | # +not_changed+ to +true+. 197 | # 198 | # However, if you give it three arguments, then the second one will act as 199 | # +not_changed+, not the third: 200 | # 201 | # 1 2 3 202 | # tree["FILE", true] = "Test" 203 | # tree["FILE"].changed? # => false 204 | # 205 | def store(key, value, not_changed = nil) 206 | key, value, not_changed = if not_changed.nil? 207 | [key, value] 208 | else 209 | [key, not_changed, value] 210 | end 211 | 212 | if key.include?("/") 213 | keys = key.split("/") 214 | name = keys.pop 215 | keys.inject(self) do |memo, i| 216 | memo[i, not_changed] = Tree.new(:parent => self) unless memo.include?(i) 217 | memo[i, true] 218 | end[name, not_changed] = value 219 | else 220 | value = normalize(value) 221 | value.parent = self 222 | super(key, value) 223 | end 224 | ensure 225 | self.changed! unless not_changed 226 | end 227 | 228 | alias []= store 229 | 230 | # Converts the tree to a Hash. 231 | def to_hash 232 | inject({}) do |memo, (key, value)| 233 | memo[key] = value.respond_to?(:to_hash) ? value.to_hash : value.to_s 234 | memo 235 | end 236 | end 237 | 238 | # :stopdoc: 239 | def fetch(*args) 240 | key = args.first.to_s 241 | 242 | case args.length 243 | when 1 244 | r = true 245 | when 2 246 | r = false 247 | default = args.last 248 | else 249 | raise ArgumentError, "wrong number of arguments (#{args.length} for 2)" 250 | end 251 | 252 | if key.include?("/") 253 | key, rest = key.split("/", 2) 254 | value = super(key) 255 | value.fetch(rest) 256 | else 257 | super(key) 258 | end 259 | rescue IndexError => e 260 | (r && raise(e)) || default 261 | end 262 | 263 | def delete(key) 264 | super && changed! 265 | end 266 | 267 | def self.[](*val) 268 | new.merge!(Hash[*val]) 269 | end 270 | 271 | def ==(other) 272 | if other.is_a?(Tree) && sha1 && other.sha1 273 | sha1 == other.sha1 274 | else 275 | super 276 | end 277 | end 278 | 279 | def merge(hash) 280 | tree = self.dup 281 | tree.merge!(hash) 282 | end 283 | 284 | def merge!(hash) 285 | hash.each do |key, value| 286 | self[key] = value 287 | end 288 | self 289 | end 290 | 291 | alias update merge! 292 | 293 | def replace(hash) 294 | if hash.is_a?(Gash::Tree) 295 | super 296 | else 297 | clear 298 | merge!(hash) 299 | end 300 | end 301 | # :startdoc: 302 | end 303 | 304 | # A Blob represent a string: 305 | # 306 | # blob = Gash::Blob.new(:content => "Some content") 307 | # blob # => "Some content" 308 | # 309 | # == Using SHA1 310 | # 311 | # However, if you provide a SHA1 (and have a parent which is connected to 312 | # a Gash-object) it will then load the content from the repo when needed: 313 | # 314 | # blob = Gash::Blob.new(:sha1 => "1234" * 10, :parent => gash_OR_tree_connected_to_gash) 315 | # blob # => # 316 | # blob.upcase # It's loaded when needed 317 | # #blob.load! # or forced with #load! 318 | # blob # => "Content of the blob" 319 | # 320 | # Tree#[]= automatically sets the parent to itself, so you don't need to 321 | # provide it then: 322 | # 323 | # tree["FILE"] = Gash::Blob.new(:sha1 => a_sha1) 324 | # 325 | # See also: Helpers, Tree 326 | class Blob < Delegator 327 | include Helpers, Comparable 328 | attr_accessor :content 329 | 330 | # Loads the file from Git, unless it's already been loaded. 331 | def load! 332 | @content ||= gash.send(:cat_file, @sha1) 333 | end 334 | 335 | def inspect #:nodoc: 336 | @content ? @content.inspect : (@sha1 ? "#" : to_s.inspect) 337 | end 338 | 339 | def <=>(other) #:nodoc: 340 | if other.is_a?(Blob) && sha1 && other.sha1 341 | sha1 <=> other.sha1 342 | else 343 | __getobj__ <=> other 344 | end 345 | end 346 | 347 | def __getobj__ #:nodoc: 348 | @content ||= @sha1 ? load! : '' 349 | end 350 | alias_method :to_s, :__getobj__ 351 | 352 | def __setobj__(value) #:nodoc: 353 | Blob.new(:content => value.to_s) 354 | end 355 | 356 | end 357 | 358 | # 359 | # 360 | # 361 | 362 | attr_accessor :branch, :repository 363 | 364 | # Opens the +repo+ with the specified +branch+. 365 | def initialize(repo = ".", branch = "master") 366 | @branch = branch 367 | @repository = repo 368 | @repository = find_repo(repo) 369 | __setobj__(Tree.new(:parent => self)) 370 | update! 371 | end 372 | 373 | def gash #:nodoc: 374 | self 375 | end 376 | 377 | # Fetch the latest data from Git; you can use this as a +clear+-method. 378 | def update! 379 | clear 380 | self.sha1 = git_tree_sha1 381 | git_tree do |line| 382 | line.strip! 383 | mode = line[0, 6] 384 | type = line[7] 385 | sha1 = line[12, 40] 386 | name = line[53..-1] 387 | name = name[/[^\/]+$/] 388 | parent = if $`.empty? 389 | self 390 | else 391 | self[$`.chomp("/")] 392 | end 393 | parent[name, true] = case type 394 | when ?b 395 | Blob.new(:sha1 => sha1, :mode => mode) 396 | when ?t 397 | Tree.new(:sha1 => sha1, :mode => mode) 398 | end if parent 399 | end 400 | self 401 | end 402 | 403 | # Commit the current changes and returns the commit-hash. 404 | # 405 | # Returns +nil+ if nothing has changed. 406 | def commit(msg) 407 | return unless changed? 408 | commit = commit_tree(to_tree!, msg) 409 | @sha1 = git_tree_sha1 410 | commit 411 | end 412 | 413 | # Checks if the current branch exists 414 | def branch_exists? 415 | git_status('rev-parse', @branch) == 0 416 | end 417 | 418 | def inspect #:nodoc: 419 | __getobj__.inspect 420 | end 421 | undef_method :dup 422 | 423 | #private 424 | 425 | def find_repo(dir) 426 | Dir.chdir(dir) do 427 | File.expand_path(git('rev-parse', '--git-dir', :git_dir => false)) 428 | end 429 | rescue Errno::ENOENT, Gash::Errors::Git 430 | raise Errors::NoGitRepo.new("No Git repository at: " + @repository) 431 | end 432 | 433 | def cat_file(blob) 434 | git('cat-file', 'blob', blob) 435 | end 436 | 437 | def to_tree!(from = self) 438 | input = [] 439 | from.each do |key, value| 440 | if value.tree? 441 | value.sha1 ||= to_tree!(value) 442 | value.mode ||= "040000" 443 | input << "#{value.mode} tree #{value.sha1}\t#{key}\0" 444 | else 445 | value.sha1 ||= git('hash-object', '-w', '--stdin', :input => value.to_s) 446 | value.mode ||= "100644" 447 | input << "#{value.mode} blob #{value.sha1}\t#{key}\0" 448 | end 449 | end 450 | git('mktree', '-z', :input => input) 451 | end 452 | 453 | def update_head(new_head) 454 | git('update-ref', 'refs/heads/%s' % @branch, new_head) 455 | end 456 | 457 | def commit_tree(tree, msg) 458 | if branch_exists? 459 | commit = git('commit-tree', tree, '-p', @branch, :input => msg) 460 | update_head(commit) 461 | else 462 | commit = git('commit-tree', tree, :input => msg) 463 | git('branch', @branch, commit) 464 | end 465 | commit 466 | end 467 | 468 | def git_tree(&blk) 469 | git('ls-tree', '-r', '-t', '-z', @branch).split("\0").each(&blk) 470 | rescue Errors::Git 471 | "" 472 | end 473 | 474 | def git_tree_sha1(from = @branch) 475 | git('rev-parse', @branch + '^{tree}') 476 | rescue Errors::Git 477 | end 478 | 479 | def method_missing(meth, *args, &blk) 480 | target = self.__getobj__ 481 | unless target.respond_to?(meth) 482 | Object.instance_method(:method_missing).bind(self).call(meth, *args, &blk) 483 | end 484 | target.__send__(meth, *args, &blk) 485 | end 486 | 487 | # passes the command over to git 488 | # 489 | # ==== Parameters 490 | # cmd:: the git command to execute 491 | # *rest:: any number of String arguments to the command, followed by an options hash 492 | # &block:: if you supply a block, you can communicate with git throught a pipe. NEVER even think about closing the stream! 493 | # 494 | # ==== Options 495 | # :strip:: true to strip the output String#strip, false not to to it 496 | # 497 | # ==== Raises 498 | # Errors::Git:: if git returns non-null, an Exception is raised 499 | # 500 | # ==== Returns 501 | # String:: if you didn't supply a block, the things git said on STDOUT, otherwise noting 502 | def git(cmd, *rest, &block) 503 | result, reserr, status = run_git(cmd, *rest, &block) 504 | 505 | if status != 0 506 | raise Errors::Git.new("Error: #{cmd} returned #{status}. STDERR: #{reserr}") 507 | end 508 | result 509 | end 510 | 511 | 512 | # passes the command over to git and returns its status ($?) 513 | # 514 | # ==== Parameters 515 | # cmd:: the git command to execute 516 | # *rest:: any number of String arguments to the command, followed by an options hash 517 | # &block:: if you supply a block, you can communicate with git throught a pipe. NEVER even think about closing the stream! 518 | # 519 | # ==== Returns 520 | # Integer:: the return status of git 521 | def git_status(cmd, *rest, &block) 522 | run_git(cmd, *rest, &block)[2] 523 | end 524 | 525 | # passes the command over to git (you should not call this directly) 526 | # 527 | # ==== Parameters 528 | # cmd:: the git command to execute 529 | # *rest:: any number of String arguments to the command, followed by an options hash 530 | # &block:: if you supply a block, you can communicate with git throught a pipe. NEVER even think about closing the stream! 531 | # 532 | # ==== Options 533 | # :strip:: true to strip the output String#strip, false not to to it 534 | # :git_dir:: true to automatically use @repository as git-dir, false to not use anything. 535 | # 536 | # ==== Raises 537 | # Errors::Git:: if git returns non-null, an Exception is raised 538 | # 539 | # ==== Returns 540 | # Array[String, String, Integer]:: the first item is the STDOUT of git, the second is the STDERR, the third is the return-status 541 | def run_git(cmd, *args, &block) 542 | options = if args.last.kind_of?(Hash) 543 | args.pop 544 | else 545 | {} 546 | end 547 | options[:strip] = true unless options.key?(:strip) 548 | 549 | git_cmd = ["git"] 550 | 551 | unless options[:git_dir] == false 552 | git_cmd.push("--git-dir", @repository) 553 | end 554 | 555 | git_cmd.push(cmd, *args) 556 | 557 | result = "" 558 | reserr = "" 559 | status = Open4.popen4(*git_cmd) do |pid, stdin, stdout, stderr| 560 | if input = options.delete(:input) 561 | raw = input.is_a?(Array) ? input.join : input 562 | stdin.write(raw) 563 | elsif block_given? 564 | yield stdin 565 | end 566 | stdin.close_write 567 | 568 | result = "" 569 | reserr = "" 570 | 571 | while !stdout.eof 572 | result << stdout.read 573 | end 574 | 575 | while !stderr.eof 576 | reserr << stderr.read 577 | end 578 | end 579 | 580 | result.strip! if options[:strip] == true 581 | 582 | [result, reserr, status] 583 | end 584 | end 585 | --------------------------------------------------------------------------------