active_git 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in active_git.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 gabriel
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # ActiveGit
2
+
3
+ DB and GIT synchronization via ActiveRecord and GitWrapper
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'active_git'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install active_git
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/active_git/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "active_git"
6
+ s.version = ActiveGit::VERSION
7
+ s.authors = ["Gabriel Naiman"]
8
+ s.email = ["gabynaiman@gmail.com"]
9
+ s.description = 'DB and GIT synchronization via ActiveRecord and GitWrapper'
10
+ s.summary = 'DB and GIT synchronization via ActiveRecord and GitWrapper'
11
+ s.homepage = 'https://github.com/gabynaiman/active_git'
12
+
13
+ s.files = `git ls-files`.split($\)
14
+ s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
15
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
16
+ s.require_paths = ["lib"]
17
+
18
+ s.add_dependency 'activerecord', '>= 3.0.0'
19
+ s.add_dependency 'git_wrapper', '>= 1.0.2'
20
+ s.add_dependency 'activerecord-import'
21
+ s.add_dependency 'easy_diff'
22
+
23
+ s.add_development_dependency 'rspec'
24
+ s.add_development_dependency 'sqlite3'
25
+ end
@@ -0,0 +1,50 @@
1
+ module ActiveGit
2
+ module ActiveRecord
3
+
4
+ module ClassMethods
5
+
6
+ def git_versioned
7
+ ActiveGit.models << self
8
+
9
+ after_save do |record|
10
+ Synchronizer.synchronize FileSave.new(record)
11
+ end
12
+
13
+ after_destroy do |record|
14
+ Synchronizer.synchronize FileDelete.new(record)
15
+ end
16
+
17
+ def git_folder
18
+ "#{ActiveGit.configuration.working_path}/#{table_name}"
19
+ end
20
+
21
+ def from_json(json)
22
+ record = self.new
23
+ hash = json.is_a?(Hash) ? json : JSON.parse(json)
24
+ hash.each do |attr, value|
25
+ if record.respond_to? "#{attr}="
26
+ if self.columns_hash[attr].type == :datetime
27
+ record.send("#{attr}=", Time.parse(value).utc)
28
+ else
29
+ record.send("#{attr}=", value)
30
+ end
31
+ end
32
+ end
33
+ record
34
+ end
35
+
36
+ include InstanceMethods
37
+ end
38
+
39
+ end
40
+
41
+ module InstanceMethods
42
+
43
+ def git_file
44
+ "#{self.class.git_folder}/#{id}.json"
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,146 @@
1
+ module ActiveGit
2
+ module Commands
3
+
4
+ GitWrapper::Repository.instance_methods(false).each do |method|
5
+ define_method method do |*args, &block|
6
+ repository.send method, *args, &block
7
+ end
8
+ end
9
+
10
+ def dump_db
11
+ events = [FolderRemove.new(ActiveGit.configuration.working_path)]
12
+
13
+ ActiveGit.models.each do |model|
14
+ model.all.each do |record|
15
+ events << FileSave.new(record)
16
+ end
17
+ end
18
+
19
+ Synchronizer.synchronize events
20
+ end
21
+
22
+ def load_files
23
+ events = []
24
+
25
+ ActiveGit.models.each do |model|
26
+ events << DbDeleteAll.new(model)
27
+ Dir.glob("#{model.git_folder}/*.json").each do |file_name|
28
+ events << DbCreate.new(file_name)
29
+ end
30
+ end
31
+
32
+ Synchronizer.synchronize events
33
+ end
34
+
35
+ def commit_all(message, options={})
36
+ add_all
37
+ commit(message, options)
38
+ end
39
+
40
+ def pull(remote='origin', branch='master')
41
+ fetch remote
42
+ merge "#{remote}/#{branch}"
43
+ end
44
+
45
+ def merge(commit)
46
+ last_log = log.first
47
+ diffs = diff_reverse commit unless last_log
48
+
49
+ unless repository.merge(commit)
50
+ resolve_conflicts
51
+ diffs = diff 'HEAD'
52
+ commit_all 'Resolve conflicts'
53
+ end
54
+
55
+ diffs ||= repository.diff(last_log.commit_hash)
56
+ begin
57
+ synchronize_diffs diffs
58
+ rescue => e
59
+ ::ActiveRecord::Base.logger.error "[ActiveGit] #{e}"
60
+ reset last_log.commit_hash
61
+ return false
62
+ end
63
+
64
+ true
65
+ end
66
+
67
+ def conflicts
68
+ status.select { |e| e.status == :merge_conflict }.map { |e| e.file_name }
69
+ end
70
+
71
+ def resolve_conflicts
72
+ json_parse = Proc.new do |text|
73
+ text.present? ? JSON.parse(text) : {}
74
+ end
75
+
76
+ events = conflicts.map do |file_name|
77
+ base = json_parse.call(show_base(file_name))
78
+ mine = json_parse.call(show_mine(file_name))
79
+ theirs = json_parse.call(show_theirs(file_name))
80
+
81
+ r_diff, a_diff = base.easy_diff(mine)
82
+ merge = theirs.easy_unmerge(r_diff).easy_merge(a_diff)
83
+
84
+ model = File.dirname(file_name).split(/\/|\\/).pop.classify.constantize
85
+
86
+ FileSave.new(model.from_json(merge))
87
+ end
88
+
89
+ Synchronizer.synchronize events
90
+ end
91
+
92
+ def checkout(commit, new_branch=nil)
93
+ current = current_branch
94
+ diffs = repository.diff_reverse commit
95
+ if repository.checkout(commit.split('/').last, new_branch)
96
+ begin
97
+ synchronize_diffs diffs
98
+ rescue SynchronizationError => e
99
+ ::ActiveRecord::Base.logger.error "[ActiveGit] #{e}"
100
+ repository.checkout current
101
+ return false
102
+ end
103
+ true
104
+ else
105
+ false
106
+ end
107
+ end
108
+
109
+ def reset(commit='HEAD')
110
+ diffs = diff_reverse commit
111
+ if repository.reset mode: :hard, commit: commit
112
+ begin
113
+ synchronize_diffs diffs
114
+ rescue SynchronizationError => e
115
+ ::ActiveRecord::Base.logger.error "[ActiveGit] #{e}"
116
+ #TODO: Rollback reset
117
+ return false
118
+ end
119
+ true
120
+ else
121
+ false
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ def synchronize_diffs(diffs)
128
+ events = diffs.map do |d|
129
+ file_name = "#{location}/#{d.file_name}"
130
+
131
+ if d.status == :new_file
132
+ DbCreate.new file_name
133
+ elsif [:modified, :renamed, :copied].include? d.status
134
+ DbUpdate.new file_name
135
+ elsif d.status == :deleted
136
+ DbDelete.new file_name
137
+ else
138
+ raise "Unexpected file status [#{d.status}]"
139
+ end
140
+ end
141
+
142
+ Synchronizer.synchronize events
143
+ end
144
+
145
+ end
146
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveGit
2
+ class Configuration
3
+
4
+ def working_path
5
+ @working_path.is_a?(Proc) ? @working_path.call : @working_path
6
+ end
7
+
8
+ def working_path=(path)
9
+ @working_path = path
10
+ end
11
+
12
+ def bare_path
13
+ @bare_path.is_a?(Proc) ? @bare_path.call : @bare_path
14
+ end
15
+
16
+ def bare_path=(path)
17
+ @bare_path = path
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveGit
2
+ class DbCreate < DbEvent
3
+
4
+ def synchronize(synchronizer)
5
+ synchronizer.bulk_insert data
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module ActiveGit
2
+ class DbDelete < DbEvent
3
+
4
+ def synchronize(synchronizer)
5
+ synchronizer.define_job do
6
+ ::ActiveRecord::Base.logger.debug "[ActiveGit] Deleting #{model.model_name} #{model_id}"
7
+ record = model.find_by_id(model_id)
8
+ record.delete if record
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ module ActiveGit
2
+ class DbDeleteAll
3
+
4
+ def initialize(model)
5
+ @model = model
6
+ end
7
+
8
+ def synchronize(synchronizer)
9
+ synchronizer.define_job do
10
+ ::ActiveRecord::Base.logger.debug "[ActiveGit] Deleting all #{@model.model_name} models"
11
+ @model.delete_all
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ module ActiveGit
2
+ class DbEvent
3
+
4
+ def initialize(file_name)
5
+ @file_name = file_name
6
+ end
7
+
8
+ def synchronize(synchronizer)
9
+ raise 'Must implement in subclass'
10
+ end
11
+
12
+ protected
13
+
14
+ def model
15
+ @model ||= File.dirname(@file_name).split(/\/|\\/).pop.classify.constantize
16
+ end
17
+
18
+ def model_id
19
+ File.basename(@file_name, '.json')
20
+ end
21
+
22
+ def data
23
+ json = File.open(@file_name, 'r') { |f| f.readlines.join("\n") }
24
+ model.from_json(json)
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveGit
2
+ class DbUpdate < DbEvent
3
+
4
+ def synchronize(synchronizer)
5
+ synchronizer.bulk_insert data
6
+
7
+ synchronizer.define_job do
8
+ ::ActiveRecord::Base.logger.debug "[ActiveGit] Deleting #{data.class.model_name} #{data.id}"
9
+ record = data.class.find_by_id(data.id)
10
+ record.delete if record
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveGit
2
+ class FileDelete < FileEvent
3
+
4
+ def synchronize(synchronizer)
5
+ synchronizer.define_job do
6
+ ::ActiveRecord::Base.logger.debug "[ActiveGit] Deleting file #{file_name}"
7
+ File.delete(file_name) if File.exist?(file_name)
8
+ end
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ module ActiveGit
2
+ class FileEvent
3
+
4
+ def initialize(data, path=nil)
5
+ @data = data
6
+ @path = path || ActiveGit.configuration.working_path
7
+ end
8
+
9
+ def synchronize(synchronizer)
10
+ raise 'Must implement in subclass'
11
+ end
12
+
13
+ protected
14
+
15
+ def model
16
+ @data.class.to_s.classify.constantize
17
+ end
18
+
19
+ def model_path
20
+ "#{@path}/#{model.table_name}"
21
+ end
22
+
23
+ def file_name
24
+ "#{model_path}/#{@data.id}.json"
25
+ end
26
+
27
+ def json
28
+ JSON.pretty_generate(@data.attributes)
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveGit
2
+ class FileSave < FileEvent
3
+
4
+ def synchronize(synchronizer)
5
+ synchronizer.define_job do
6
+ ::ActiveRecord::Base.logger.debug "[ActiveGit] Writing file #{file_name}"
7
+ FileUtils.mkpath(File.dirname(file_name)) unless Dir.exist?(File.dirname(file_name))
8
+ File.open(file_name, 'w') { |f| f.puts json }
9
+ end
10
+
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ module ActiveGit
2
+ class FolderRemove
3
+
4
+ def initialize(path)
5
+ @path = path
6
+ end
7
+
8
+ def synchronize(synchronizer)
9
+ synchronizer.define_job do
10
+ ::ActiveRecord::Base.logger.debug "[ActiveGit] Removing working folder #{@path}"
11
+ FileUtils.rm_rf @path
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ module ActiveGit
2
+ class SynchronizationError < StandardError
3
+
4
+ def initialize(models)
5
+ @models = models
6
+ end
7
+
8
+ def message
9
+ messages = []
10
+ @models.each do |model|
11
+ model.errors.full_messages.each do |msg|
12
+ attributes = {}
13
+ model.attributes.each do |name, value|
14
+ attributes[model.class.human_attribute_name(name)] = value
15
+ end
16
+
17
+ attributes = model.attributes.inject({}) do |memo, item|
18
+ memo[model.class.human_attribute_name(item[0])] = item[1]
19
+ memo
20
+ end
21
+
22
+ messages << "#{model.class.model_name.human} - #{msg}\n#{attributes}"
23
+ end
24
+ end
25
+ messages.join("\n")
26
+ end
27
+
28
+ def to_s
29
+ "#{self.class.name} (#{message}):\n#{backtrace.join("\n")}"
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,54 @@
1
+ module ActiveGit
2
+ class Synchronizer
3
+
4
+ def self.synchronize(*events)
5
+ batch = self.new
6
+
7
+ Array(events).flatten.each do |event|
8
+ event.synchronize batch
9
+ end
10
+
11
+ batch.run
12
+ end
13
+
14
+ def run
15
+ unless bulk_inserts.empty?
16
+ define_job do
17
+ bulk_inserts.each do |model, records|
18
+ ::ActiveRecord::Base.logger.debug "[ActiveGit] Inserting #{model.model_name} models"
19
+ import_result = model.import records, :timestamps => false
20
+ raise SynchronizationError.new(import_result.failed_instances) unless import_result.failed_instances.empty?
21
+ end
22
+ end
23
+ end
24
+
25
+ ::ActiveRecord::Base.transaction do
26
+ jobs.each do |job|
27
+ job.call
28
+ end
29
+ end
30
+ ActiveGit.add_all
31
+ end
32
+
33
+ def bulk_insert(data)
34
+ bulk_inserts[data.class] ||= [] unless bulk_inserts.has_key? data.class
35
+ bulk_inserts[data.class] << data
36
+ end
37
+
38
+ def define_job(&block)
39
+ jobs << Proc.new(&block)
40
+ end
41
+
42
+ private
43
+
44
+ def bulk_inserts
45
+ @bulk_inserts ||= {}
46
+ end
47
+
48
+ def jobs
49
+ @jobs ||= []
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveGit
2
+ VERSION = "0.0.1"
3
+ end
data/lib/active_git.rb ADDED
@@ -0,0 +1,47 @@
1
+ require 'active_record'
2
+ require 'git_wrapper'
3
+ require 'activerecord-import'
4
+ require 'json'
5
+ require 'easy_diff'
6
+
7
+ require 'active_git/version'
8
+ require 'active_git/synchronizer'
9
+ require 'active_git/synchronization_error'
10
+ require 'active_git/events/db_event'
11
+ require 'active_git/events/db_create'
12
+ require 'active_git/events/db_update'
13
+ require 'active_git/events/db_delete'
14
+ require 'active_git/events/db_delete_all'
15
+ require 'active_git/events/file_event'
16
+ require 'active_git/events/file_save'
17
+ require 'active_git/events/file_delete'
18
+ require 'active_git/events/folder_remove'
19
+ require 'active_git/active_record_extension'
20
+ require 'active_git/configuration'
21
+ require 'active_git/commands'
22
+
23
+
24
+ module ActiveGit
25
+ extend Commands
26
+
27
+ def self.configuration
28
+ @@configuration ||= Configuration.new
29
+ end
30
+
31
+ def self.configure
32
+ yield(configuration)
33
+ end
34
+
35
+ def self.models
36
+ @@models ||= []
37
+ end
38
+
39
+ def self.repository
40
+ @repository = GitWrapper::Repository.new(ActiveGit.configuration.working_path) if @repository.nil? || @repository.location != ActiveGit.configuration.working_path
41
+ @repository
42
+ end
43
+
44
+ end
45
+
46
+ ActiveRecord::Base.send :extend, ActiveGit::ActiveRecord::ClassMethods
47
+
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveGit::ActiveRecord do
4
+
5
+ before :each do
6
+ @file_helper = FileHelper.new
7
+ ActiveGit.configuration.working_path = @file_helper.create_temp_folder
8
+ end
9
+
10
+ after :each do
11
+ @file_helper.remove_temp_folders
12
+ end
13
+
14
+ it 'Registered models' do
15
+ ActiveGit.models.should include Language
16
+ end
17
+
18
+ it 'Create' do
19
+ language = Language.create! name: 'Spanish'
20
+
21
+ File.exist?(language.git_file).should be_true
22
+
23
+ json = JSON.parse(@file_helper.read_file(language.git_file))
24
+
25
+ json['id'].should eq language.id
26
+ json['name'].should eq language.name
27
+ end
28
+
29
+ it 'Update' do
30
+ language = Language.create! name: 'Spanish'
31
+
32
+ json = JSON.parse(@file_helper.read_file(language.git_file))
33
+ json['name'].should eq 'Spanish'
34
+
35
+ language.update_attributes name: 'English'
36
+
37
+ json = JSON.parse(@file_helper.read_file(language.git_file))
38
+ json['name'].should eq 'English'
39
+ end
40
+
41
+ it 'Destroy' do
42
+ language = Language.create! name: 'Spanish'
43
+
44
+ File.exist?(language.git_file).should be_true
45
+
46
+ language.destroy
47
+
48
+ File.exist?(language.git_file).should be_false
49
+ end
50
+
51
+ end
@@ -0,0 +1,211 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveGit::Commands do
4
+
5
+ before :each do
6
+ @file_helper = FileHelper.new
7
+ ActiveGit.configuration.working_path = @file_helper.create_temp_folder
8
+ ActiveGit.init
9
+ end
10
+
11
+ after :each do
12
+ @file_helper.remove_temp_folders
13
+ end
14
+
15
+ context 'Dump and load' do
16
+
17
+ it 'Dump complete db to files' do
18
+ languages = [
19
+ Language.create!(name: 'Spanish'),
20
+ Language.create!(name: 'English')
21
+ ]
22
+
23
+ @file_helper.write_file "#{ActiveGit.configuration.working_path}/test.txt", 'test'
24
+ @file_helper.write_file "#{Language.git_folder}/0.json", 'test'
25
+
26
+ ActiveGit.dump_db
27
+
28
+ File.exist?("#{ActiveGit.configuration.working_path}/test.txt").should be_false
29
+
30
+ Dir.glob("#{Language.git_folder}/*.json").should have(2).items
31
+
32
+ languages.each do |language|
33
+ File.exist?(language.git_file).should be_true
34
+ json = JSON.parse(@file_helper.read_file(language.git_file))
35
+ json['id'].should eq language.id
36
+ json['name'].should eq language.name
37
+ end
38
+ end
39
+
40
+ it 'Load all files to db' do
41
+ languages = [
42
+ Language.create!(name: 'Spanish'),
43
+ Language.create!(name: 'English')
44
+ ]
45
+
46
+ Language.first.delete
47
+
48
+ languages.each do |language|
49
+ File.exist?(language.git_file).should be_true
50
+ end
51
+
52
+ ActiveGit.load_files
53
+
54
+ Language.count.should be 2
55
+
56
+ languages.each do |language|
57
+ language.reload.should be_a Language
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ context 'Git synchronization' do
64
+
65
+ it 'Commit all files' do
66
+ Language.create! name: 'Spanish'
67
+
68
+ ActiveGit.status.should_not be_empty
69
+
70
+ ActiveGit.commit_all 'Commit for test'
71
+
72
+ ActiveGit.status.should be_empty
73
+
74
+ ActiveGit.log.first.subject.should eq 'Commit for test'
75
+ end
76
+
77
+ it 'Push and pull' do
78
+ bare = GitWrapper::Repository.new @file_helper.create_temp_folder
79
+ bare.init_bare
80
+
81
+ spanish = Language.create! name: 'Spanish'
82
+ english = Language.create! name: 'English'
83
+ ActiveGit.commit_all 'Local commit'
84
+ ActiveGit.add_remote 'bare', bare.location
85
+ ActiveGit.push 'bare'
86
+
87
+ remote = GitWrapper::Repository.new @file_helper.create_temp_folder
88
+ remote.init
89
+ remote.add_remote 'bare', bare.location
90
+ remote.pull 'bare'
91
+
92
+ to_json = Proc.new do |id, name|
93
+ JSON.pretty_generate id: id, name: name, created_at: Time.now, updated_at: Time.now
94
+ end
95
+ @file_helper.write_file "#{remote.location}/languages/#{spanish.id}.json", to_json.call(spanish.id, 'Spanish 2')
96
+ @file_helper.write_file "#{remote.location}/languages/888.json", to_json.call(888, 'Portuguese')
97
+ @file_helper.write_file "#{remote.location}/languages/999.json", to_json.call(999, 'French')
98
+ FileUtils.rm "#{remote.location}/languages/#{english.id}.json"
99
+
100
+ remote.add_all
101
+ remote.commit 'Remote commit'
102
+ remote.push 'bare'
103
+
104
+ Language.count.should eq 2
105
+
106
+ ActiveGit.pull('bare').should be_true
107
+
108
+ ActiveGit.log.first.subject.should eq 'Remote commit'
109
+ Language.count.should eq 3
110
+ ['Spanish 2', 'Portuguese', 'French'].each do |lang_name|
111
+ File.exist?(Language.find_by_name(lang_name).git_file).should be_true
112
+ end
113
+ end
114
+
115
+ it 'Checkout' do
116
+ Language.create! name: 'Spanish'
117
+ Language.create! name: 'English'
118
+
119
+ ActiveGit.commit_all 'Commit 1'
120
+ ActiveGit.branch 'branch_test'
121
+ ActiveGit.checkout 'branch_test'
122
+
123
+ Language.first.destroy
124
+ ActiveGit.commit_all 'Commit 2'
125
+
126
+ Language.count.should eq 1
127
+
128
+ ActiveGit.checkout 'master'
129
+
130
+ Language.count.should eq 2
131
+
132
+ ActiveGit.checkout 'branch_test'
133
+
134
+ Language.count.should eq 1
135
+ end
136
+
137
+ it 'Reset to specific commit' do
138
+ spanish = Language.create! name: 'Spanish'
139
+ ActiveGit.commit_all 'Commit 1'
140
+
141
+ english = Language.create! name: 'English'
142
+ ActiveGit.commit_all 'Commit 1'
143
+
144
+ Language.count.should eq 2
145
+ File.exist?(spanish.git_file).should be_true
146
+ File.exist?(english.git_file).should be_true
147
+
148
+ ActiveGit.reset ActiveGit.log.last.commit_hash
149
+
150
+ Language.count.should eq 1
151
+ File.exist?(spanish.git_file).should be_true
152
+ File.exist?(english.git_file).should be_false
153
+ end
154
+
155
+ it 'Reset to HEAD' do
156
+ spanish = Language.create! name: 'Spanish'
157
+ ActiveGit.commit_all 'Commit 1'
158
+
159
+ english = Language.create! name: 'English'
160
+
161
+ Language.count.should eq 2
162
+ File.exist?(spanish.git_file).should be_true
163
+ File.exist?(english.git_file).should be_true
164
+
165
+ ActiveGit.reset
166
+
167
+ Language.count.should eq 1
168
+ File.exist?(spanish.git_file).should be_true
169
+ File.exist?(english.git_file).should be_false
170
+ end
171
+
172
+ it 'Resolve version conflicts' do
173
+ bare = GitWrapper::Repository.new @file_helper.create_temp_folder
174
+ bare.init_bare
175
+
176
+ ActiveGit.add_remote 'bare', bare.location
177
+
178
+ spanish = Language.create! name: 'Spanish'
179
+
180
+ ActiveGit.commit_all 'Commit v1'
181
+ ActiveGit.push 'bare'
182
+
183
+ other_repo = GitWrapper::Repository.new @file_helper.create_temp_folder
184
+ other_repo.init
185
+ other_repo.add_remote 'bare', bare.location
186
+ other_repo.pull 'bare'
187
+
188
+ @file_helper.write_file "#{other_repo.location}/languages/#{spanish.id}.json",
189
+ JSON.pretty_generate(id: spanish.id, name: 'Spanish 2', created_at: Time.now, updated_at: Time.now)
190
+
191
+ other_repo.add_all
192
+ other_repo.commit 'Commit v2'
193
+ other_repo.push 'bare'
194
+
195
+ spanish.update_attributes name: 'Spanish 3'
196
+ ActiveGit.commit 'Commit v3'
197
+
198
+ ActiveGit.pull 'bare'
199
+
200
+ spanish.reload.name.should eq 'Spanish 3'
201
+
202
+ ActiveGit.log.should have(4).items
203
+ ActiveGit.log[0].subject.should eq 'Resolve conflicts'
204
+ ActiveGit.log[1].subject.should eq 'Commit v3'
205
+ ActiveGit.log[2].subject.should eq 'Commit v2'
206
+ ActiveGit.log[3].subject.should eq 'Commit v1'
207
+ end
208
+
209
+ end
210
+
211
+ end
@@ -0,0 +1,9 @@
1
+ class CreateCountries < ActiveRecord::Migration
2
+ def change
3
+ create_table :countries do |t|
4
+ t.string :name, :null => false
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class CreateLanguages < ActiveRecord::Migration
2
+ def change
3
+ create_table :languages do |t|
4
+ t.string :name, :null => false
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ require 'active_git'
2
+ require 'logger'
3
+
4
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
5
+
6
+ TEMP_PATH = ENV['TMP'].gsub("\\", '/')
7
+
8
+ ActiveRecord::Base.logger = Logger.new($stdout)
9
+ ActiveRecord::Migrator.migrations_path = "#{File.dirname(__FILE__)}/migrations"
10
+
11
+ RSpec.configure do |config|
12
+ config.around do |example|
13
+ ActiveRecord::Base.transaction do
14
+ example.run
15
+ raise ActiveRecord::Rollback
16
+ end
17
+ end
18
+
19
+ config.before :all do
20
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ":memory:"
21
+ ActiveRecord::Base.connection
22
+ ActiveRecord::Migrator.migrate ActiveRecord::Migrator.migrations_path
23
+ end
24
+ end
@@ -0,0 +1,44 @@
1
+ require 'fileutils'
2
+
3
+ class FileHelper
4
+
5
+ def initialize
6
+ @temp_folders = []
7
+ end
8
+
9
+ def create_temp_folder
10
+ folder_name = "#{ENV['TMP'].gsub('\\', '/')}/#{timestamp}"
11
+ @temp_folders << folder_name
12
+ Dir.mkdir folder_name
13
+ folder_name
14
+ end
15
+
16
+ def remove_temp_folders
17
+ @temp_folders.each do |folder_name|
18
+ FileUtils.rm_rf folder_name
19
+ end
20
+ end
21
+
22
+ def create_temp_file(folder, content)
23
+ file_name = "#{folder}/file #{timestamp}.txt"
24
+ write_file file_name, content
25
+ file_name
26
+ end
27
+
28
+ def write_file(file_name, content)
29
+ Dir.mkdir File.dirname(file_name) unless Dir.exist? File.dirname(file_name)
30
+ File.open(file_name, 'w') { |f| f.puts content }
31
+ end
32
+
33
+ def read_file(file_name)
34
+ File.open(file_name, 'r') { |f| f.readlines.join("\n").strip }
35
+ end
36
+
37
+ private
38
+
39
+ def timestamp
40
+ sleep(0.01)
41
+ (Time.now.to_f * 1000).to_i
42
+ end
43
+
44
+ end
@@ -0,0 +1,3 @@
1
+ class Country < ActiveRecord::Base
2
+ attr_accessible :name
3
+ end
@@ -0,0 +1,5 @@
1
+ class Language < ActiveRecord::Base
2
+ git_versioned
3
+
4
+ attr_accessible :name
5
+ end
@@ -0,0 +1,101 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'ActiveGit::Synchronizers' do
4
+
5
+ before :each do
6
+ @file_helper = FileHelper.new
7
+ end
8
+
9
+ after :each do
10
+ @file_helper.remove_temp_folders
11
+ end
12
+
13
+ context 'Target GIT' do
14
+
15
+ it 'Create' do
16
+ country = Country.create! name: 'Argentina'
17
+
18
+ working_path = @file_helper.create_temp_folder
19
+
20
+ File.exist?("#{working_path}/countries/#{country.id}.json").should be_false
21
+
22
+ ActiveGit::Synchronizer.synchronize ActiveGit::FileSave.new(country, working_path)
23
+
24
+ File.exist?("#{working_path}/countries/#{country.id}.json").should be_true
25
+ end
26
+
27
+ it 'Update' do
28
+ country = Country.create! name: 'Argentina'
29
+
30
+ working_path = @file_helper.create_temp_folder
31
+
32
+ file_name = "#{working_path}/countries/#{country.id}.json"
33
+ @file_helper.write_file file_name, 'test'
34
+
35
+ @file_helper.read_file(file_name).should eq 'test'
36
+
37
+ ActiveGit::Synchronizer.synchronize ActiveGit::FileSave.new(country, working_path)
38
+
39
+ json = JSON.parse @file_helper.read_file(file_name)
40
+ json['id'].should eq country.id
41
+ json['name'].should eq country.name
42
+ end
43
+
44
+ it 'Destroy' do
45
+ country = Country.create! name: 'Argentina'
46
+
47
+ working_path = @file_helper.create_temp_folder
48
+
49
+ file_name = "#{working_path}/countries/#{country.id}.json"
50
+ @file_helper.write_file file_name, 'test'
51
+
52
+ ActiveGit::Synchronizer.synchronize ActiveGit::FileDelete.new(country, working_path)
53
+
54
+ File.exist?("#{working_path}/countries/#{country.id}.json").should be_false
55
+ end
56
+
57
+ end
58
+
59
+ context 'Target DB' do
60
+
61
+ it 'Create' do
62
+ working_path = @file_helper.create_temp_folder
63
+
64
+ file_name = "#{working_path}/countries/1.json"
65
+ @file_helper.write_file file_name, {id: 1, name: 'Argentina', created_at: '2012-04-20T11:24:11-03:00', updated_at: '2012-04-20T11:24:11-03:00'}.to_json
66
+
67
+ Country.count.should eq 0
68
+
69
+ ActiveGit::Synchronizer.synchronize ActiveGit::DbCreate.new(file_name)
70
+
71
+ Country.find(1).name.should eq 'Argentina'
72
+ end
73
+
74
+ it 'Update' do
75
+ working_path = @file_helper.create_temp_folder
76
+
77
+ country = Country.create! name: 'Argentina'
78
+
79
+ file_name = "#{working_path}/countries/#{country.id}.json"
80
+ @file_helper.write_file file_name, country.attributes.merge('name' => 'Brasil').to_json
81
+
82
+ ActiveGit::Synchronizer.synchronize ActiveGit::DbUpdate.new(file_name)
83
+
84
+ country.reload.name.should eq 'Brasil'
85
+ end
86
+
87
+ it 'Destroy' do
88
+ working_path = @file_helper.create_temp_folder
89
+
90
+ country = Country.create! name: 'Argentina'
91
+
92
+ file_name = "#{working_path}/countries/#{country.id}.json"
93
+
94
+ ActiveGit::Synchronizer.synchronize ActiveGit::DbDelete.new(file_name)
95
+
96
+ Country.find_by_id(country.id).should be_nil
97
+ end
98
+
99
+ end
100
+
101
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_git
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Gabriel Naiman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: &28066404 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *28066404
25
+ - !ruby/object:Gem::Dependency
26
+ name: git_wrapper
27
+ requirement: &28066104 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 1.0.2
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *28066104
36
+ - !ruby/object:Gem::Dependency
37
+ name: activerecord-import
38
+ requirement: &28065876 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *28065876
47
+ - !ruby/object:Gem::Dependency
48
+ name: easy_diff
49
+ requirement: &28065600 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *28065600
58
+ - !ruby/object:Gem::Dependency
59
+ name: rspec
60
+ requirement: &28065348 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *28065348
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: &28065096 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *28065096
80
+ description: DB and GIT synchronization via ActiveRecord and GitWrapper
81
+ email:
82
+ - gabynaiman@gmail.com
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - .gitignore
88
+ - Gemfile
89
+ - LICENSE
90
+ - README.md
91
+ - Rakefile
92
+ - active_git.gemspec
93
+ - lib/active_git.rb
94
+ - lib/active_git/active_record_extension.rb
95
+ - lib/active_git/commands.rb
96
+ - lib/active_git/configuration.rb
97
+ - lib/active_git/events/db_create.rb
98
+ - lib/active_git/events/db_delete.rb
99
+ - lib/active_git/events/db_delete_all.rb
100
+ - lib/active_git/events/db_event.rb
101
+ - lib/active_git/events/db_update.rb
102
+ - lib/active_git/events/file_delete.rb
103
+ - lib/active_git/events/file_event.rb
104
+ - lib/active_git/events/file_save.rb
105
+ - lib/active_git/events/folder_remove.rb
106
+ - lib/active_git/synchronization_error.rb
107
+ - lib/active_git/synchronizer.rb
108
+ - lib/active_git/version.rb
109
+ - spec/active_record_extension_spec.rb
110
+ - spec/commands_spec.rb
111
+ - spec/migrations/20121004135939_create_countries.rb
112
+ - spec/migrations/20121004135940_create_languages.rb
113
+ - spec/spec_helper.rb
114
+ - spec/support/helpers/file_helper.rb
115
+ - spec/support/models/country.rb
116
+ - spec/support/models/language.rb
117
+ - spec/synchronization_spec.rb
118
+ homepage: https://github.com/gabynaiman/active_git
119
+ licenses: []
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ! '>='
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ none: false
132
+ requirements:
133
+ - - ! '>='
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ requirements: []
137
+ rubyforge_project:
138
+ rubygems_version: 1.8.16
139
+ signing_key:
140
+ specification_version: 3
141
+ summary: DB and GIT synchronization via ActiveRecord and GitWrapper
142
+ test_files:
143
+ - spec/active_record_extension_spec.rb
144
+ - spec/commands_spec.rb
145
+ - spec/migrations/20121004135939_create_countries.rb
146
+ - spec/migrations/20121004135940_create_languages.rb
147
+ - spec/spec_helper.rb
148
+ - spec/support/helpers/file_helper.rb
149
+ - spec/support/models/country.rb
150
+ - spec/support/models/language.rb
151
+ - spec/synchronization_spec.rb