tvc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +14 -0
- data/Gemfile.lock +24 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +86 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/bin/tvc +5 -0
- data/lib/tvc.rb +635 -0
- data/test/helper.rb +18 -0
- data/test/test_tvc.rb +7 -0
- data/tvc.gemspec +71 -0
- metadata +132 -0
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
gem "json", ">= 0"
|
6
|
+
gem "diff-lcs", ">= 0"
|
7
|
+
# Add dependencies to develop your gem here.
|
8
|
+
# Include everything needed to run rake, tests, features, etc.
|
9
|
+
group :development do
|
10
|
+
gem "shoulda", ">= 0"
|
11
|
+
gem "bundler", "~> 1.0.0"
|
12
|
+
gem "jeweler", "~> 1.5.2"
|
13
|
+
gem "rcov", ">= 0"
|
14
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
diff-lcs (1.1.2)
|
5
|
+
git (1.2.5)
|
6
|
+
jeweler (1.5.2)
|
7
|
+
bundler (~> 1.0.0)
|
8
|
+
git (>= 1.2.5)
|
9
|
+
rake
|
10
|
+
json (1.5.1-x86-mingw32)
|
11
|
+
rake (0.8.7)
|
12
|
+
rcov (0.9.9)
|
13
|
+
shoulda (2.11.3)
|
14
|
+
|
15
|
+
PLATFORMS
|
16
|
+
x86-mingw32
|
17
|
+
|
18
|
+
DEPENDENCIES
|
19
|
+
bundler (~> 1.0.0)
|
20
|
+
diff-lcs
|
21
|
+
jeweler (~> 1.5.2)
|
22
|
+
json
|
23
|
+
rcov
|
24
|
+
shoulda
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Chris O'Neal
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
= TVC - Terrible Version Control
|
2
|
+
|
3
|
+
== What TVC Is:
|
4
|
+
|
5
|
+
TVC is a really terrible pseudo-remake of git in Ruby. It should never really be used; its creation was a joke and a learning experience. It will always be inferior to git. I read a blog post called [The Git Parable](http://tom.preston-werner.com/2009/05/19/the-git-parable.html), and wrote it based on some of the explanations given there. It's not a terribly well written piece of code, just a script I hacked together in the span of about a day. I'm slowly adding to and revising it, but don't expect much. If you're the version control police, please don't arrest me. It was all meant as good fun, I swear!
|
6
|
+
|
7
|
+
Also, this is the first real thing I've written in Ruby. It's not going to be pretty, and I'm pretty sure I'm not doing things "the Ruby way" or whatever. Be gentle.
|
8
|
+
|
9
|
+
However, you might find it interesting to look at. It works in limited situations: if you only want a local repository, and you don't need a whole lot of features or quality, well, it might do.
|
10
|
+
|
11
|
+
== Features (if you can call them that):
|
12
|
+
|
13
|
+
* Storing versions
|
14
|
+
* Branching
|
15
|
+
* Merging (but it's a pretty terrible merge right now, don't trust it!)
|
16
|
+
* Viewing a list of commits for the branch you're on
|
17
|
+
* Pulling a previous revision
|
18
|
+
|
19
|
+
== What You Need:
|
20
|
+
|
21
|
+
* Ruby 1.9
|
22
|
+
* The desire to use sub-par code
|
23
|
+
|
24
|
+
== What To Do:
|
25
|
+
|
26
|
+
Well, if you're still reading after all my warnings, I guess you want to try this out. You're crazy.
|
27
|
+
|
28
|
+
First, install the TVC gem.
|
29
|
+
|
30
|
+
% gem install tvc
|
31
|
+
|
32
|
+
Go to where you want your repository and type:
|
33
|
+
|
34
|
+
% tvc init
|
35
|
+
|
36
|
+
This will initialize the repository. After that, commit like so:
|
37
|
+
|
38
|
+
% tvc commit "some message about committing"
|
39
|
+
|
40
|
+
And of course, it's going to be the same anytime you want to commit changes to the repository. To branch, type:
|
41
|
+
|
42
|
+
% tvc branch <some name for the branch>
|
43
|
+
|
44
|
+
If no name is specified, it will list all current branches. To move to a branch, type:
|
45
|
+
|
46
|
+
% tvc checkout <name of a branch>
|
47
|
+
|
48
|
+
Now you're on that branch, ready to modify it. Once you've committed changes to the branch and want to merge them in somewhere else (say, for instance, the master branch), type:
|
49
|
+
|
50
|
+
% tvc checkout <branch you want to merge into>
|
51
|
+
% tvc merge <branch you want to merge from>
|
52
|
+
|
53
|
+
By now, you've noticed each commit has a corresponding SHA-2 hash. If you want to pull the files from a revision, type:
|
54
|
+
|
55
|
+
% tvc replace <the hash of the commit you want>
|
56
|
+
|
57
|
+
You don't have to type the whole hash, you can type just a few characters off the front of it. You run the risk of messing up, I suppose. Be careful. If you want to roll back your repository to how it was at a specific commit, type:
|
58
|
+
|
59
|
+
% tvc rollback <the hash of the commit you want>
|
60
|
+
|
61
|
+
And if you want to just reset everything to how it was before you screwed everything up, you can go back to the last commit by typing:
|
62
|
+
|
63
|
+
% tvc reset
|
64
|
+
|
65
|
+
To get a list of all commits up the tree from where you currently are, type:
|
66
|
+
|
67
|
+
% tvc history
|
68
|
+
|
69
|
+
To see a list of what has changed since the last commit to the repository, type:
|
70
|
+
|
71
|
+
% tvc status
|
72
|
+
|
73
|
+
And, if you ever forget these few commands, they're accessible by typing:
|
74
|
+
|
75
|
+
% tvc
|
76
|
+
|
77
|
+
or
|
78
|
+
|
79
|
+
% tvc help
|
80
|
+
|
81
|
+
|
82
|
+
== Things That Might Happen Eventually:
|
83
|
+
|
84
|
+
* Better status information.
|
85
|
+
* Unit tests. Probably should have started with those, but oh well.
|
86
|
+
* The ability to work with external repositories (push, pull, users, blah blah blah)
|
data/Rakefile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
15
|
+
gem.name = "tvc"
|
16
|
+
gem.homepage = "http://github.com/ctoneal/tvc"
|
17
|
+
gem.license = "MIT"
|
18
|
+
gem.summary = %Q{TVC - Terrible Version Control}
|
19
|
+
gem.description = %Q{TVC is an awful git clone that should never be used, ever.}
|
20
|
+
gem.email = "ctoneal@gmail.com"
|
21
|
+
gem.authors = ["Chris O'Neal"]
|
22
|
+
# Include your dependencies below. Runtime dependencies are required when using your gem,
|
23
|
+
# and development dependencies are only needed for development (ie running rake tasks, tests, etc)
|
24
|
+
#gem.add_dependency = "json", ">= 0"
|
25
|
+
#gem.add_dependency = "diff-lcs", ">= 0"
|
26
|
+
# gem.add_runtime_dependency 'jabber4r', '> 0.1'
|
27
|
+
# gem.add_development_dependency 'rspec', '> 1.2.3'
|
28
|
+
end
|
29
|
+
Jeweler::RubygemsDotOrgTasks.new
|
30
|
+
|
31
|
+
require 'rake/testtask'
|
32
|
+
Rake::TestTask.new(:test) do |test|
|
33
|
+
test.libs << 'lib' << 'test'
|
34
|
+
test.pattern = 'test/**/test_*.rb'
|
35
|
+
test.verbose = true
|
36
|
+
end
|
37
|
+
|
38
|
+
require 'rcov/rcovtask'
|
39
|
+
Rcov::RcovTask.new do |test|
|
40
|
+
test.libs << 'test'
|
41
|
+
test.pattern = 'test/**/test_*.rb'
|
42
|
+
test.verbose = true
|
43
|
+
end
|
44
|
+
|
45
|
+
task :default => :test
|
46
|
+
|
47
|
+
require 'rake/rdoctask'
|
48
|
+
Rake::RDocTask.new do |rdoc|
|
49
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
50
|
+
|
51
|
+
rdoc.rdoc_dir = 'rdoc'
|
52
|
+
rdoc.title = "tvc #{version}"
|
53
|
+
rdoc.rdoc_files.include('README*')
|
54
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
55
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/tvc
ADDED
data/lib/tvc.rb
ADDED
@@ -0,0 +1,635 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'digest/sha2'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'json'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'diff/lcs'
|
7
|
+
require 'zlib'
|
8
|
+
|
9
|
+
class Diff::LCS::Change
|
10
|
+
attr_accessor :position
|
11
|
+
end
|
12
|
+
|
13
|
+
class TVC
|
14
|
+
# print an error that states that the program must be run in the directory
|
15
|
+
# with the repository, and that the repository needs to be initialized.
|
16
|
+
#
|
17
|
+
# this was a commonly used error, hence the function just for it.
|
18
|
+
def repositoryIssueError
|
19
|
+
puts "The repository either has not been initialized
|
20
|
+
or this command is not being run from the base
|
21
|
+
directory of the repository".gsub(/\s+/, " ").strip
|
22
|
+
end
|
23
|
+
|
24
|
+
# initialize a repository
|
25
|
+
def init
|
26
|
+
Dir.chdir(@runDir)
|
27
|
+
Dir.mkdir(".tvc") unless Dir::exists?(".tvc")
|
28
|
+
@repoDir = getRepositoryDirectory
|
29
|
+
Dir.mkdir(getObjectsDirectory) unless Dir::exists?(getObjectsDirectory)
|
30
|
+
saveDataToJson(File.join(@repoDir, "pointers"), [{"name" => "master", "hash" => nil, "parent" => nil}])
|
31
|
+
saveDataToJson(File.join(@repoDir, "history"), [])
|
32
|
+
saveDataToJson(File.join(@repoDir, "current"), [{"name" => "master", "hash" => nil, "parent" => nil}])
|
33
|
+
end
|
34
|
+
|
35
|
+
# commit current changes
|
36
|
+
def commit(message)
|
37
|
+
hash = createObjects(@runDir)
|
38
|
+
current = getCurrentEntry
|
39
|
+
data = {"message" => message, "hash" => hash, "parent" => current["hash"]}
|
40
|
+
addHistoryEntry(data)
|
41
|
+
current["parent"] = data["parent"]
|
42
|
+
current["hash"] = data["hash"]
|
43
|
+
changeCurrentEntry(current)
|
44
|
+
changeBranchHash(current["name"], current["hash"])
|
45
|
+
puts hash
|
46
|
+
puts message
|
47
|
+
end
|
48
|
+
|
49
|
+
# replace current files with requested version
|
50
|
+
def replace(versionHash)
|
51
|
+
hash = getFullHash(versionHash)
|
52
|
+
if not hash.nil?
|
53
|
+
deleteFiles(@runDir)
|
54
|
+
rootInfo = getDataFromJson(File.join(getObjectsDirectory, hash))
|
55
|
+
moveFiles(@runDir, rootInfo)
|
56
|
+
else
|
57
|
+
puts "This version does not exist"
|
58
|
+
return
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# prints a list of commits up the chain
|
63
|
+
def history
|
64
|
+
current = getCurrentEntry
|
65
|
+
hash = current["hash"]
|
66
|
+
versions = getHistoryEntries
|
67
|
+
while not hash.nil?
|
68
|
+
versions.each do |version|
|
69
|
+
if version["hash"] == hash
|
70
|
+
puts "#{version["hash"]}\n#{version["message"]}\n\n"
|
71
|
+
hash = version["parent"]
|
72
|
+
break
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# create a branch
|
79
|
+
def branch(name)
|
80
|
+
b = findBranch(name)
|
81
|
+
if b.nil?
|
82
|
+
current = getCurrentEntry
|
83
|
+
newBranch = {"name" => name, "hash" => current["hash"], "parent" => current["hash"]}
|
84
|
+
addBranchEntry(newBranch)
|
85
|
+
else
|
86
|
+
puts "Branch already exists"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# checkout a branch
|
91
|
+
def checkout(branchName)
|
92
|
+
checkoutBranch = findBranch(branchName)
|
93
|
+
if not checkoutBranch.nil?
|
94
|
+
changeCurrentEntry(checkoutBranch)
|
95
|
+
replace(checkoutBranch["hash"])
|
96
|
+
else
|
97
|
+
puts "Branch does not exist"
|
98
|
+
return
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# merges a branch into the current branch
|
103
|
+
# i'm betting this is going to look like crap
|
104
|
+
def merge(branchName)
|
105
|
+
source = findBranch(branchName)
|
106
|
+
target = getCurrentEntry
|
107
|
+
parent = findCommonAncestor(source, target)
|
108
|
+
if not source.nil?
|
109
|
+
if not parent.nil?
|
110
|
+
mergeFilesForDirectory(@runDir, source["hash"], target["hash"], parent["hash"])
|
111
|
+
commit("Merged branch #{branchName}")
|
112
|
+
else
|
113
|
+
puts "Could not find common ancestor"
|
114
|
+
end
|
115
|
+
else
|
116
|
+
puts "Branch doesn't exist"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# find and display the changes from the current version in the repository
|
121
|
+
def status
|
122
|
+
current = getCurrentEntry
|
123
|
+
changes = findChanges(@runDir, current["hash"])
|
124
|
+
if changes.empty?
|
125
|
+
puts "No changes made"
|
126
|
+
else
|
127
|
+
changes.each do |change|
|
128
|
+
puts "#{change["name"]} #{change["type"]}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# creates a list of changes made
|
134
|
+
def findChanges(directory, currentHash)
|
135
|
+
currentData = getDataFromJson(File.join(getObjectsDirectory, currentHash))
|
136
|
+
changes = []
|
137
|
+
# loop through every item in the directory
|
138
|
+
Dir.foreach(directory) do |dirItem|
|
139
|
+
if dirItem != '.' && dirItem != '..' && dirItem != ".tvc"
|
140
|
+
dirPath = File.join(directory, dirItem)
|
141
|
+
foundItem = nil
|
142
|
+
itemType = File.directory?(dirPath) ? "tree" : "blob"
|
143
|
+
# compare the item to the directory's json
|
144
|
+
currentData.each_index do |index|
|
145
|
+
currentItem = currentData[index]
|
146
|
+
if currentItem["name"] == dirItem && currentItem["type"] == itemType
|
147
|
+
foundItem = currentItem
|
148
|
+
currentData.delete_at(index)
|
149
|
+
break
|
150
|
+
end
|
151
|
+
end
|
152
|
+
# if nothing in the json matched this item, it must be new
|
153
|
+
if foundItem.nil?
|
154
|
+
newChange = { "type" => "added", "name" => dirPath.gsub(@runDir, "") }
|
155
|
+
changes << newChange
|
156
|
+
if itemType == "tree"
|
157
|
+
newChanges = findChangesInNewDirectory(dirPath)
|
158
|
+
newChanges.each do |change|
|
159
|
+
changes << change
|
160
|
+
end
|
161
|
+
end
|
162
|
+
# we found a match, so we need to compare it
|
163
|
+
else
|
164
|
+
# if it's a directory, continue down to find changes in there
|
165
|
+
if itemType == "tree"
|
166
|
+
newChanges = findChanges(dirPath, foundItem["hash"])
|
167
|
+
newChanges.each do |newChange|
|
168
|
+
changes << newChange
|
169
|
+
end
|
170
|
+
# if it's a file, compare the contents
|
171
|
+
else
|
172
|
+
# this is a really hacky way to do this, but i'm so lazy
|
173
|
+
repoData = getFileData(File.join(getObjectsDirectory, foundItem["hash"]))
|
174
|
+
repoData = repoData.gsub(/\s/, "")
|
175
|
+
fileData = File.open(dirPath) { |f| f.read }
|
176
|
+
fileData = fileData.gsub(/\s/, "")
|
177
|
+
diffs = Diff::LCS.diff(repoData, fileData)
|
178
|
+
modified = false
|
179
|
+
diffs.each do |diffArray|
|
180
|
+
diffArray.each do |diff|
|
181
|
+
if diff.action != "=" && diff.element != ""
|
182
|
+
modified = true;
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
if modified
|
187
|
+
newChange = { "type" => "modified", "name" => dirPath.gsub(@runDir, "") }
|
188
|
+
changes << newChange
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
# if we still have items in the directory json, they must have been deleted
|
195
|
+
currentData.each do |currentItem|
|
196
|
+
newChange = { "type" => "deleted", "name" => currentItem["name"] }
|
197
|
+
changes << newChange
|
198
|
+
end
|
199
|
+
return changes
|
200
|
+
end
|
201
|
+
|
202
|
+
# returns a list of everything in this directory.
|
203
|
+
def findChangesInNewDirectory(directory)
|
204
|
+
changes = []
|
205
|
+
Dir.foreach(directory) do |dirItem|
|
206
|
+
if dirItem != '.' && dirItem != '..' && dirItem != ".tvc"
|
207
|
+
dirPath = File.join(directory, dirItem)
|
208
|
+
newChange = { "type" => "added", "name" => dirPath.gsub(@runDir, "") }
|
209
|
+
changes << newChange
|
210
|
+
if File.directory?(dirPath)
|
211
|
+
newChanges = findChangesInNewDirectory(dirPath)
|
212
|
+
newChanges.each do |change|
|
213
|
+
changes << change
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
return changes
|
219
|
+
end
|
220
|
+
|
221
|
+
# attempts to find a common parent for the two revisions
|
222
|
+
def findCommonAncestor(source, target)
|
223
|
+
sourceParentHash = source["hash"]
|
224
|
+
versions = getHistoryEntries
|
225
|
+
# loop through until we hit the end of the tree for the source
|
226
|
+
while not sourceParentHash.nil?
|
227
|
+
sourceParent = nil
|
228
|
+
targetParentHash = target["hash"]
|
229
|
+
versions.each do |version|
|
230
|
+
if version["hash"] == sourceParentHash
|
231
|
+
sourceParent = version
|
232
|
+
break
|
233
|
+
end
|
234
|
+
end
|
235
|
+
# loop through the target tree, hoping we find something that matches up
|
236
|
+
while not targetParentHash.nil?
|
237
|
+
versions.each do |version|
|
238
|
+
if version["hash"] == targetParentHash
|
239
|
+
if targetParentHash == sourceParentHash
|
240
|
+
return version
|
241
|
+
else
|
242
|
+
targetParentHash = version["parent"]
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
if sourceParent.nil?
|
248
|
+
sourceParentHash = nil
|
249
|
+
else
|
250
|
+
sourceParentHash = sourceParent["parent"]
|
251
|
+
end
|
252
|
+
end
|
253
|
+
return nil
|
254
|
+
end
|
255
|
+
|
256
|
+
# attempts to merge the items for the specified directory together
|
257
|
+
def mergeFilesForDirectory(directory, sourceHash, targetHash, parentHash)
|
258
|
+
sourceJson = getDataFromJson(File.join(getObjectsDirectory, sourceHash))
|
259
|
+
targetJson = getDataFromJson(File.join(getObjectsDirectory, targetHash))
|
260
|
+
parentJson = getDataFromJson(File.join(getObjectsDirectory, parentHash))
|
261
|
+
# for each item in the source, attempt to find a corresponding item in the target
|
262
|
+
sourceJson.each do |sourceItem|
|
263
|
+
matchingItem = nil
|
264
|
+
parentMatch = nil
|
265
|
+
targetJson.each do |targetItem|
|
266
|
+
if targetItem["name"] == sourceItem["name"] && targetItem["type"] == sourceItem["type"]
|
267
|
+
matchingItem = targetItem
|
268
|
+
break
|
269
|
+
end
|
270
|
+
end
|
271
|
+
parentJson.each do |parentItem|
|
272
|
+
if parentItem["name"] == sourceItem["name"] && parentItem["type"] == sourceItem["type"]
|
273
|
+
parentMatch = parentItem
|
274
|
+
break
|
275
|
+
end
|
276
|
+
end
|
277
|
+
# we're only going to attempt to merge if we've found some common parent
|
278
|
+
# otherwise, we're just going to straight up replace that thing
|
279
|
+
if parentMatch.nil?
|
280
|
+
puts "No common ancestor found, pushing change to target"
|
281
|
+
createItem(directory, sourceItem)
|
282
|
+
# if there's no matching item, but there is a parent, well, i guess it got deleted
|
283
|
+
# in the target, but is needed by source. add it back in.
|
284
|
+
elsif matchingItem.nil?
|
285
|
+
puts "No matching item found, pushing change to target"
|
286
|
+
createItem(directory, sourceItem)
|
287
|
+
# if we've got everything we need, we'll attempt to merge
|
288
|
+
else
|
289
|
+
# if this is a tree, continue on to do all the logic for it
|
290
|
+
if sourceItem["type"] == "tree"
|
291
|
+
mergeFilesForDirectory(File.join(directory, sourceItem["name"]), sourceItem["hash"], matchingItem["hash"], parentMatch["hash"])
|
292
|
+
# if it's a fine, try to merge the two together
|
293
|
+
# man, this might fail horribly with binary files.
|
294
|
+
# watch out for that
|
295
|
+
elsif sourceItem["type"] == "blob"
|
296
|
+
if sourceItem["hash"] != matchingItem["hash"]
|
297
|
+
mergeFiles(sourceItem["hash"], matchingItem["hash"], parentMatch["hash"], File.join(directory, sourceItem["name"]))
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# attempt to merge two files together given a common parent
|
305
|
+
# this seems like a pretty naive way of doing things, but i'm lazy!
|
306
|
+
def mergeFiles(sourceHash, targetHash, parentHash, targetPath)
|
307
|
+
sourceData = getFileData(File.join(getObjectsDirectory, sourceHash))
|
308
|
+
targetData = getFileData(File.join(getObjectsDirectory, targetHash))
|
309
|
+
parentData = getFileData(File.join(getObjectsDirectory, parentHash))
|
310
|
+
# get the changes it took to get from the parent to the source
|
311
|
+
sourceDiffs = Diff::LCS.diff(parentData, sourceData)
|
312
|
+
targetDiffs = Diff::LCS.diff(parentData, targetData)
|
313
|
+
# need to modify the source diffs based on the changes between the parent
|
314
|
+
# and the target.
|
315
|
+
# (this is really hacky and inefficient. i need to come up with a better
|
316
|
+
# way to do this)
|
317
|
+
targetDiffs.each do |targetDiffArray|
|
318
|
+
targetDiffArray.each do |targetDiff|
|
319
|
+
sourceDiffs.each do |sourceDiffArray|
|
320
|
+
sourceDiffArray.each do |sourceDiff|
|
321
|
+
if targetDiff.position <= sourceDiff.position
|
322
|
+
if targetDiff.action == "-"
|
323
|
+
sourceDiff.position -= targetDiff.element.length
|
324
|
+
elsif targetDiff.action == "+"
|
325
|
+
sourceDiff.position += targetDiff.element.length
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
# once we have all the changes between the source and parent, and they've
|
333
|
+
# been adjusted by the changes between target and parent, apply the source
|
334
|
+
# changes to the target
|
335
|
+
mergedData = Diff::LCS.patch!(targetData, sourceDiffs)
|
336
|
+
mergedFile = File.open(targetPath, "w")
|
337
|
+
mergedFile.write mergedData
|
338
|
+
mergedFile.close
|
339
|
+
end
|
340
|
+
|
341
|
+
# attempt to create the item specified in the entry
|
342
|
+
def createItem(directory, entry)
|
343
|
+
# each "tree" item specifies a directory that should be made corresponding to
|
344
|
+
# that directory's json file (hash)
|
345
|
+
if entry["type"] == "tree"
|
346
|
+
d = directory + '/' + entry["name"]
|
347
|
+
Dir.mkdir(d)
|
348
|
+
dirJson = getDataFromJson(File.join(getObjectsDirectory, entry["hash"]))
|
349
|
+
moveFiles(d, dirJson)
|
350
|
+
# copy the item out of the objects folder into its proper place
|
351
|
+
elsif entry["type"] == "blob"
|
352
|
+
f = File.open(File.join(directory, entry["name"]), "w")
|
353
|
+
f.write(getFileData(File.join(getObjectsDirectory, entry["hash"])))
|
354
|
+
f.close
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
# prints a list of valid functions and their uses
|
359
|
+
def help
|
360
|
+
puts "help - This text"
|
361
|
+
puts "init - Initialize a repository"
|
362
|
+
puts "commit - Commit changes to a repository"
|
363
|
+
puts "branch - Create a new branch. If not given a branch name, it lists all current branches."
|
364
|
+
puts "checkout - Move to the specified branch for modifying"
|
365
|
+
puts "replace - Pulls the desired revision down from the repository"
|
366
|
+
puts "merge - Merges the specified branch with the current branch"
|
367
|
+
puts "status - Prints a list of changes from the current version of the repository"
|
368
|
+
end
|
369
|
+
|
370
|
+
# extracts json data from a given file
|
371
|
+
def getDataFromJson(filePath)
|
372
|
+
JSON.parse(getFileData(filePath))
|
373
|
+
end
|
374
|
+
|
375
|
+
# gets and decompresses a given file
|
376
|
+
def getFileData(filePath)
|
377
|
+
decompress(File.open(filePath, "rb") { |f| f.read })
|
378
|
+
end
|
379
|
+
|
380
|
+
# decompresses given data
|
381
|
+
def decompress(data)
|
382
|
+
Zlib::Inflate.inflate(data)
|
383
|
+
end
|
384
|
+
|
385
|
+
# compresses and saves data to a path
|
386
|
+
def saveFileData(filePath, data)
|
387
|
+
f = File.open(filePath, "wb")
|
388
|
+
f.write(compress(data))
|
389
|
+
f.close
|
390
|
+
end
|
391
|
+
|
392
|
+
# compresses given data
|
393
|
+
def compress(data)
|
394
|
+
Zlib::Deflate.deflate(data)
|
395
|
+
end
|
396
|
+
|
397
|
+
# puts data in json format in a given file
|
398
|
+
# warning: this will replace all text in the file
|
399
|
+
def saveDataToJson(filePath, data)
|
400
|
+
saveFileData(filePath, JSON.generate(data))
|
401
|
+
end
|
402
|
+
|
403
|
+
# changes the hash for a specified branch, and sets the parent to the previous hash
|
404
|
+
def changeBranchHash(name, hash)
|
405
|
+
b = findBranch(name)
|
406
|
+
branches = getBranches
|
407
|
+
branches.each do |branch|
|
408
|
+
if branch == b
|
409
|
+
branch["parent"] = branch["hash"]
|
410
|
+
branch["hash"] = hash
|
411
|
+
saveBranches(branches)
|
412
|
+
return
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
# returns the path to the objects directory
|
418
|
+
def getObjectsDirectory
|
419
|
+
return @repoDir + '/' + "objects"
|
420
|
+
end
|
421
|
+
|
422
|
+
# adds an entry to the history file
|
423
|
+
def addHistoryEntry(entry)
|
424
|
+
versions = getHistoryEntries
|
425
|
+
versions << entry
|
426
|
+
saveDataToJson(File.join(@repoDir, "history"), versions)
|
427
|
+
end
|
428
|
+
|
429
|
+
# retrieves the history entries
|
430
|
+
def getHistoryEntries
|
431
|
+
history = getDataFromJson(File.join(@repoDir, "history"))
|
432
|
+
end
|
433
|
+
|
434
|
+
# change the entry in the current file
|
435
|
+
def changeCurrentEntry(entry)
|
436
|
+
newEntry = [entry]
|
437
|
+
saveDataToJson(File.join(@repoDir, "current"), newEntry)
|
438
|
+
end
|
439
|
+
|
440
|
+
# get the entry in the current file
|
441
|
+
def getCurrentEntry
|
442
|
+
current = (getDataFromJson(File.join(@repoDir, "current")))[0]
|
443
|
+
end
|
444
|
+
|
445
|
+
# get all branches
|
446
|
+
def getBranches
|
447
|
+
branches = getDataFromJson(File.join(@repoDir, "pointers"))
|
448
|
+
end
|
449
|
+
|
450
|
+
# find a branch. nil if it does not exist
|
451
|
+
def findBranch(name)
|
452
|
+
branches = getBranches
|
453
|
+
branches.each do |branch|
|
454
|
+
if branch["name"] == name
|
455
|
+
return branch
|
456
|
+
end
|
457
|
+
end
|
458
|
+
return nil
|
459
|
+
end
|
460
|
+
|
461
|
+
# gets a full hash if only given a short hash.
|
462
|
+
# allows for easier specification of revisions,
|
463
|
+
# but can be kind of dangerous if the short hash is too short
|
464
|
+
def getFullHash(hash)
|
465
|
+
Dir.foreach(getObjectsDirectory) do |dir|
|
466
|
+
dirpath = getObjectsDirectory + '/' + dir
|
467
|
+
unless File.directory?(dirpath)
|
468
|
+
if dir[0, hash.length] == hash
|
469
|
+
return dir
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
473
|
+
return nil
|
474
|
+
end
|
475
|
+
|
476
|
+
# recursively deletes all files in the directory.
|
477
|
+
def deleteFiles(directoryName)
|
478
|
+
Dir.foreach(directoryName) do |dir|
|
479
|
+
dirpath = directoryName + '/' + dir
|
480
|
+
if File.directory?(dirpath)
|
481
|
+
if dir != '.' && dir != '..' && dir != ".tvc"
|
482
|
+
deleteFiles(dirpath)
|
483
|
+
Dir.delete(dirpath)
|
484
|
+
end
|
485
|
+
else
|
486
|
+
File.delete(dirpath)
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
# gets files from the given json
|
492
|
+
def moveFiles(directoryName, jsonInfo)
|
493
|
+
jsonInfo.each do |item|
|
494
|
+
createItem(directoryName, item)
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
# save objects in the objects folder, based on hash
|
499
|
+
# create json files to index them
|
500
|
+
def createObjects(directoryName)
|
501
|
+
objects = []
|
502
|
+
Dir.foreach(directoryName) do |dir|
|
503
|
+
dirpath = directoryName + '/' + dir
|
504
|
+
# go down into each valid directory and make objects for its contents
|
505
|
+
if File.directory?(dirpath)
|
506
|
+
if dir != '.' && dir != '..' && dir != ".tvc"
|
507
|
+
hash = createObjects(dirpath)
|
508
|
+
data = { "type" => "tree", "name" => dir, "hash" => hash }
|
509
|
+
objects << data
|
510
|
+
end
|
511
|
+
# create a hash based on the file contents
|
512
|
+
else
|
513
|
+
fileData = File.open(dirpath, "rb") { |f| f.read }
|
514
|
+
tempFileName = File.join(@repoDir, "temp")
|
515
|
+
saveFileData(tempFileName, fileData)
|
516
|
+
hash = createHash(tempFileName)
|
517
|
+
data = { "type" => "blob", "name" => dir, "hash" => hash }
|
518
|
+
FileUtils.cp(tempFileName, File.join(getObjectsDirectory, hash))
|
519
|
+
File.delete(tempFileName)
|
520
|
+
objects << data
|
521
|
+
end
|
522
|
+
end
|
523
|
+
# save off objects json file to a temp file
|
524
|
+
tempFileName = File.join(@repoDir, "temp")
|
525
|
+
saveDataToJson(tempFileName, objects)
|
526
|
+
# get the hash for the temp file, save it as that, and return the hash
|
527
|
+
hash = createHash(tempFileName)
|
528
|
+
FileUtils.cp(tempFileName, File.join(getObjectsDirectory, hash))
|
529
|
+
File.delete(tempFileName)
|
530
|
+
return hash
|
531
|
+
end
|
532
|
+
|
533
|
+
# create a sha2 hash of a file
|
534
|
+
def createHash(filePath)
|
535
|
+
hashfunc = Digest::SHA2.new
|
536
|
+
open(filePath, "rb") do |io|
|
537
|
+
while !io.eof
|
538
|
+
readBuffer = io.readpartial(1024)
|
539
|
+
hashfunc.update(readBuffer)
|
540
|
+
end
|
541
|
+
end
|
542
|
+
return hashfunc.hexdigest
|
543
|
+
end
|
544
|
+
|
545
|
+
# add an entry to the branch list
|
546
|
+
def addBranchEntry(entry)
|
547
|
+
branches = getBranches
|
548
|
+
branches << entry
|
549
|
+
saveBranches(branches)
|
550
|
+
end
|
551
|
+
|
552
|
+
# save off all branches
|
553
|
+
def saveBranches(branches)
|
554
|
+
saveDataToJson(File.join(@repoDir, "pointers"), branches)
|
555
|
+
end
|
556
|
+
|
557
|
+
# get the repository directory
|
558
|
+
def getRepositoryDirectory
|
559
|
+
while true
|
560
|
+
atRoot = true
|
561
|
+
Dir.foreach(Dir.getwd) do |dir|
|
562
|
+
if dir == ".tvc"
|
563
|
+
@runDir = Dir.getwd
|
564
|
+
Dir.chdir(".tvc")
|
565
|
+
return Dir.getwd
|
566
|
+
end
|
567
|
+
if dir == ".."
|
568
|
+
atRoot = false
|
569
|
+
end
|
570
|
+
end
|
571
|
+
if atRoot
|
572
|
+
Dir.chdir(@runDir)
|
573
|
+
return nil
|
574
|
+
else
|
575
|
+
Dir.chdir("..")
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
# list all existing branches
|
581
|
+
def listBranches
|
582
|
+
branches = getBranches
|
583
|
+
branches.each do |branch|
|
584
|
+
puts branch["name"]
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
# main function
|
589
|
+
def initialize(args)
|
590
|
+
# get directories for use later
|
591
|
+
@runDir= Dir.getwd
|
592
|
+
@repoDir = getRepositoryDirectory
|
593
|
+
|
594
|
+
# if we're not initializing and we have no repository
|
595
|
+
# issue an error and exit
|
596
|
+
if args[0] != "init" && @repoDir.nil?
|
597
|
+
repositoryIssueError
|
598
|
+
Process.exit
|
599
|
+
end
|
600
|
+
# switch based on command
|
601
|
+
case args[0]
|
602
|
+
when "init"
|
603
|
+
if @repoDir.nil?
|
604
|
+
init
|
605
|
+
else
|
606
|
+
puts "Repository already initialized"
|
607
|
+
end
|
608
|
+
when "commit"
|
609
|
+
commit args[1]
|
610
|
+
when "replace"
|
611
|
+
replace args[1]
|
612
|
+
when "rollback"
|
613
|
+
replace args[1]
|
614
|
+
commit "Rolled back to #{args[1]}"
|
615
|
+
when "reset"
|
616
|
+
replace(getCurrentEntry["hash"])
|
617
|
+
when "history"
|
618
|
+
history
|
619
|
+
when "branch"
|
620
|
+
if not args[1].nil?
|
621
|
+
branch args[1]
|
622
|
+
else
|
623
|
+
listBranches
|
624
|
+
end
|
625
|
+
when "checkout"
|
626
|
+
checkout args[1]
|
627
|
+
when "merge"
|
628
|
+
merge args[1]
|
629
|
+
when "status"
|
630
|
+
status
|
631
|
+
else
|
632
|
+
help
|
633
|
+
end
|
634
|
+
end
|
635
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
require 'shoulda'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
15
|
+
require 'tvc'
|
16
|
+
|
17
|
+
class Test::Unit::TestCase
|
18
|
+
end
|
data/test/test_tvc.rb
ADDED
data/tvc.gemspec
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{tvc}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Chris O'Neal"]
|
12
|
+
s.date = %q{2011-03-25}
|
13
|
+
s.default_executable = %q{tvc}
|
14
|
+
s.description = %q{TVC is an awful git clone that should never be used, ever.}
|
15
|
+
s.email = %q{ctoneal@gmail.com}
|
16
|
+
s.executables = ["tvc"]
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE.txt",
|
19
|
+
"README.rdoc"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
"Gemfile",
|
23
|
+
"Gemfile.lock",
|
24
|
+
"LICENSE.txt",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"bin/tvc",
|
29
|
+
"lib/tvc.rb",
|
30
|
+
"test/helper.rb",
|
31
|
+
"test/test_tvc.rb",
|
32
|
+
"tvc.gemspec"
|
33
|
+
]
|
34
|
+
s.homepage = %q{http://github.com/ctoneal/tvc}
|
35
|
+
s.licenses = ["MIT"]
|
36
|
+
s.require_paths = ["lib"]
|
37
|
+
s.rubygems_version = %q{1.6.2}
|
38
|
+
s.summary = %q{TVC - Terrible Version Control}
|
39
|
+
s.test_files = [
|
40
|
+
"test/helper.rb",
|
41
|
+
"test/test_tvc.rb"
|
42
|
+
]
|
43
|
+
|
44
|
+
if s.respond_to? :specification_version then
|
45
|
+
s.specification_version = 3
|
46
|
+
|
47
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
48
|
+
s.add_runtime_dependency(%q<json>, [">= 0"])
|
49
|
+
s.add_runtime_dependency(%q<diff-lcs>, [">= 0"])
|
50
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
51
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
52
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
|
53
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
54
|
+
else
|
55
|
+
s.add_dependency(%q<json>, [">= 0"])
|
56
|
+
s.add_dependency(%q<diff-lcs>, [">= 0"])
|
57
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
58
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
59
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
60
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
61
|
+
end
|
62
|
+
else
|
63
|
+
s.add_dependency(%q<json>, [">= 0"])
|
64
|
+
s.add_dependency(%q<diff-lcs>, [">= 0"])
|
65
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
66
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
67
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
68
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tvc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chris O'Neal
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-03-25 00:00:00.000000000 -05:00
|
13
|
+
default_executable: tvc
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: json
|
17
|
+
requirement: &9782652 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *9782652
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: diff-lcs
|
28
|
+
requirement: &9781788 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *9781788
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: shoulda
|
39
|
+
requirement: &9764052 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
type: :development
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *9764052
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: bundler
|
50
|
+
requirement: &9763044 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ~>
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 1.0.0
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *9763044
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: jeweler
|
61
|
+
requirement: &9761964 !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ~>
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 1.5.2
|
67
|
+
type: :development
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: *9761964
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rcov
|
72
|
+
requirement: &9760764 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: *9760764
|
81
|
+
description: TVC is an awful git clone that should never be used, ever.
|
82
|
+
email: ctoneal@gmail.com
|
83
|
+
executables:
|
84
|
+
- tvc
|
85
|
+
extensions: []
|
86
|
+
extra_rdoc_files:
|
87
|
+
- LICENSE.txt
|
88
|
+
- README.rdoc
|
89
|
+
files:
|
90
|
+
- Gemfile
|
91
|
+
- Gemfile.lock
|
92
|
+
- LICENSE.txt
|
93
|
+
- README.rdoc
|
94
|
+
- Rakefile
|
95
|
+
- VERSION
|
96
|
+
- bin/tvc
|
97
|
+
- lib/tvc.rb
|
98
|
+
- test/helper.rb
|
99
|
+
- test/test_tvc.rb
|
100
|
+
- tvc.gemspec
|
101
|
+
has_rdoc: true
|
102
|
+
homepage: http://github.com/ctoneal/tvc
|
103
|
+
licenses:
|
104
|
+
- MIT
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
111
|
+
requirements:
|
112
|
+
- - ! '>='
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
segments:
|
116
|
+
- 0
|
117
|
+
hash: -707806879
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ! '>='
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
requirements: []
|
125
|
+
rubyforge_project:
|
126
|
+
rubygems_version: 1.6.2
|
127
|
+
signing_key:
|
128
|
+
specification_version: 3
|
129
|
+
summary: TVC - Terrible Version Control
|
130
|
+
test_files:
|
131
|
+
- test/helper.rb
|
132
|
+
- test/test_tvc.rb
|