Class: TVC
- Inherits:
-
Object
- Object
- TVC
- Defined in:
- lib/tvc.rb
Instance Method Summary collapse
-
#addBranchEntry(entry) ⇒ Object
add an entry to the branch list.
-
#addHistoryEntry(entry) ⇒ Object
adds an entry to the history file.
-
#branch(name) ⇒ Object
create a branch.
-
#changeBranchHash(name, hash) ⇒ Object
changes the hash for a specified branch, and sets the parent to the previous hash.
-
#changeCurrentEntry(entry) ⇒ Object
change the entry in the current file.
-
#checkout(branchName) ⇒ Object
checkout a branch.
-
#commit(message) ⇒ Object
commit current changes.
-
#compress(data) ⇒ Object
compresses given data.
-
#createHash(filePath) ⇒ Object
create a sha2 hash of a file.
-
#createItem(directory, entry) ⇒ Object
attempt to create the item specified in the entry.
-
#createObjects(directoryName) ⇒ Object
save objects in the objects folder, based on hash create json files to index them.
-
#decompress(data) ⇒ Object
decompresses given data.
-
#deleteFiles(directoryName) ⇒ Object
recursively deletes all files in the directory.
-
#findBranch(name) ⇒ Object
find a branch.
-
#findChanges(directory, currentHash) ⇒ Object
creates a list of changes made.
-
#findChangesInNewDirectory(directory) ⇒ Object
returns a list of everything in this directory.
-
#findCommonAncestor(source, target) ⇒ Object
attempts to find a common parent for the two revisions.
-
#getBranches ⇒ Object
get all branches.
-
#getCurrentEntry ⇒ Object
get the entry in the current file.
-
#getDataFromJson(filePath) ⇒ Object
extracts json data from a given file.
-
#getFileData(filePath) ⇒ Object
gets and decompresses a given file.
-
#getFullHash(hash) ⇒ Object
gets a full hash if only given a short hash.
-
#getHistoryEntries ⇒ Object
retrieves the history entries.
-
#getObjectsDirectory ⇒ Object
returns the path to the objects directory.
-
#getRepositoryDirectory ⇒ Object
get the repository directory.
-
#help ⇒ Object
prints a list of valid functions and their uses.
-
#history ⇒ Object
prints a list of commits up the chain.
-
#init ⇒ Object
initialize a repository.
-
#initialize(args) ⇒ TVC
constructor
main function.
-
#listBranches ⇒ Object
list all existing branches.
-
#merge(branchName) ⇒ Object
merges a branch into the current branch i’m betting this is going to look like crap.
-
#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!.
-
#mergeFilesForDirectory(directory, sourceHash, targetHash, parentHash) ⇒ Object
attempts to merge the items for the specified directory together.
-
#moveFiles(directoryName, jsonInfo) ⇒ Object
gets files from the given json.
-
#replace(versionHash) ⇒ Object
replace current files with requested version.
-
#repositoryIssueError ⇒ Object
this was a commonly used error, hence the function just for it.
-
#saveBranches(branches) ⇒ Object
save off all branches.
-
#saveDataToJson(filePath, data) ⇒ Object
puts data in json format in a given file warning: this will replace all text in the file.
-
#saveFileData(filePath, data) ⇒ Object
compresses and saves data to a path.
-
#status ⇒ Object
find and display the changes from the current version in the repository.
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() hash = createObjects(@runDir) current = getCurrentEntry data = {"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 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 |
#getBranches ⇒ Object
get all branches
446 447 448 |
# File 'lib/tvc.rb', line 446 def getBranches branches = getDataFromJson(File.join(@repoDir, "pointers")) end |
#getCurrentEntry ⇒ Object
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 |
#getHistoryEntries ⇒ Object
retrieves the history entries
430 431 432 |
# File 'lib/tvc.rb', line 430 def getHistoryEntries history = getDataFromJson(File.join(@repoDir, "history")) end |
#getObjectsDirectory ⇒ Object
returns the path to the objects directory
418 419 420 |
# File 'lib/tvc.rb', line 418 def getObjectsDirectory return @repoDir + '/' + "objects" end |
#getRepositoryDirectory ⇒ Object
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 |
#help ⇒ Object
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 |
#history ⇒ Object
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 |
#init ⇒ Object
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 |
#listBranches ⇒ Object
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 |
#repositoryIssueError ⇒ Object
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 |
#status ⇒ Object
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 |