From fde3263abc5c7866aa7dce7aef28eacaa33d7664 Mon Sep 17 00:00:00 2001 From: scott Chacon Date: Sat, 10 Nov 2007 12:43:33 -0800 Subject: few hours work - diff is done --- EXAMPLES | 44 ++++++++++------ lib/git.rb | 14 +++-- lib/git/base.rb | 51 ++++++++++++++++--- lib/git/branches.rb | 3 +- lib/git/diff.rb | 124 +++++++++++++++++++++++++++++++++++++++++---- lib/git/lib.rb | 83 +++++++++++++++++++++++++----- lib/git/object.rb | 10 ++-- lib/git/path.rb | 4 +- tests/files/working | 2 +- tests/test_helper.rb | 4 +- tests/units/test_config.rb | 26 ++++++++++ tests/units/test_diff.rb | 67 ++++++++++++++++++++++-- tests/units/test_init.rb | 31 ++++++++++++ 13 files changed, 395 insertions(+), 68 deletions(-) create mode 100644 tests/units/test_config.rb diff --git a/EXAMPLES b/EXAMPLES index bea9be7..6dbfa6c 100644 --- a/EXAMPLES +++ b/EXAMPLES @@ -38,43 +38,57 @@ g.grep('hello') # implies HEAD g.blob('v2.5:Makefile').grep('hello') g.tag('v2.5').grep('hello', 'docs/') +g.diff(commit1, commit2).size +g.diff(commit1, commit2).stats +g.tree('v2.5').diff('v2.6').insertions +g.diff('gitsearch1', 'v2.5').path('lib/') +g.diff('gitsearch1', @git.tree('v2.5')) +g.diff('gitsearch1', 'v2.5').path('docs/').patch +g.tree('v2.5').diff('v2.6').patch + +g.tree('v2.5').diff('v2.6').each do |file_diff| + puts file_diff.path + puts file_diff.patch + puts file_diff.blob(:src).contents +end + +g.config('user.name') # returns 'Scott Chacon' +g.config # returns whole config hash ***** IMPLEMENTED ***** -g.diff -g.diff.shortstat -g.diff.summary -g.diff(commit1, commit2) -g.diff("commit1..commit2") +g.ls_files +g.ls_files(:stage => true) + +g.tag # returns array of Git::Tag objects + -g.diff_tree(Git::Tree, Git::Tree) -g.status -g.ls_files -g.ls_files(:stage => true) -g.tag # returns array of Git::Tag objects # needs write permission -g = Git.clone(URI, 'name', working_dir = GIT_DIR, {options}) - (username, password, ssl_key, git_dir, index_file) g = Git.init Git.init('project') Git.init('/home/schacon/proj', { :git_dir => '/opt/git/proj.git', :index_file => '/tmp/index'} ) + + +***** IMPLEMENTED ***** + +g = Git.clone(URI, :name => 'name', :path => '/tmp/checkout' + (username, password, ssl_key, git_dir, index_file) + g.config('user.name', 'Scott Chacon') -g.config('user.email', 'email@email.com') -g.config('user.name') # returns 'Scott Chacon' -g.config # returns whole config hash +g.config('user.email', 'email@email.com') g.add('.') g.add([file1, file2]) diff --git a/lib/git.rb b/lib/git.rb index a43aee2..ec52c54 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -19,13 +19,11 @@ require 'git/branches' require 'git/branch' require 'git/remote' +require 'git/diff' =begin require 'git/author' require 'git/file' -require 'git/diff' -require 'git/remote' - require 'git/sha' require 'git/ref' =end @@ -40,12 +38,12 @@ module Git Base.open(working_dir, options) end - def clone - Base.clone() + def self.init(working_dir = '.', options = {}) + Base.init(working_dir, options) end - def init(working_dir = '.') - Base.clone() + def self.clone(uri, options = {}) + Base.clone(working_dir, options) end - + end diff --git a/lib/git/base.rb b/lib/git/base.rb index 8170065..1111887 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -22,19 +22,33 @@ module Git self.new(git_options) end - def initialize(options = {}) - @working_directory = Git::Repository.new(options[:working_directory]) if options[:working_directory] - @repository = Git::Repository.new(options[:repository]) if options[:repository] - @index = Git::Index.new(options[:index]) if options[:index] + def self.init(working_dir, opts = {}) + default = {:working_directory => working_dir, + :repository => File.join(working_dir, '.git')} + git_options = default.merge(opts) + + if git_options[:working_directory] + # if !working_dir, make it + FileUtils.mkdir_p(git_options[:working_directory]) if !File.directory?(git_options[:working_directory]) + end + + # run git_init there + Git::Lib.new(git_options).init + + self.new(git_options) end - + def self.clone raise NotImplementedError end - - def self.init - raise NotImplementedError + + def initialize(options = {}) + @working_directory = Git::WorkingDirectory.new(options[:working_directory]) if options[:working_directory] + @repository = Git::Repository.new(options[:repository]) if options[:repository] + @index = Git::Index.new(options[:index]) if options[:index] end + + def dir @@ -49,6 +63,23 @@ module Git @index end + + #g.config('user.name', 'Scott Chacon') # sets value + #g.config('user.email', 'email@email.com') # sets value + #g.config('user.name') # returns 'Scott Chacon' + #g.config # returns whole config hash + def config(name = nil, value = nil) + if(name && value) + # set value + elsif (name) + # return value + lib.config_get(name) + else + # return hash + lib.config_list + end + end + # factory methods def object(objectish) @@ -75,6 +106,10 @@ module Git self.object('HEAD').grep(string) end + def diff(objectish = 'HEAD', obj2 = nil) + Git::Diff.new(self, objectish, obj2) + end + # convenience methods def revparse(objectish) diff --git a/lib/git/branches.rb b/lib/git/branches.rb index 81abe22..72dff0a 100644 --- a/lib/git/branches.rb +++ b/lib/git/branches.rb @@ -11,8 +11,9 @@ module Git @branches = {} @base = base + @base.lib.branches_all.each do |b| - @branches[b.full] = b + @branches[b[0]] = Git::Branch.new(@base, b[0], b[1]) end end diff --git a/lib/git/diff.rb b/lib/git/diff.rb index 3686edb..b43ecd9 100644 --- a/lib/git/diff.rb +++ b/lib/git/diff.rb @@ -7,31 +7,137 @@ module Git @base = nil @from = nil @to = nil + @path = nil @full_diff = nil + @full_diff_files = nil + @stats = nil def initialize(base, from = nil, to = nil) - dirty_log @base = base - @from = from - @to = to + @from = from.to_s + @to = to.to_s end - def + def path(path) + @path = path + return self + end + + def size + cache_stats + @stats[:total][:files] + end + + def lines + cache_stats + @stats[:total][:lines] + end + + def deletions + cache_stats + @stats[:total][:deletions] + end + + def insertions + cache_stats + @stats[:total][:insertions] + end + + def stats + cache_stats + @stats + end + + # if file is provided and is writable, it will write the patch into the file + def patch(file = nil) + cache_full + @full_diff + end + alias_method :to_s, :patch + # enumerable methods + def [](key) + process_full + @full_diff_files.assoc(key)[1] + end + def each - cache_diff - @full_diff.each do |file| - yield file + process_full + @full_diff_files.each do |file| + yield file[1] + end + end + + class DiffFile + attr_accessor :patch, :path, :mode, :src, :dst, :type + @base = nil + + def initialize(base, hash) + @base = base + @patch = hash[:patch] + @path = hash[:path] + @mode = hash[:mode] + @src = hash[:src] + @dst = hash[:dst] + @type = hash[:type] + end + + def blob(type = :dst) + if type == :src + @base.object(@src) if @src != '0000000' + else + @base.object(@dst) if @dst != '0000000' + end end end private - def cache_diff + def cache_full if !@full_diff - @full_diff = @base.lib.diff_files(@from, @to) + @full_diff = @base.lib.diff_full(@from, @to, {:path_limiter => @path}) + end + end + + def process_full + if !@full_diff_files + cache_full + @full_diff_files = process_full_diff + end + end + + def cache_stats + if !@stats + @stats = @base.lib.diff_stats(@from, @to, {:path_limiter => @path}) + end + end + + # break up @diff_full + def process_full_diff + final = {} + current_file = nil + @full_diff.split("\n").each do |line| + if m = /diff --git a\/(.*?) b\/(.*?)/.match(line) + current_file = m[1] + final[current_file] = {:patch => line, :path => current_file, + :mode => '', :src => '', :dst => '', :type => 'modified'} + else + if m = /index (.......)\.\.(.......)( ......)*/.match(line) + final[current_file][:src] = m[1] + final[current_file][:dst] = m[2] + final[current_file][:mode] = m[3].strip if m[3] + end + if m = /(.*?) file mode (......)/.match(line) + final[current_file][:type] = m[1] + final[current_file][:mode] = m[2] + end + final[current_file][:patch] << "\n" + line + end end + final.map { |e| [e[0], DiffFile.new(@base, e[1])] } end + + end end \ No newline at end of file diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 2937d52..8bb518b 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -3,15 +3,26 @@ module Git class GitExecuteError < StandardError end - class GitNoOutput < StandardError - end - class Lib - @base = nil - + @git_dir = nil + @git_index_file = nil + @git_work_dir = nil + def initialize(base) - @base = base + if base.is_a?(Git::Base) + @git_dir = base.repo.path + @git_index_file = base.index.path + @git_work_dir = base.dir.path + elsif base.is_a?(Hash) + @git_dir = base[:repository] + @git_index_file = base[:index] + @git_work_dir = base[:working_directory] + end + end + + def init + command('init') end def log_commits(opts = {}) @@ -42,13 +53,28 @@ module Git end def branches_all - command_lines('branch', '-a').map do |b| + arr = [] + command_lines('branch', '-a').each do |b| current = false current = true if b[0, 2] == '* ' - Git::Branch.new(@base, b.gsub('* ', '').strip, current) + arr << [b.gsub('* ', '').strip, current] end + arr + end + + def config_get(name) + command('config', ['--get', name]) end + def config_list + hsh = {} + command_lines('config', ['--list']).each do |line| + (key, value) = line.split('=') + hsh[key] = value + end + hsh + end + def config_remote(name) hsh = {} command_lines('config', ['--get-regexp', "remote.#{name}"]).each do |line| @@ -68,7 +94,7 @@ module Git grep_opts << '-i' if opts[:ignore_case] grep_opts << '-v' if opts[:invert_match] grep_opts << "-e '#{string}'" - grep_opts << opts[:object] if opts[:object].is_a? String + grep_opts << opts[:object] if opts[:object].is_a?(String) grep_opts << ('-- ' + opts[:path_limiter]) if opts[:path_limiter].is_a? String hsh = {} command_lines('grep', grep_opts).each do |line| @@ -80,17 +106,46 @@ module Git hsh end + def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {}) + diff_opts = ['-p'] + diff_opts << obj1 + diff_opts << obj2 if obj2.is_a?(String) + diff_opts << ('-- ' + opts[:path_limiter]) if opts[:path_limiter].is_a? String + + command('diff', diff_opts) + end + + def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {}) + diff_opts = ['--numstat'] + diff_opts << obj1 + diff_opts << obj2 if obj2.is_a?(String) + diff_opts << ('-- ' + opts[:path_limiter]) if opts[:path_limiter].is_a? String + + hsh = {:total => {:insertions => 0, :deletions => 0, :lines => 0, :files => 0}, :files => {}} + + command_lines('diff', diff_opts).each do |file| + (insertions, deletions, filename) = file.split("\t") + hsh[:total][:insertions] += insertions.to_i + hsh[:total][:deletions] += deletions.to_i + hsh[:total][:lines] = (hsh[:total][:deletions] + hsh[:total][:insertions]) + hsh[:total][:files] += 1 + hsh[:files][filename] = {:insertions => insertions.to_i, :deletions => deletions.to_i} + end + + hsh + end + private def command_lines(cmd, opts) command(cmd, opts).split("\n") end - def command(cmd, opts) - ENV['GIT_DIR'] = @base.repo.path - ENV['GIT_INDEX_FILE'] = @base.index.path - ENV['GIT_WORK_DIR'] = @base.dir.path - Dir.chdir(@base.dir.path) do + def command(cmd, opts = {}) + ENV['GIT_DIR'] = @git_dir + ENV['GIT_INDEX_FILE'] = @git_index_file if @git_index_file + ENV['GIT_WORK_DIR'] = @git_work_dir if @git_work_dir + Dir.chdir(@git_work_dir || @git_dir) do opts = opts.to_a.join(' ') #puts "git #{cmd} #{opts}" out = `git #{cmd} #{opts} 2>&1`.chomp diff --git a/lib/git/object.rb b/lib/git/object.rb index f2d4114..f100597 100644 --- a/lib/git/object.rb +++ b/lib/git/object.rb @@ -2,7 +2,7 @@ module Git class Object class AbstractObject - attr_accessor :sha, :size, :type + attr_accessor :sha, :size, :type, :mode @base = nil @@ -26,7 +26,7 @@ module Git end def to_s - "#{@type.ljust(6)} #{@sha}" + @sha end def grep(string, path_limiter = nil, opts = {}) @@ -35,8 +35,12 @@ module Git @base.lib.grep(string, grep_options) end + def diff(objectish) + Git::Diff.new(@base, @sha, objectish) + end + def log(count = 30) - Git::Log.new(self, count).object(@sha) + Git::Log.new(@base, count).object(@sha) end end diff --git a/lib/git/path.rb b/lib/git/path.rb index 9d6ba49..18845fa 100644 --- a/lib/git/path.rb +++ b/lib/git/path.rb @@ -3,8 +3,8 @@ module Git attr_accessor :path - def initialize(path) - if File.exists?(path) + def initialize(path, check_path = true) + if !check_path || File.exists?(path) @path = path else raise ArgumentError, "path does not exist", path diff --git a/tests/files/working b/tests/files/working index 935badc..5e53019 160000 --- a/tests/files/working +++ b/tests/files/working @@ -1 +1 @@ -Subproject commit 935badc874edd62a8629aaf103418092c73f0a56 +Subproject commit 5e53019b3238362144c2766f02a2c00d91fcc023 diff --git a/tests/test_helper.rb b/tests/test_helper.rb index fe8c34d..03b5f8a 100644 --- a/tests/test_helper.rb +++ b/tests/test_helper.rb @@ -19,14 +19,14 @@ class Test::Unit::TestCase @index = File.join(@test_dir, 'index') end - def in_temp_dir + def in_temp_dir(remove_after = true) filename = 'git_test' + Time.now.to_i.to_s + rand(300).to_s tmp_path = File.join("/tmp/", filename) FileUtils.mkdir(tmp_path) Dir.chdir tmp_path do yield tmp_path end - FileUtils.rm_r(tmp_path) + FileUtils.rm_r(tmp_path) if remove_after end end \ No newline at end of file diff --git a/tests/units/test_config.rb b/tests/units/test_config.rb new file mode 100644 index 0000000..5adb391 --- /dev/null +++ b/tests/units/test_config.rb @@ -0,0 +1,26 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' + +class TestBranch < Test::Unit::TestCase + def setup + set_file_paths + @git = Git.open(@wdir) + end + + def test_config + c = @git.config + assert_equal('scott Chacon', c['user.name']) + assert_equal('false', c['core.bare']) + end + + def test_read_config + assert_equal('scott Chacon', @git.config('user.name')) + assert_equal('false', @git.config('core.bare')) + end + + def test_set_config + # !! TODO !! + end + +end \ No newline at end of file diff --git a/tests/units/test_diff.rb b/tests/units/test_diff.rb index b8ed6b8..b989c8a 100644 --- a/tests/units/test_diff.rb +++ b/tests/units/test_diff.rb @@ -6,24 +6,81 @@ class TestDiff < Test::Unit::TestCase def setup set_file_paths @git = Git.open(@wdir) + @diff = @git.diff('gitsearch1', 'v2.5') end - def test_diff + #def test_diff + # g.diff + # assert(1, d.size) + #end + + def test_diff_tags + d = @git.diff('gitsearch1', 'v2.5') + assert_equal(3, d.size) + assert_equal(74, d.lines) + assert_equal(10, d.deletions) + assert_equal(64, d.insertions) + end + + def test_diff_path + d = @git.diff('gitsearch1', 'v2.5').path('scott/') + assert_equal(2, d.size) + assert_equal(9, d.lines) + assert_equal(9, d.deletions) + assert_equal(0, d.insertions) end - def test_diff_summary + def test_diff_objects + d = @git.diff('gitsearch1', @git.tree('v2.5')) + assert(3, d.size) end - def test_diff_stat + def test_object_diff + d = @git.tree('v2.5').diff('gitsearch1') + assert_equal(3, d.size) + assert_equal(74, d.lines) + assert_equal(10, d.insertions) + assert_equal(64, d.deletions) + + d = @git.tree('v2.6').diff(@git.tree('gitsearch1')) + assert_equal(2, d.size) + assert_equal(9, d.lines) end - def test_diff_shortstat + def test_diff_stats + s = @diff.stats + assert_equal(3, s[:total][:files]) + assert_equal(74, s[:total][:lines]) + assert_equal(10, s[:total][:deletions]) + assert_equal(64, s[:total][:insertions]) + + # per file + assert_equal(1, s[:files]["scott/newfile"][:deletions]) + end + + def test_diff_hashkey + assert_equal('5d46068', @diff["scott/newfile"].src) + assert_nil(@diff["scott/newfile"].blob(:dst)) + assert(@diff["scott/newfile"].blob(:src).is_a?(Git::Object::Blob)) end def test_patch + p = @git.diff('v2.8^', 'v2.8').patch + diff = "diff --git a/example.txt b/example.txt\nindex 1f09f2e..8dc79ae 100644\n--- a/example.txt\n+++ b/example.txt\n@@ -1 +1 @@\n-replace with new text\n+replace with new text - diff test" + assert_equal(diff, p) end - def test_unified + def test_diff_each + files = {} + @diff.each do |d| + files[d.path] = d + end + + assert(files['example.txt']) + assert_equal('100644', files['scott/newfile'].mode) + assert_equal('deleted', files['scott/newfile'].type) + assert_equal(160, files['scott/newfile'].patch.size) end + end \ No newline at end of file diff --git a/tests/units/test_init.rb b/tests/units/test_init.rb index 56d2a18..f1a8ba4 100644 --- a/tests/units/test_init.rb +++ b/tests/units/test_init.rb @@ -24,6 +24,37 @@ class TestInit < Test::Unit::TestCase g = Git.repo @wbare assert_equal(g.repo.path, @wbare) end + + #g = Git.init + # Git.init('project') + # Git.init('/home/schacon/proj', + # { :git_dir => '/opt/git/proj.git', + # :index_file => '/tmp/index'} ) + def test_git_init + in_temp_dir do |path| + Git.init + assert(File.directory?(File.join(path, '.git'))) + assert(File.exists?(File.join(path, '.git', 'config'))) + end + end + + def test_git_init_remote_git + in_temp_dir do |dir| + assert(!File.exists?(File.join(dir, 'config'))) + + in_temp_dir do |path| + Git.init(path, :repository => dir) + assert(File.exists?(File.join(dir, 'config'))) + end + end + end + + def test_git_clone + in_temp_dir do |path| + Git.clone(uri, :repository => dir) + assert(File.exists?(File.join(dir, 'config'))) + end + end # trying to open a git project using a bare repo - rather than using Git.repo def test_git_open_error -- cgit