diff --git a/lib/git.rb b/lib/git.rb index 9dcc79ce..2dd91752 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -26,6 +26,8 @@ require 'git/stashes' require 'git/stash' +require 'git/stream' + # Git/Ruby Library # diff --git a/lib/git/author.rb b/lib/git/author.rb index 545abb9b..e9476f79 100644 --- a/lib/git/author.rb +++ b/lib/git/author.rb @@ -2,7 +2,7 @@ module Git class Author attr_accessor :name, :email, :date - def initialize(author_string) + def initialize(author_string = nil) if m = /(.*?) <(.*?)> (\d+) (.*)/.match(author_string) @name = m[1] @email = m[2] @@ -10,5 +10,12 @@ def initialize(author_string) end end + def self.from_parts(name, email, date = Time.now) + a = Author.new + a.name = name + a.email = email + a.date = date + return a + end end end \ No newline at end of file diff --git a/lib/git/base.rb b/lib/git/base.rb index 342796e2..e7c3fd03 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -364,6 +364,14 @@ def archive(treeish, file = nil, opts = {}) self.object(treeish).archive(file, opts) end + # use a stream to import into the repository + def import_stream + stream = Git::Stream.new + yield stream + + self.lib.fast_import(stream) + end + # repacks the repository def repack self.lib.repack diff --git a/lib/git/lib.rb b/lib/git/lib.rb index cffeffbd..437786aa 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -581,6 +581,17 @@ def checkout_index(opts = {}) command('checkout-index', arr_opts) end + def fast_import(stream) + tmp = Tempfile.new("stream-file") + tmp.write(stream.to_s) + tmp.flush + begin + command('fast-import',"--date-format=rfc2822",true,"< #{escape tmp.path}") + ensure + tmp.close(true) + end + end + # creates an archive file # # options diff --git a/lib/git/stream.rb b/lib/git/stream.rb new file mode 100644 index 00000000..00f7e106 --- /dev/null +++ b/lib/git/stream.rb @@ -0,0 +1,189 @@ +module Git + + require 'time' + + #==================== + # Stream Classes + # + # These classes must support the following methods + # + # to_s - write the command to the git protocol stream + #==================== + + class StreamCommit + + + attr_accessor :branch, :mark, :author, :committer, :message, :ancestor, :changes + + def initialize() + @branch = nil + @mark = StreamMark.new + @author = nil + @committer = nil + @message = nil + @ancestor = nil + @changes = [] + end + + def modify_file(repos_path, data, mode = nil) + sfm = StreamFileModify.new(repos_path, data) + sfm.mode = mode unless mode == nil + changes << sfm + end + + def delete_file(repos_path) + changes << StreamFileDelete.new(repos_path) + end + + def rename_file(repos_path_from, repos_path_to) + changes << StreamFileRename.new(repos_path_form, repos_path_to) + end + + def copy_file(repos_path_from, repos_path_to) + changes << StreamFileCopy.new(repos_path_form, repos_path_to) + end + + def delete_all_files() + changes << StreamFileDeleteAll.new + end + + def to_s + out = "commit refs/heads/#{branch.to_s}\n" + out << "mark #{mark}\n" + out << "author #{author.name} <#{author.email}> #{author.date.rfc2822}\n" unless author == nil + out << "committer #{committer.name} <#{committer.email}> #{committer.date.rfc2822}\n" unless committer == nil + if (message == nil) + out << StreamData.emit_empty_data + else + out << StreamData.emit_inline_data(message) + end + out << "from #{ancestor}\n" unless ancestor == nil + changes.each do |c| + out << c.to_s + end + out << "\n" + end + end + + class StreamMark + + @@mark_counter = 1 + + def initialize(id = (@@mark_counter += 1)) + @id = id + end + + def to_s + ":#{@id}" + end + end + + # This class is used in the filemodify change on the commit stream + # At this time only the inline mode data stream is supported + class StreamFileModify + + attr_accessor :mode, :repository_path, :inline_data + + def initialize(repository_path, data) + @mode = 100644 + @repository_path = repository_path + @inline_data = data + end + + def to_s + "M #{mode} inline #{repository_path}\n#{StreamData.emit_inline_data(inline_data)}" + end + end + + class StreamFileDelete + + attr_accessor :repository_path + + def initialize(repository_path) + @repository_path = repository_path + end + + def to_s + "D #{repository_path}\n" + end + end + + class StreamFileCopy + + attr_accessor :repository_path_from, :repository_path_to + + def initialize(repository_path_from, repository_path_to) + @repository_path_from = repository_path_from + @repository_path_to = repository_path_to + end + + def to_s + "C #{repository_path_from} #{repository_path_to}\n" + end + + end + + class StreamFileRename + + attr_accessor :repository_path_from, :repository_path_to + + def initialize(repository_path_from,repository_path_to) + @repository_path_from = repository_path_from + @repository_path_to = repository_path_to + end + + def to_s + "R #{repository_path_from} #{repository_path_to}\n" + end + + end + + class StreamFileDeleteAll + + def to_s + "deleteall\n" + end + end + + # Represents a stream of data bytes in the git stream + class StreamData + + def self.emit_inline_data(data_string) + "data #{data_string.length}\n#{data_string}\n" + end + + def self.emit_empty_data + "data 0\n\n" + end + end + + #==================== + # Stream Implementation + #==================== + + # This is an initial implementation of git fast-import/export streams + # It is not complete! + class Stream + + attr_reader :commands + + def initialize + @commands = [] + end + + def commit + cmt = Git::StreamCommit.new + yield cmt + commands << cmt + end + + def to_s + s = "" + commands.each do |cmd| + s << cmd.to_s + end + return s + end + end + +end \ No newline at end of file diff --git a/ruby-git.gemspec b/ruby-git.gemspec index e68d909b..ffb4d611 100644 --- a/ruby-git.gemspec +++ b/ruby-git.gemspec @@ -1,11 +1,11 @@ spec = Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.name = "git" - s.version = "1.1.1" + s.version = "1.1.4" s.author = "Scott Chacon" s.email = "schacon@gmail.com" s.summary = "A package for using Git in Ruby code." - s.files = ["lib/git", "lib/git/author.rb", "lib/git/base.rb", "lib/git/branch.rb", "lib/git/branches.rb", "lib/git/diff.rb", "lib/git/index.rb", "lib/git/lib.rb", "lib/git/log.rb", "lib/git/object.rb", "lib/git/path.rb", "lib/git/remote.rb", "lib/git/repository.rb", "lib/git/stash.rb", "lib/git/stashes.rb", "lib/git/status.rb", "lib/git/working_directory.rb", "lib/git.rb"] + s.files = ["lib/git", "lib/git/author.rb", "lib/git/base.rb", "lib/git/branch.rb", "lib/git/branches.rb", "lib/git/diff.rb", "lib/git/index.rb", "lib/git/lib.rb", "lib/git/log.rb", "lib/git/object.rb", "lib/git/path.rb", "lib/git/remote.rb", "lib/git/repository.rb", "lib/git/stash.rb", "lib/git/stashes.rb", "lib/git/status.rb", "lib/git/stream.rb", "lib/git/working_directory.rb", "lib/git.rb"] s.require_path = "lib" s.autorequire = "git" s.test_files = Dir.glob('tests/*.rb') diff --git a/tests/units/test_stream.rb b/tests/units/test_stream.rb new file mode 100644 index 00000000..d58f178e --- /dev/null +++ b/tests/units/test_stream.rb @@ -0,0 +1,172 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' + +class TestStream < Test::Unit::TestCase + def setup + set_file_paths + end + + def test_minimal_commit_stream + t = Time.now + + commit = Git::StreamCommit.new + commit.branch = "master" + commit.committer = Git::Author.from_parts("Arthur Developer","arthur@example.com",t) + + str = "commit refs/heads/master\n" + str << "mark #{commit.mark}\n" + str << "committer Arthur Developer #{t.rfc2822}\n" + str << "data 0\n" + str << "\n\n" + + assert_equal str, commit.to_s + end + + def test_single_file_add + t = Time.now + + commit = Git::StreamCommit.new + commit.branch = "master" + commit.author = Git::Author.from_parts("Arthur Developer","arthur@example.com",t) + commit.committer = Git::Author.from_parts("Jane Developer","jane@example.com",t+10) + commit.message = "Adding a single file." + commit.ancestor = "2e937ac5d5a6e95f4abb9f636273eaa6528f5dae" + commit.changes << Git::StreamFileModify.new("p/e/added-file.txt","This is the contents of the\nadded file.") + + str = "commit refs/heads/master\n" + str << "mark #{commit.mark}\n" + str << "author Arthur Developer #{t.rfc2822}\n" + str << "committer Jane Developer #{(t+10).rfc2822}\n" + str << "data 21\n" + str << "Adding a single file.\n" + str << "from 2e937ac5d5a6e95f4abb9f636273eaa6528f5dae\n" + str << "M 100644 inline p/e/added-file.txt\n" + str << "data 39\n" + str << "This is the contents of the\nadded file.\n" + str << "\n" + + assert_equal str, commit.to_s + end + + def test_single_file_add + t = Time.now + + commit = Git::StreamCommit.new + commit.branch = "master" + commit.author = Git::Author.from_parts("Arthur Developer","arthur@example.com",t) + commit.committer = Git::Author.from_parts("Jane Developer","jane@example.com",t+10) + commit.message = "Adding a single file." + commit.ancestor = "2e937ac5d5a6e95f4abb9f636273eaa6528f5dae" + commit.changes << Git::StreamFileModify.new("p/e/added-file.txt","This is the contents of the\nadded file.") + + str = "commit refs/heads/master\n" + str << "mark #{commit.mark}\n" + str << "author Arthur Developer #{t.rfc2822}\n" + str << "committer Jane Developer #{(t+10).rfc2822}\n" + str << "data 21\n" + str << "Adding a single file.\n" + str << "from 2e937ac5d5a6e95f4abb9f636273eaa6528f5dae\n" + str << "M 100644 inline p/e/added-file.txt\n" + str << "data 39\n" + str << "This is the contents of the\nadded file.\n" + str << "\n" + + assert_equal str, commit.to_s + end + + def test_multiple_changes + t = Time.now + + commit = Git::StreamCommit.new + commit.branch = "master" + commit.author = Git::Author.from_parts("Arthur Developer","arthur@example.com",t) + commit.committer = Git::Author.from_parts("Jane Developer","jane@example.com",t+10) + commit.message = "Add/Delete/Rename/Copy/DeleteAll." + commit.ancestor = "2e937ac5d5a6e95f4abb9f636273eaa6528f5dae" + commit.changes << Git::StreamFileModify.new("p/e/added-file.txt","This is the contents of the\nadded file.") + commit.changes << Git::StreamFileDelete.new("del-file.txt") + commit.changes << Git::StreamFileRename.new("file1.txt","file2.txt") + commit.changes << Git::StreamFileCopy.new("file2.txt","file3.txt") + commit.changes << Git::StreamFileDeleteAll.new + + str = "commit refs/heads/master\n" + str << "mark #{commit.mark}\n" + str << "author Arthur Developer #{t.rfc2822}\n" + str << "committer Jane Developer #{(t+10).rfc2822}\n" + str << "data 33\n" + str << "Add/Delete/Rename/Copy/DeleteAll.\n" + str << "from 2e937ac5d5a6e95f4abb9f636273eaa6528f5dae\n" + str << "M 100644 inline p/e/added-file.txt\n" + str << "data 39\n" + str << "This is the contents of the\nadded file.\n" + str << "D del-file.txt\n" + str << "R file1.txt file2.txt\n" + str << "C file2.txt file3.txt\n" + str << "deleteall\n" + str << "\n" + + assert_equal str, commit.to_s + end + + def test_temp_repo_baseline + create_temp_repo_with_branched_data do |g| + assert_equal 'blahblahblah3', g.cat_file('other_branch:test-file1') + assert_equal 'blahblahblah2', g.cat_file('other_branch:test-file2') + assert_equal 'blahblahblah1', g.cat_file('new_branch:test-file1') + assert_equal 'blahblahblah2', g.cat_file('new_branch:test-file2') + end + end + + def test_add_file_to_different_branch + create_temp_repo_with_branched_data do |g| + + t = Time.new + + # make sure we are on 'new_branch' + g.branch('new_branch').checkout + + # import a file into the 'other_branch' + g.import_stream do |stream| + stream.commit do |c| + c.branch = g.branch('other_branch') + c.committer = Git::Author.from_parts("Jane Developer","jane@example.com",t) + c.message = "Test adding a single file to a different branch, without switching." + c.modify_file("test-file1","testtesttesttest5") + c.ancestor = g.log.object('other_branch').first + end + end + + assert_equal 'blahblahblah2', g.cat_file('other_branch:test-file2') + assert_equal 'blahblahblah1', g.cat_file('new_branch:test-file1') + assert_equal 'blahblahblah2', g.cat_file('new_branch:test-file2') + assert_equal "testtesttesttest5", g.cat_file('other_branch:test-file1') + end + end + + def create_temp_repo_with_branched_data + in_temp_dir do |path| + g = Git.clone(@wbare, 'branch_test') + Dir.chdir('branch_test') do + + # create a basic repo with two branches and some content + g.branch('new_branch').checkout + + new_file('test-file1', 'blahblahblah1') + new_file('test-file2', 'blahblahblah2') + + g.add(['test-file1', 'test-file2']) + g.commit("Initial commit.") + + g.branch('other_branch').checkout + + new_file('test-file1', 'blahblahblah3') + g.add(['test-file1']) + g.commit("Second commit.") + + yield g + end + end + end + +end