Class: TVC

Inherits:
Object
  • Object
show all
Defined in:
lib/tvc.rb

Instance Method Summary collapse

Constructor Details

#initialize(args) ⇒ TVC

main function



589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
# File 'lib/tvc.rb', line 589

def initialize(args)
	# get directories for use later
	@runDir= Dir.getwd
	@repoDir = getRepositoryDirectory

	# if we're not initializing and we have no repository
	# issue an error and exit
	if args[0] != "init" && @repoDir.nil?
		repositoryIssueError
		Process.exit
	end
	# switch based on command
	case args[0]
	when "init"
		if @repoDir.nil?
			init
		else
			puts "Repository already initialized"
		end
	when "commit"
		commit args[1]
	when "replace"
		replace args[1]
	when "rollback"
		replace args[1]
		commit "Rolled back to #{args[1]}"
	when "reset"
		replace(getCurrentEntry["hash"])
	when "history"
		history
	when "branch"
		if not args[1].nil?
			branch args[1]
		else
			listBranches
		end
	when "checkout"
		checkout args[1]
	when "merge"
		merge args[1]
	when "status"
		status
	else
		help
	end
end

Instance Method Details

#addBranchEntry(entry) ⇒ Object

add an entry to the branch list



546
547
548
549
550
# File 'lib/tvc.rb', line 546

def addBranchEntry(entry)
	branches = getBranches
	branches << entry
	saveBranches(branches)
end

#addHistoryEntry(entry) ⇒ Object

adds an entry to the history file



423
424
425
426
427
# File 'lib/tvc.rb', line 423

def addHistoryEntry(entry)
	versions = getHistoryEntries
	versions << entry
	saveDataToJson(File.join(@repoDir, "history"), versions)
end

#branch(name) ⇒ Object

create a branch



79
80
81
82
83
84
85
86
87
88
# File 'lib/tvc.rb', line 79

def branch(name)
	b = findBranch(name)
	if b.nil?
		current = getCurrentEntry
		newBranch = {"name" => name, "hash" => current["hash"], "parent" => current["hash"]}
		addBranchEntry(newBranch)
	else
		puts "Branch already exists"
	end
end

#changeBranchHash(name, hash) ⇒ Object

changes the hash for a specified branch, and sets the parent to the previous hash



404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/tvc.rb', line 404

def changeBranchHash(name, hash)
	b = findBranch(name)
	branches = getBranches
	branches.each do |branch|
		if branch == b
			branch["parent"] = branch["hash"]
			branch["hash"] = hash
			saveBranches(branches)
			return
		end
	end
end

#changeCurrentEntry(entry) ⇒ Object

change the entry in the current file



435
436
437
438
# File 'lib/tvc.rb', line 435

def changeCurrentEntry(entry)
	newEntry = [entry]
	saveDataToJson(File.join(@repoDir, "current"), newEntry)
end

#checkout(branchName) ⇒ Object

checkout a branch



91
92
93
94
95
96
97
98
99
100
# File 'lib/tvc.rb', line 91

def checkout(branchName)
	checkoutBranch = findBranch(branchName)
	if not checkoutBranch.nil?
		changeCurrentEntry(checkoutBranch)
		replace(checkoutBranch["hash"])
	else
		puts "Branch does not exist"
		return
	end
end

#commit(message) ⇒ Object

commit current changes



36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/tvc.rb', line 36

def commit(message)
	hash = createObjects(@runDir)
	current = getCurrentEntry
	data = {"message" => message, "hash" => hash, "parent" => current["hash"]}
	addHistoryEntry(data)
	current["parent"] = data["parent"]
	current["hash"] = data["hash"]
	changeCurrentEntry(current)
	changeBranchHash(current["name"], current["hash"])
	puts hash
	puts message
end

#compress(data) ⇒ Object

compresses given data



393
394
395
# File 'lib/tvc.rb', line 393

def compress(data)
	Zlib::Deflate.deflate(data)
end

#createHash(filePath) ⇒ Object

create a sha2 hash of a file



534
535
536
537
538
539
540
541
542
543
# File 'lib/tvc.rb', line 534

def createHash(filePath)
	hashfunc = Digest::SHA2.new
	open(filePath, "rb") do |io|
		while !io.eof
			readBuffer = io.readpartial(1024)
			hashfunc.update(readBuffer)
		end
	end
	return hashfunc.hexdigest
end

#createItem(directory, entry) ⇒ Object

attempt to create the item specified in the entry



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/tvc.rb', line 342

def createItem(directory, entry)
	# each "tree" item specifies a directory that should be made corresponding to 
	# that directory's json file (hash)
	if entry["type"] == "tree"
		d = directory + '/' + entry["name"]
		Dir.mkdir(d)
		dirJson = getDataFromJson(File.join(getObjectsDirectory, entry["hash"]))
		moveFiles(d, dirJson)
	# copy the item out of the objects folder into its proper place
	elsif entry["type"] == "blob"
		f = File.open(File.join(directory, entry["name"]), "w")
		f.write(getFileData(File.join(getObjectsDirectory, entry["hash"])))
		f.close
	end
end

#createObjects(directoryName) ⇒ Object

save objects in the objects folder, based on hash create json files to index them



500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/tvc.rb', line 500

def createObjects(directoryName)
	objects = []
	Dir.foreach(directoryName) do |dir|
		dirpath = directoryName + '/' + dir
		# go down into each valid directory and make objects for its contents
		if File.directory?(dirpath)
			if dir != '.' && dir != '..' && dir != ".tvc"
				hash = createObjects(dirpath)
				data = { "type" => "tree", "name" => dir, "hash" => hash }
				objects << data
			end
		# create a hash based on the file contents
		else
			fileData = File.open(dirpath, "rb") { |f| f.read }
			tempFileName = File.join(@repoDir, "temp")
			saveFileData(tempFileName, fileData)
			hash = createHash(tempFileName)
			data = { "type" => "blob", "name" => dir, "hash" => hash }
			FileUtils.cp(tempFileName, File.join(getObjectsDirectory, hash))
			File.delete(tempFileName)
			objects << data
		end
	end
	# save off objects json file to a temp file
	tempFileName = File.join(@repoDir, "temp")
	saveDataToJson(tempFileName, objects)
	# get the hash for the temp file, save it as that, and return the hash
	hash = createHash(tempFileName)
	FileUtils.cp(tempFileName, File.join(getObjectsDirectory, hash))
	File.delete(tempFileName)
	return hash
end

#decompress(data) ⇒ Object

decompresses given data



381
382
383
# File 'lib/tvc.rb', line 381

def decompress(data)
	Zlib::Inflate.inflate(data)
end

#deleteFiles(directoryName) ⇒ Object

recursively deletes all files in the directory.



477
478
479
480
481
482
483
484
485
486
487
488
489
# File 'lib/tvc.rb', line 477

def deleteFiles(directoryName)
	Dir.foreach(directoryName) do |dir|
		dirpath = directoryName + '/' + dir
		if File.directory?(dirpath)
			if dir != '.' && dir != '..' && dir != ".tvc"
				deleteFiles(dirpath)
				Dir.delete(dirpath)
			end
		else
			File.delete(dirpath)
		end
	end
end

#findBranch(name) ⇒ Object

find a branch. nil if it does not exist



451
452
453
454
455
456
457
458
459
# File 'lib/tvc.rb', line 451

def findBranch(name)
	branches = getBranches
	branches.each do |branch|
		if branch["name"] == name
			return branch
		end
	end
	return nil
end

#findChanges(directory, currentHash) ⇒ Object

creates a list of changes made



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/tvc.rb', line 134

def findChanges(directory, currentHash)
	currentData = getDataFromJson(File.join(getObjectsDirectory, currentHash))
	changes = []
	# loop through every item in the directory
	Dir.foreach(directory) do |dirItem|
		if dirItem != '.' && dirItem != '..' && dirItem != ".tvc"
			dirPath = File.join(directory, dirItem)
			foundItem = nil
			itemType = File.directory?(dirPath) ? "tree" : "blob"
			# compare the item to the directory's json
			currentData.each_index do |index|
				currentItem = currentData[index]
				if currentItem["name"] == dirItem && currentItem["type"] == itemType
					foundItem = currentItem
					currentData.delete_at(index)
					break
				end
			end
			# if nothing in the json matched this item, it must be new
			if foundItem.nil?
				newChange = { "type" => "added", "name" => dirPath.gsub(@runDir, "") }
				changes << newChange
				if itemType == "tree"
					newChanges = findChangesInNewDirectory(dirPath)
					newChanges.each do |change|
						changes << change
					end
				end
			# we found a match, so we need to compare it
			else
				# if it's a directory, continue down to find changes in there
				if itemType == "tree"
					newChanges = findChanges(dirPath, foundItem["hash"])
					newChanges.each do |newChange|
						changes << newChange
					end
				# if it's a file, compare the contents
				else
					# this is a really hacky way to do this, but i'm so lazy
					repoData = getFileData(File.join(getObjectsDirectory, foundItem["hash"]))
					repoData = repoData.gsub(/\s/, "")
					fileData = File.open(dirPath) { |f| f.read }
					fileData = fileData.gsub(/\s/, "")
					diffs = Diff::LCS.diff(repoData, fileData)
					modified = false
					diffs.each do |diffArray|
						diffArray.each do |diff|
							if diff.action != "=" && diff.element != ""
								modified = true;
							end
						end
					end
					if modified
						newChange = { "type" => "modified", "name" => dirPath.gsub(@runDir, "") }
						changes << newChange
					end
				end
			end
		end
	end
	# if we still have items in the directory json, they must have been deleted
	currentData.each do |currentItem|
		newChange = { "type" => "deleted", "name" => currentItem["name"] }
		changes << newChange
	end
	return changes
end

#findChangesInNewDirectory(directory) ⇒ Object

returns a list of everything in this directory.



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/tvc.rb', line 203

def findChangesInNewDirectory(directory)
	changes = []
	Dir.foreach(directory) do |dirItem|
		if dirItem != '.' && dirItem != '..' && dirItem != ".tvc"
			dirPath = File.join(directory, dirItem)
			newChange = { "type" => "added", "name" => dirPath.gsub(@runDir, "") }
			changes << newChange
			if File.directory?(dirPath)
				newChanges = findChangesInNewDirectory(dirPath)
				newChanges.each do |change|
					changes << change
				end
			end
		end
	end
	return changes
end

#findCommonAncestor(source, target) ⇒ Object

attempts to find a common parent for the two revisions



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/tvc.rb', line 222

def findCommonAncestor(source, target)
	sourceParentHash = source["hash"]
	versions = getHistoryEntries
	# loop through until we hit the end of the tree for the source
	while not sourceParentHash.nil?
		sourceParent = nil
		targetParentHash = target["hash"]
		versions.each do |version|
			if version["hash"] == sourceParentHash
				sourceParent = version
				break
			end
		end
		# loop through the target tree, hoping we find something that matches up
		while not targetParentHash.nil?
			versions.each do |version|
				if version["hash"] == targetParentHash
					if targetParentHash == sourceParentHash
						return version
					else
						targetParentHash = version["parent"]
					end
				end
			end
		end
		if sourceParent.nil?
			sourceParentHash = nil
		else
			sourceParentHash = sourceParent["parent"]
		end
	end
	return nil
end

#getBranchesObject

get all branches



446
447
448
# File 'lib/tvc.rb', line 446

def getBranches
	branches = getDataFromJson(File.join(@repoDir, "pointers"))
end

#getCurrentEntryObject

get the entry in the current file



441
442
443
# File 'lib/tvc.rb', line 441

def getCurrentEntry
	current = (getDataFromJson(File.join(@repoDir, "current")))[0]
end

#getDataFromJson(filePath) ⇒ Object

extracts json data from a given file



371
372
373
# File 'lib/tvc.rb', line 371

def getDataFromJson(filePath)
	JSON.parse(getFileData(filePath))
end

#getFileData(filePath) ⇒ Object

gets and decompresses a given file



376
377
378
# File 'lib/tvc.rb', line 376

def getFileData(filePath)
	decompress(File.open(filePath, "rb") { |f| f.read })	
end

#getFullHash(hash) ⇒ Object

gets a full hash if only given a short hash. allows for easier specification of revisions, but can be kind of dangerous if the short hash is too short



464
465
466
467
468
469
470
471
472
473
474
# File 'lib/tvc.rb', line 464

def getFullHash(hash)
	Dir.foreach(getObjectsDirectory) do |dir|
		dirpath = getObjectsDirectory + '/' + dir
		unless File.directory?(dirpath)
			if dir[0, hash.length] == hash
				return dir
			end
		end
	end
	return nil
end

#getHistoryEntriesObject

retrieves the history entries



430
431
432
# File 'lib/tvc.rb', line 430

def getHistoryEntries
	history = getDataFromJson(File.join(@repoDir, "history"))
end

#getObjectsDirectoryObject

returns the path to the objects directory



418
419
420
# File 'lib/tvc.rb', line 418

def getObjectsDirectory
	return @repoDir + '/' + "objects"
end

#getRepositoryDirectoryObject

get the repository directory



558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
# File 'lib/tvc.rb', line 558

def getRepositoryDirectory
	while true
		atRoot = true
		Dir.foreach(Dir.getwd) do |dir|
			if dir == ".tvc"
				@runDir = Dir.getwd
				Dir.chdir(".tvc")
				return Dir.getwd
			end
			if dir == ".."
				atRoot = false
			end
		end
		if atRoot
			Dir.chdir(@runDir)
			return nil
		else
			Dir.chdir("..")
		end
	end
end

#helpObject

prints a list of valid functions and their uses



359
360
361
362
363
364
365
366
367
368
# File 'lib/tvc.rb', line 359

def help
	puts "help - This text"
	puts "init - Initialize a repository"
	puts "commit - Commit changes to a repository"
	puts "branch - Create a new branch.  If not given a branch name, it lists all current branches."
	puts "checkout - Move to the specified branch for modifying"
	puts "replace - Pulls the desired revision down from the repository"
	puts "merge - Merges the specified branch with the current branch"
	puts "status - Prints a list of changes from the current version of the repository"
end

#historyObject

prints a list of commits up the chain



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/tvc.rb', line 63

def history
	current = getCurrentEntry
	hash = current["hash"]
	versions = getHistoryEntries
	while not hash.nil?
		versions.each do |version|
			if version["hash"] == hash
				puts "#{version["hash"]}\n#{version["message"]}\n\n"
				hash = version["parent"]
				break
			end
		end
	end
end

#initObject

initialize a repository



25
26
27
28
29
30
31
32
33
# File 'lib/tvc.rb', line 25

def init
	Dir.chdir(@runDir)
	Dir.mkdir(".tvc") unless Dir::exists?(".tvc")
	@repoDir = getRepositoryDirectory
	Dir.mkdir(getObjectsDirectory) unless Dir::exists?(getObjectsDirectory)
	saveDataToJson(File.join(@repoDir, "pointers"), [{"name" => "master", "hash" => nil, "parent" => nil}])
	saveDataToJson(File.join(@repoDir, "history"), [])
	saveDataToJson(File.join(@repoDir, "current"), [{"name" => "master", "hash" => nil, "parent" => nil}])
end

#listBranchesObject

list all existing branches



581
582
583
584
585
586
# File 'lib/tvc.rb', line 581

def listBranches
	branches = getBranches
	branches.each do |branch|
		puts branch["name"]
	end
end

#merge(branchName) ⇒ Object

merges a branch into the current branch i’m betting this is going to look like crap



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/tvc.rb', line 104

def merge(branchName)
	source = findBranch(branchName)
	target = getCurrentEntry
	parent = findCommonAncestor(source, target)
	if not source.nil?
		if not parent.nil?
			mergeFilesForDirectory(@runDir, source["hash"], target["hash"], parent["hash"])
			commit("Merged branch #{branchName}")
		else
			puts "Could not find common ancestor"
		end
	else
		puts "Branch doesn't exist"
	end
end

#mergeFiles(sourceHash, targetHash, parentHash, targetPath) ⇒ Object

attempt to merge two files together given a common parent this seems like a pretty naive way of doing things, but i’m lazy!



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/tvc.rb', line 306

def mergeFiles(sourceHash, targetHash, parentHash, targetPath)
	sourceData = getFileData(File.join(getObjectsDirectory, sourceHash))
	targetData = getFileData(File.join(getObjectsDirectory, targetHash))
	parentData = getFileData(File.join(getObjectsDirectory, parentHash))
	# get the changes it took to get from the parent to the source
	sourceDiffs = Diff::LCS.diff(parentData, sourceData)
	targetDiffs = Diff::LCS.diff(parentData, targetData)
	# need to modify the source diffs based on the changes between the parent
	# and the target.
	# (this is really hacky and inefficient.  i need to come up with a better 
	# way to do this)
	targetDiffs.each do |targetDiffArray|
		targetDiffArray.each do |targetDiff|
			sourceDiffs.each do |sourceDiffArray|
				sourceDiffArray.each do |sourceDiff|
					if targetDiff.position <= sourceDiff.position
						if targetDiff.action == "-"
							sourceDiff.position -= targetDiff.element.length
						elsif targetDiff.action == "+"
							sourceDiff.position += targetDiff.element.length
						end
					end
				end
			end
		end
	end
	# once we have all the changes between the source and parent, and they've
	# been adjusted by the changes between target and parent, apply the source
	# changes to the target
	mergedData = Diff::LCS.patch!(targetData, sourceDiffs)
	mergedFile = File.open(targetPath, "w")
	mergedFile.write mergedData
	mergedFile.close
end

#mergeFilesForDirectory(directory, sourceHash, targetHash, parentHash) ⇒ Object

attempts to merge the items for the specified directory together



257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/tvc.rb', line 257

def mergeFilesForDirectory(directory, sourceHash, targetHash, parentHash)
	sourceJson = getDataFromJson(File.join(getObjectsDirectory, sourceHash))
	targetJson = getDataFromJson(File.join(getObjectsDirectory, targetHash))
	parentJson = getDataFromJson(File.join(getObjectsDirectory, parentHash))
	# for each item in the source, attempt to find a corresponding item in the target
	sourceJson.each do |sourceItem|
		matchingItem = nil
		parentMatch = nil
		targetJson.each do |targetItem|
			if targetItem["name"] == sourceItem["name"] && targetItem["type"] == sourceItem["type"]
				matchingItem = targetItem
				break
			end
		end
		parentJson.each do |parentItem|
			if parentItem["name"] == sourceItem["name"] && parentItem["type"] == sourceItem["type"]
				parentMatch = parentItem
				break
			end
		end
		# we're only going to attempt to merge if we've found some common parent
		# otherwise, we're just going to straight up replace that thing
		if parentMatch.nil?
			puts "No common ancestor found, pushing change to target"
			createItem(directory, sourceItem)
		# if there's no matching item, but there is a parent, well, i guess it got deleted 
		# in the target, but is needed by source.  add it back in.
		elsif matchingItem.nil?
			puts "No matching item found, pushing change to target"
			createItem(directory, sourceItem)
		# if we've got everything we need, we'll attempt to merge
		else
			# if this is a tree, continue on to do all the logic for it
			if sourceItem["type"] == "tree"
				mergeFilesForDirectory(File.join(directory, sourceItem["name"]), sourceItem["hash"], matchingItem["hash"], parentMatch["hash"])
			# if it's a fine, try to merge the two together
			# man, this might fail horribly with binary files.
			# watch out for that
			elsif sourceItem["type"] == "blob"
				if sourceItem["hash"] != matchingItem["hash"]
					mergeFiles(sourceItem["hash"], matchingItem["hash"], parentMatch["hash"], File.join(directory, sourceItem["name"]))
				end
			end
		end
	end
end

#moveFiles(directoryName, jsonInfo) ⇒ Object

gets files from the given json



492
493
494
495
496
# File 'lib/tvc.rb', line 492

def moveFiles(directoryName, jsonInfo)
	jsonInfo.each do |item|
		createItem(directoryName, item)
	end
end

#replace(versionHash) ⇒ Object

replace current files with requested version



50
51
52
53
54
55
56
57
58
59
60
# File 'lib/tvc.rb', line 50

def replace(versionHash)
	hash = getFullHash(versionHash)
	if not hash.nil?
		deleteFiles(@runDir)
		rootInfo = getDataFromJson(File.join(getObjectsDirectory, hash))
		moveFiles(@runDir, rootInfo)
	else
		puts "This version does not exist"
		return
	end
end

#repositoryIssueErrorObject

this was a commonly used error, hence the function just for it.



18
19
20
21
22
# File 'lib/tvc.rb', line 18

def repositoryIssueError
	puts "The repository either has not been initialized 
		or this command is not being run from the base
		directory of the repository".gsub(/\s+/, " ").strip
end

#saveBranches(branches) ⇒ Object

save off all branches



553
554
555
# File 'lib/tvc.rb', line 553

def saveBranches(branches)
	saveDataToJson(File.join(@repoDir, "pointers"), branches)
end

#saveDataToJson(filePath, data) ⇒ Object

puts data in json format in a given file warning: this will replace all text in the file



399
400
401
# File 'lib/tvc.rb', line 399

def saveDataToJson(filePath, data)
	saveFileData(filePath, JSON.generate(data))
end

#saveFileData(filePath, data) ⇒ Object

compresses and saves data to a path



386
387
388
389
390
# File 'lib/tvc.rb', line 386

def saveFileData(filePath, data)
	f = File.open(filePath, "wb")
	f.write(compress(data))
	f.close
end

#statusObject

find and display the changes from the current version in the repository



121
122
123
124
125
126
127
128
129
130
131
# File 'lib/tvc.rb', line 121

def status
	current = getCurrentEntry
	changes = findChanges(@runDir, current["hash"])
	if changes.empty?
		puts "No changes made"
	else
		changes.each do |change|
			puts "#{change["name"]} #{change["type"]}"
		end
	end
end