├── testing ├── src │ ├── a.txt │ ├── b.txt │ ├── c.txt │ ├── d.txt │ ├── e.txt │ ├── g.txt │ ├── nota.txt │ ├── notb.txt │ ├── notf.txt │ ├── wug.txt │ ├── wug2.txt │ ├── wug3.txt │ ├── notwug.txt │ ├── conflict2.txt │ ├── conflict5.txt │ ├── conflict1.txt │ ├── conflict3.txt │ └── conflict6.txt ├── samples │ ├── test01-init.in │ ├── test11-basic-status.in │ ├── setup2.inc │ ├── test42-other-err.in │ ├── test41-no-command-err.in │ ├── test18-nop-add.in │ ├── setup1.inc │ ├── test19-add-missing-err.in │ ├── test21-nop-remove-err.in │ ├── test20-status-after-commit.in │ ├── blank-status.inc │ ├── test15-remove-add-status.in │ ├── test12-add-status.in │ ├── test02-basic-checkout.in │ ├── test13-remove-status.in │ ├── Test01.in │ ├── test22-remove-deleted-file.in │ ├── test14-add-remove-status.in │ ├── test03-basic-log.in │ ├── test32-file-overwrite-err.in │ ├── test40-special-merge-cases.in │ ├── Test03.in │ ├── test30-branches.in │ ├── test24-global-log-prev.in │ ├── Test02.in │ ├── test26-successful-find-orphan.in │ ├── test33-merge-no-conflicts.in │ ├── prelude1.inc │ ├── test25-successful-find.in │ ├── test36-merge-err.in │ ├── test23-global-log.in │ ├── test04-prev-checkout.in │ ├── test34-merge-conflicts.in │ ├── test35-merge-rm-conflicts.in │ ├── test43-criss-cross-merge-b.in │ ├── test44-bai-merge.in │ └── test37-reset1.in ├── Makefile └── tester.py ├── gitlet ├── Dumpable.java ├── GitletException.java ├── UnitTest.java ├── DumpObj.java ├── Makefile ├── Blob.java ├── Commit.java ├── Main.java ├── Utils.java └── Repo.java ├── LICENSE ├── README.md ├── Makefile ├── .gitignore └── gitlet-design.md /testing/src/a.txt: -------------------------------------------------------------------------------- 1 | a -------------------------------------------------------------------------------- /testing/src/b.txt: -------------------------------------------------------------------------------- 1 | b -------------------------------------------------------------------------------- /testing/src/c.txt: -------------------------------------------------------------------------------- 1 | c -------------------------------------------------------------------------------- /testing/src/d.txt: -------------------------------------------------------------------------------- 1 | d -------------------------------------------------------------------------------- /testing/src/e.txt: -------------------------------------------------------------------------------- 1 | e -------------------------------------------------------------------------------- /testing/src/g.txt: -------------------------------------------------------------------------------- 1 | g 2 | -------------------------------------------------------------------------------- /testing/src/nota.txt: -------------------------------------------------------------------------------- 1 | not a -------------------------------------------------------------------------------- /testing/src/notb.txt: -------------------------------------------------------------------------------- 1 | not b -------------------------------------------------------------------------------- /testing/src/notf.txt: -------------------------------------------------------------------------------- 1 | not f -------------------------------------------------------------------------------- /testing/src/wug.txt: -------------------------------------------------------------------------------- 1 | This is a wug. -------------------------------------------------------------------------------- /testing/src/wug2.txt: -------------------------------------------------------------------------------- 1 | This is wug2. -------------------------------------------------------------------------------- /testing/src/wug3.txt: -------------------------------------------------------------------------------- 1 | This is wug3. -------------------------------------------------------------------------------- /testing/src/notwug.txt: -------------------------------------------------------------------------------- 1 | This is not a wug. -------------------------------------------------------------------------------- /testing/src/conflict2.txt: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | This is wug2.======= 3 | >>>>>>> 4 | -------------------------------------------------------------------------------- /testing/src/conflict5.txt: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | This is a wug.======= 3 | >>>>>>> 4 | -------------------------------------------------------------------------------- /testing/samples/test01-init.in: -------------------------------------------------------------------------------- 1 | # Basic initialization 2 | > init 3 | <<< 4 | E .gitlet 5 | -------------------------------------------------------------------------------- /testing/samples/test11-basic-status.in: -------------------------------------------------------------------------------- 1 | # Empty status 2 | I prelude1.inc 3 | I blank-status.inc -------------------------------------------------------------------------------- /testing/src/conflict1.txt: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | This is wug2.======= 3 | This is not a wug.>>>>>>> 4 | -------------------------------------------------------------------------------- /testing/src/conflict3.txt: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | This is not a wug.======= 3 | This is a wug.>>>>>>> 4 | -------------------------------------------------------------------------------- /testing/src/conflict6.txt: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | This is a wug.======= 3 | This is not a wug.>>>>>>> 4 | -------------------------------------------------------------------------------- /testing/samples/setup2.inc: -------------------------------------------------------------------------------- 1 | # Set up one commit with two files. 2 | I setup1.inc 3 | > commit "Two files" 4 | <<< -------------------------------------------------------------------------------- /testing/samples/test42-other-err.in: -------------------------------------------------------------------------------- 1 | > 2 | Please enter a command. 3 | <<< 4 | > status 5 | Not in an initialized Gitlet directory. 6 | <<< 7 | -------------------------------------------------------------------------------- /testing/samples/test41-no-command-err.in: -------------------------------------------------------------------------------- 1 | # Test for wrong command name. 2 | I prelude1.inc 3 | > glorp foo 4 | No command with that name exists. 5 | <<< -------------------------------------------------------------------------------- /testing/samples/test18-nop-add.in: -------------------------------------------------------------------------------- 1 | # Check that adding a tracked, unchanged file has no effect. 2 | I setup2.inc 3 | > add f.txt 4 | <<< 5 | I blank-status.inc -------------------------------------------------------------------------------- /testing/samples/setup1.inc: -------------------------------------------------------------------------------- 1 | # Initialize and add two files. 2 | I prelude1.inc 3 | + f.txt wug.txt 4 | + g.txt notwug.txt 5 | > add g.txt 6 | <<< 7 | > add f.txt 8 | <<< -------------------------------------------------------------------------------- /testing/samples/test19-add-missing-err.in: -------------------------------------------------------------------------------- 1 | # Check that adding nonexistent file gets correct error. 2 | I prelude1.inc 3 | > add f.txt 4 | File does not exist. 5 | <<< 6 | I blank-status.inc -------------------------------------------------------------------------------- /testing/samples/test21-nop-remove-err.in: -------------------------------------------------------------------------------- 1 | # Check error message for removal of unstaged, untracked file. 2 | I prelude1.inc 3 | + f.txt wug.txt 4 | > rm f.txt 5 | No reason to remove the file. 6 | <<< 7 | -------------------------------------------------------------------------------- /testing/samples/test20-status-after-commit.in: -------------------------------------------------------------------------------- 1 | # Check that commit clears the staging area. 2 | I setup2.inc 3 | I blank-status.inc 4 | > rm f.txt 5 | <<< 6 | > commit "Removed f.txt" 7 | <<< 8 | I blank-status.inc -------------------------------------------------------------------------------- /testing/samples/blank-status.inc: -------------------------------------------------------------------------------- 1 | > status 2 | === Branches === 3 | *master 4 | 5 | === Staged Files === 6 | 7 | === Removed Files === 8 | 9 | === Modifications Not Staged For Commit === 10 | 11 | === Untracked Files === 12 | 13 | <<< -------------------------------------------------------------------------------- /testing/samples/test15-remove-add-status.in: -------------------------------------------------------------------------------- 1 | # Status with a removal followed by an add that restores former 2 | # contents. Should simply "unremove" the file without staging. 3 | I setup2.inc 4 | > rm f.txt 5 | <<< 6 | * f.txt 7 | + f.txt wug.txt 8 | > add f.txt 9 | <<< 10 | I blank-status.inc -------------------------------------------------------------------------------- /testing/samples/test12-add-status.in: -------------------------------------------------------------------------------- 1 | # Status with two adds 2 | I setup1.inc 3 | > status 4 | === Branches === 5 | *master 6 | 7 | === Staged Files === 8 | f.txt 9 | g.txt 10 | 11 | === Removed Files === 12 | 13 | === Modifications Not Staged For Commit === 14 | 15 | === Untracked Files === 16 | 17 | <<< -------------------------------------------------------------------------------- /testing/samples/test02-basic-checkout.in: -------------------------------------------------------------------------------- 1 | # A simple test of adding, committing, modifying, and checking out. 2 | > init 3 | <<< 4 | + wug.txt wug.txt 5 | > add wug.txt 6 | <<< 7 | > commit "added wug" 8 | <<< 9 | + wug.txt notwug.txt 10 | # Must change 11 | > checkout -- wug.txt 12 | <<< 13 | = wug.txt wug.txt 14 | -------------------------------------------------------------------------------- /gitlet/Dumpable.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | import java.io.Serializable; 4 | 5 | /** An interface describing dumpable objects. 6 | * @author P. N. Hilfinger 7 | */ 8 | interface Dumpable extends Serializable { 9 | /** Print useful information about this object on System.out. */ 10 | void dump(); 11 | } 12 | -------------------------------------------------------------------------------- /testing/samples/test13-remove-status.in: -------------------------------------------------------------------------------- 1 | # Remove a tracked file and check status. 2 | I setup2.inc 3 | > rm f.txt 4 | <<< 5 | * f.txt 6 | > status 7 | === Branches === 8 | *master 9 | 10 | === Staged Files === 11 | 12 | === Removed Files === 13 | f.txt 14 | 15 | === Modifications Not Staged For Commit === 16 | 17 | === Untracked Files === 18 | 19 | <<< -------------------------------------------------------------------------------- /testing/samples/Test01.in: -------------------------------------------------------------------------------- 1 | # Test add, commit, find, log 2 | I setup2.inc 3 | > branch mytest 4 | <<< 5 | > log 6 | ${COMMIT_LOG} 7 | ${COMMIT_LOG} 8 | <<<* 9 | D L1 "${1}" 10 | D L2 "${2}" 11 | > log 12 | === 13 | ${COMMIT_HEAD} 14 | Two files 15 | 16 | === 17 | ${COMMIT_HEAD}${ARBLINES} 18 | <<<* 19 | D FINDID "${2}" 20 | > find "initial commit" 21 | ${FINDID} 22 | <<<* -------------------------------------------------------------------------------- /testing/samples/test22-remove-deleted-file.in: -------------------------------------------------------------------------------- 1 | # Check that we can unstage a file we have deleted with plain Unix 'rm' 2 | I setup2.inc 3 | - f.txt 4 | > rm f.txt 5 | <<< 6 | > status 7 | === Branches === 8 | *master 9 | 10 | === Staged Files === 11 | 12 | === Removed Files === 13 | f.txt 14 | 15 | === Modifications Not Staged For Commit === 16 | 17 | === Untracked Files === 18 | 19 | <<< -------------------------------------------------------------------------------- /testing/samples/test14-add-remove-status.in: -------------------------------------------------------------------------------- 1 | # Status with two adds 2 | I setup1.inc 3 | > rm f.txt 4 | <<< 5 | > status 6 | === Branches === 7 | \*master 8 | 9 | === Staged Files === 10 | g.txt 11 | 12 | === Removed Files === 13 | 14 | === Modifications Not Staged For Commit === 15 | 16 | === Untracked Files === 17 | ${ARBLINES} 18 | 19 | <<<* 20 | # File was not tracked, so make sure it's still there 21 | = f.txt wug.txt -------------------------------------------------------------------------------- /testing/samples/test03-basic-log.in: -------------------------------------------------------------------------------- 1 | # Set up a simple chain of commits and check their log. 2 | > init 3 | <<< 4 | + wug.txt wug.txt 5 | > add wug.txt 6 | <<< 7 | > commit "added wug" 8 | <<< 9 | D HEADER "commit [a-f0-9]+" 10 | D DATE "Date: \w\w\w \w\w\w \d+ \d\d:\d\d:\d\d \d\d\d\d [-+]\d\d\d\d" 11 | > log 12 | === 13 | ${HEADER} 14 | ${DATE} 15 | added wug 16 | 17 | === 18 | ${HEADER} 19 | ${DATE} 20 | initial commit 21 | 22 | <<<* 23 | -------------------------------------------------------------------------------- /testing/samples/test32-file-overwrite-err.in: -------------------------------------------------------------------------------- 1 | # Make sure checkout does not overwrite files. 2 | I prelude1.inc 3 | > branch other 4 | <<< 5 | + f.txt wug.txt 6 | + g.txt notwug.txt 7 | > add g.txt 8 | <<< 9 | > add f.txt 10 | <<< 11 | > commit "Main two files" 12 | <<< 13 | E f.txt 14 | E g.txt 15 | > checkout other 16 | <<< 17 | + f.txt notwug.txt 18 | > checkout master 19 | There is an untracked file in the way; delete it, or add and commit it first. 20 | <<< 21 | -------------------------------------------------------------------------------- /testing/samples/test40-special-merge-cases.in: -------------------------------------------------------------------------------- 1 | # Test special cases of merge. 2 | I setup2.inc 3 | > branch b1 4 | <<< 5 | + h.txt wug2.txt 6 | > add h.txt 7 | <<< 8 | > commit "Add h.txt" 9 | <<< 10 | > branch b2 11 | <<< 12 | > rm f.txt 13 | <<< 14 | > commit "remove f.txt" 15 | <<< 16 | > merge b1 17 | Given branch is an ancestor of the current branch. 18 | <<< 19 | > checkout b2 20 | <<< 21 | = f.txt wug.txt 22 | > merge master 23 | Current branch fast-forwarded. 24 | <<< 25 | * f.txt 26 | 27 | -------------------------------------------------------------------------------- /testing/samples/Test03.in: -------------------------------------------------------------------------------- 1 | #test branch 2 | I setup2.inc 3 | > branch mytest 4 | <<< 5 | > rm f.txt 6 | <<< 7 | + b.txt c.txt 8 | > add b.txt 9 | <<< 10 | > commit "remove f.txt and add b.txt" 11 | <<< 12 | * f.txt 13 | > checkout mytest 14 | <<< 15 | > branch mytest2 16 | <<< 17 | + c.txt b.txt 18 | > add c.txt 19 | <<< 20 | > commit "add c.txt" 21 | <<< 22 | > checkout master 23 | <<< 24 | = b.txt c.txt 25 | = g.txt notwug.txt 26 | * f.txt 27 | > merge mytest 28 | <<< 29 | = g.txt notwug.txt 30 | * f.txt -------------------------------------------------------------------------------- /testing/samples/test30-branches.in: -------------------------------------------------------------------------------- 1 | # Create two branches and switch between them 2 | I prelude1.inc 3 | > branch other 4 | <<< 5 | + f.txt wug.txt 6 | + g.txt notwug.txt 7 | > add g.txt 8 | <<< 9 | > add f.txt 10 | <<< 11 | > commit "Main two files" 12 | <<< 13 | E f.txt 14 | E g.txt 15 | > checkout other 16 | <<< 17 | * f.txt 18 | * g.txt 19 | + f.txt notwug.txt 20 | > add f.txt 21 | <<< 22 | > commit "Alternative file" 23 | <<< 24 | = f.txt notwug.txt 25 | * g.txt 26 | > checkout master 27 | <<< 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /testing/samples/test24-global-log-prev.in: -------------------------------------------------------------------------------- 1 | # Check that global-log prints out commits that are no longer in any branch. 2 | I setup2.inc 3 | + h.txt wug.txt 4 | > add h.txt 5 | <<< 6 | > commit "Add h" 7 | <<< 8 | > log 9 | ${COMMIT_LOG} 10 | ${COMMIT_LOG} 11 | ${COMMIT_LOG} 12 | <<<* 13 | D L1 "${1}" 14 | D L2 "${2}" 15 | D L3 "${3}" 16 | > log 17 | === 18 | ${COMMIT_HEAD} 19 | Add h 20 | 21 | === 22 | ${COMMIT_HEAD}${ARBLINES} 23 | <<<* 24 | D ID "${2}" 25 | > reset ${ID} 26 | <<< 27 | > global-log 28 | ${ARBLINES}${L1}?${ARBLINES} 29 | <<<* -------------------------------------------------------------------------------- /gitlet/GitletException.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | /** General exception indicating a Gitlet error. For fatal errors, the 4 | * result of .getMessage() is the error message to be printed. 5 | * @author P. N. Hilfinger 6 | */ 7 | class GitletException extends RuntimeException { 8 | 9 | 10 | /** A GitletException with no message. */ 11 | GitletException() { 12 | super(); 13 | } 14 | 15 | /** A GitletException MSG as its message. */ 16 | GitletException(String msg) { 17 | super(msg); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /testing/samples/Test02.in: -------------------------------------------------------------------------------- 1 | #test rm, status 2 | I setup2.inc 3 | > rm f.txt 4 | <<< 5 | > status 6 | === Branches === 7 | *master 8 | 9 | === Staged Files === 10 | 11 | === Removed Files === 12 | f.txt 13 | 14 | === Modifications Not Staged For Commit === 15 | 16 | === Untracked Files === 17 | <<< 18 | + b.txt b.txt 19 | > add b.txt 20 | <<< 21 | > status 22 | === Branches === 23 | *master 24 | 25 | === Staged Files === 26 | b.txt 27 | 28 | === Removed Files === 29 | f.txt 30 | 31 | === Modifications Not Staged For Commit === 32 | 33 | === Untracked Files === 34 | 35 | <<< -------------------------------------------------------------------------------- /testing/samples/test26-successful-find-orphan.in: -------------------------------------------------------------------------------- 1 | # Test the find command when it finds commits that are no longer 2 | # on any branch. 3 | I setup2.inc 4 | > rm f.txt 5 | <<< 6 | > commit "Remove one file" 7 | <<< 8 | > log 9 | === 10 | ${COMMIT_HEAD} 11 | Remove one file 12 | 13 | === 14 | ${COMMIT_HEAD} 15 | Two files 16 | 17 | === 18 | ${COMMIT_HEAD} 19 | initial commit 20 | 21 | <<<* 22 | # UID of initial version 23 | D UID1 "${3}" 24 | # UID of second version 25 | D UID2 "${2}" 26 | # UID of third version 27 | D UID3 "${1}" 28 | > reset ${2} 29 | <<< 30 | > find "Remove one file" 31 | ${UID3} 32 | <<< -------------------------------------------------------------------------------- /testing/samples/test33-merge-no-conflicts.in: -------------------------------------------------------------------------------- 1 | # Create two branches and merge other into master. 2 | I setup2.inc 3 | > branch other 4 | <<< 5 | + h.txt wug2.txt 6 | > add h.txt 7 | <<< 8 | > rm g.txt 9 | <<< 10 | > commit "Add h.txt and remove g.txt" 11 | <<< 12 | > checkout other 13 | <<< 14 | > rm f.txt 15 | <<< 16 | + k.txt wug3.txt 17 | > add k.txt 18 | <<< 19 | > commit "Add k.txt and remove f.txt" 20 | <<< 21 | > checkout master 22 | <<< 23 | > merge other 24 | <<< 25 | * f.txt 26 | * g.txt 27 | = h.txt wug2.txt 28 | = k.txt wug3.txt 29 | > log 30 | === 31 | ${COMMIT_HEAD} 32 | Merged other into master\. 33 | 34 | ${ARBLINES} 35 | <<<* 36 | 37 | -------------------------------------------------------------------------------- /gitlet/UnitTest.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | import ucb.junit.textui; 4 | import org.junit.Test; 5 | import static org.junit.Assert.*; 6 | 7 | /** The suite of all JUnit tests for the gitlet package. 8 | * @author 9 | */ 10 | public class UnitTest { 11 | 12 | /** Run the JUnit tests in the loa package. Add xxxTest.class entries to 13 | * the arguments of runClasses to run other JUnit tests. */ 14 | public static void main(String[] ignored) { 15 | System.exit(textui.runClasses(UnitTest.class)); 16 | } 17 | 18 | /** A dummy test to avoid complaint. */ 19 | @Test 20 | public void placeholderTest() { 21 | } 22 | 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /testing/samples/prelude1.inc: -------------------------------------------------------------------------------- 1 | # Standard commands and definitions 2 | > init 3 | <<< 4 | D DATE "Date: \w\w\w \w\w\w \d+ \d\d:\d\d:\d\d \d\d\d\d [-+]\d\d\d\d" 5 | # A status log header RE. Captures the commit id in its sole group. 6 | D COMMIT_HEAD "commit ([a-f0-9]+)[ \t]*\n(?:Merge:\s+[0-9a-f]{7}\s+[0-9a-f]{7}[ ]*\n)?${DATE}" 7 | # A full log entry. Captures the entry. Assume logs messages don't contain 8 | # "===" 9 | D COMMIT_LOG "(===[ ]*\ncommit [a-f0-9]+[ ]*\n(?:Merge:\s+[0-9a-f]{7}\s+[0-9a-f]{7}[ ]*\n)?${DATE}[ ]*\n(?:.|\n)*?(?=\Z|\n===))" 10 | # An arbitrary line of text (works even with ?s) 11 | D ARBLINE "[^\n]*(?=\n|\Z)" 12 | # Zero or more arbitrary full lines of text. 13 | D ARBLINES "(?:(?:.|\n)*(?:\n|\Z)|\A|\Z)" -------------------------------------------------------------------------------- /testing/samples/test25-successful-find.in: -------------------------------------------------------------------------------- 1 | # Test the find command when it succeeds. 2 | I setup2.inc 3 | > rm f.txt 4 | <<< 5 | > commit "Remove one file" 6 | <<< 7 | + f.txt notwug.txt 8 | > add f.txt 9 | <<< 10 | > commit "Two files" 11 | <<< 12 | > log 13 | === 14 | ${COMMIT_HEAD} 15 | Two files 16 | 17 | === 18 | ${COMMIT_HEAD} 19 | Remove one file 20 | 21 | === 22 | ${COMMIT_HEAD} 23 | Two files 24 | 25 | === 26 | ${COMMIT_HEAD} 27 | initial commit 28 | 29 | <<<* 30 | # UID of initial version 31 | D UID1 "${4}" 32 | # UID of second version 33 | D UID2 "${3}" 34 | # UID of third version 35 | D UID3 "${2}" 36 | # UID of current version 37 | D UID4 "${1}" 38 | > find "Two files" 39 | (${UID4}\n${UID2}|${UID2}\n${UID4}) 40 | <<<* 41 | > find "initial commit" 42 | ${UID1} 43 | <<< 44 | > find "Remove one file" 45 | ${UID3} 46 | <<< -------------------------------------------------------------------------------- /testing/samples/test36-merge-err.in: -------------------------------------------------------------------------------- 1 | # Check various merge error cases. 2 | I setup2.inc 3 | > branch other 4 | <<< 5 | + h.txt wug2.txt 6 | > add h.txt 7 | <<< 8 | > rm g.txt 9 | <<< 10 | > commit "Add h.txt and remove g.txt" 11 | <<< 12 | > checkout other 13 | <<< 14 | > merge other 15 | Cannot merge a branch with itself. 16 | <<< 17 | > rm f.txt 18 | <<< 19 | + k.txt wug3.txt 20 | > add k.txt 21 | <<< 22 | > commit "Add k.txt and remove f.txt" 23 | <<< 24 | > checkout master 25 | <<< 26 | > merge foobar 27 | A branch with that name does not exist. 28 | <<< 29 | + k.txt wug.txt 30 | > merge other 31 | There is an untracked file in the way; delete it, or add and commit it first. 32 | <<< 33 | - k.txt 34 | + k.txt wug.txt 35 | > add k.txt 36 | <<< 37 | > merge other 38 | You have uncommitted changes. 39 | <<< 40 | > rm k.txt 41 | <<< 42 | - k.txt 43 | -------------------------------------------------------------------------------- /testing/samples/test23-global-log.in: -------------------------------------------------------------------------------- 1 | # Check that global-log prints out commits in master branch. 2 | # FIXME: kludged around problem with regular-expression pattern characters in 3 | # log. 4 | I setup2.inc 5 | # Kludge starts. 6 | D DATE1 "Date: \w\w\w \w\w\w \d+ \d\d:\d\d:\d\d \d\d\d\d" 7 | D COMMIT_LOG "(===[ ]*\ncommit [a-f0-9]+[ ]*\n(?:Merge:\s+[0-9a-f]{7}\s+[0-9a-f]{7}[ ]*\n)?${DATE1}) [-+](\d\d\d\d[ ]*\n(?:.|\n)*?(?=\Z|\n===))" 8 | + h.txt wug.txt 9 | > add h.txt 10 | <<< 11 | > commit "Add h" 12 | <<< 13 | > log 14 | ${COMMIT_LOG} 15 | ${COMMIT_LOG} 16 | ${COMMIT_LOG} 17 | <<<* 18 | D L1 "${1} [-+]${2}" 19 | D L2 "${3} [-+]${4}" 20 | D L3 "${5} [-+]${6}" 21 | > global-log 22 | ${ARBLINES}${L1}?${ARBLINES} 23 | <<<* 24 | > global-log 25 | ${ARBLINES}${L2}?${ARBLINES} 26 | <<<* 27 | > global-log 28 | ${ARBLINES}${L3}${ARBLINES} 29 | <<<* 30 | -------------------------------------------------------------------------------- /testing/samples/test04-prev-checkout.in: -------------------------------------------------------------------------------- 1 | # Check that we can check out a previous version. 2 | > init 3 | <<< 4 | + wug.txt wug.txt 5 | > add wug.txt 6 | <<< 7 | > commit "version 1 of wug.txt" 8 | <<< 9 | + wug.txt notwug.txt 10 | > add wug.txt 11 | <<< 12 | > commit "version 2 of wug.txt" 13 | <<< 14 | = wug.txt notwug.txt 15 | # Each ${HEADER} captures its commit UID. 16 | D UID "[a-f0-9]+" 17 | D HEADER "commit (${UID})" 18 | D DATE "Date: \w\w\w \w\w\w \d+ \d\d:\d\d:\d\d \d\d\d\d [-+]\d\d\d\d" 19 | > log 20 | === 21 | ${HEADER} 22 | ${DATE} 23 | version 2 of wug.txt 24 | 25 | === 26 | ${HEADER} 27 | ${DATE} 28 | version 1 of wug.txt 29 | 30 | === 31 | ${HEADER} 32 | ${DATE} 33 | initial commit 34 | 35 | <<<* 36 | # UID of second version 37 | D UID2 "${1}" 38 | # UID of current version 39 | D UID1 "${2}" 40 | > checkout ${UID1} -- wug.txt 41 | <<< 42 | = wug.txt wug.txt 43 | > checkout ${UID2} -- wug.txt 44 | <<< 45 | = wug.txt notwug.txt 46 | -------------------------------------------------------------------------------- /testing/Makefile: -------------------------------------------------------------------------------- 1 | # This makefile is defined to give you the following targets: 2 | # 3 | # default: Same as check 4 | # check: Run the integration tests. 5 | # clean: Remove all files and directories generated by testing. 6 | # 7 | 8 | SHELL = /bin/bash 9 | 10 | # Flags to Java interpreter: check assertions 11 | JFLAGS = -ea 12 | 13 | # See comment in ../Makefile 14 | PYTHON = python3 15 | 16 | RMAKE = "$(MAKE)" 17 | 18 | TESTER = CLASSPATH="$$(pwd)/..:$(CLASSPATH):;$$(pwd)/..;$(CLASSPATH)" $(PYTHON) tester.py 19 | 20 | TESTER_FLAGS = 21 | 22 | TESTS = samples/*.in *.in 23 | 24 | .PHONY: default check clean std 25 | 26 | # First, and therefore default, target. 27 | default: 28 | $(RMAKE) -C .. 29 | $(RMAKE) PYTHON=$(PYTHON) check 30 | 31 | check: 32 | @echo "Testing application gitlet.Main..." 33 | $(TESTER) $(TESTER_FLAGS) $(TESTS) 34 | 35 | # 'make clean' will clean up stuff you can reconstruct. 36 | clean: 37 | $(RM) -r */*~ *~ __pycache__ 38 | -------------------------------------------------------------------------------- /testing/samples/test34-merge-conflicts.in: -------------------------------------------------------------------------------- 1 | # Create two branches and merge other into master with a merge conflict. 2 | I setup2.inc 3 | > branch other 4 | <<< 5 | + h.txt wug2.txt 6 | > add h.txt 7 | <<< 8 | > rm g.txt 9 | <<< 10 | + f.txt wug2.txt 11 | > add f.txt 12 | <<< 13 | > commit "Add h.txt, remove g.txt, and change f.txt" 14 | <<< 15 | > checkout other 16 | <<< 17 | + f.txt notwug.txt 18 | > add f.txt 19 | <<< 20 | + k.txt wug3.txt 21 | > add k.txt 22 | <<< 23 | > commit "Add k.txt and modify f.txt" 24 | <<< 25 | > checkout master 26 | <<< 27 | > log 28 | === 29 | ${COMMIT_HEAD} 30 | ${ARBLINES} 31 | <<<* 32 | D MASTER_HEAD "${1}" 33 | > merge other 34 | Encountered a merge conflict. 35 | <<< 36 | * g.txt 37 | = h.txt wug2.txt 38 | = k.txt wug3.txt 39 | = f.txt conflict1.txt 40 | > log 41 | ${COMMIT_LOG} 42 | === 43 | commit ${MASTER_HEAD} 44 | ${ARBLINES} 45 | <<<* 46 | > status 47 | === Branches === 48 | \*master 49 | other 50 | 51 | === Staged Files === 52 | 53 | === Removed Files === 54 | 55 | === Modifications Not Staged For Commit === 56 | 57 | === Untracked Files === 58 | 59 | <<<* 60 | -------------------------------------------------------------------------------- /testing/samples/test35-merge-rm-conflicts.in: -------------------------------------------------------------------------------- 1 | #Create two branches and merge other into master with a conflict caused by 2 | # a file changed in one and removed in the other. 3 | I setup2.inc 4 | > branch other 5 | <<< 6 | + h.txt wug2.txt 7 | > add h.txt 8 | <<< 9 | > rm g.txt 10 | <<< 11 | + f.txt wug2.txt 12 | > add f.txt 13 | <<< 14 | > commit "Add h.txt, remove g.txt, and change f.txt" 15 | <<< 16 | > checkout other 17 | <<< 18 | > rm f.txt 19 | <<< 20 | + k.txt wug3.txt 21 | > add k.txt 22 | <<< 23 | > commit "Add k.txt and remove f.txt" 24 | <<< 25 | > checkout master 26 | <<< 27 | > log 28 | === 29 | ${COMMIT_HEAD} 30 | ${ARBLINES} 31 | <<<* 32 | D MASTER_HEAD "${1}" 33 | > merge other 34 | Encountered a merge conflict. 35 | <<< 36 | * g.txt 37 | = h.txt wug2.txt 38 | = k.txt wug3.txt 39 | = f.txt conflict2.txt 40 | > log 41 | ${COMMIT_LOG} 42 | === 43 | commit ${MASTER_HEAD} 44 | ${ARBLINES} 45 | <<<* 46 | > status 47 | === Branches === 48 | \*master 49 | other 50 | 51 | === Staged Files === 52 | 53 | === Removed Files === 54 | 55 | === Modifications Not Staged For Commit === 56 | 57 | === Untracked Files === 58 | 59 | <<<* 60 | 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sharif.XU 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /testing/samples/test43-criss-cross-merge-b.in: -------------------------------------------------------------------------------- 1 | # As for test43-criss-cross-merge.in, but we switch the roles of the parents 2 | # in the first two merges. 3 | # Criss-cross merge. 4 | > init 5 | <<< 6 | > branch given 7 | <<< 8 | > checkout given 9 | <<< 10 | + f.txt wug.txt 11 | > add f.txt 12 | <<< 13 | > commit "Add f.txt containing wug.txt" 14 | <<< 15 | > branch B 16 | <<< 17 | > checkout master 18 | <<< 19 | + f.txt notwug.txt 20 | > add f.txt 21 | <<< 22 | > commit "Add f.txt containing notwug.txt" 23 | <<< 24 | > checkout given 25 | <<< 26 | > merge master 27 | Encountered a merge conflict. 28 | <<< 29 | = f.txt conflict6.txt 30 | + f.txt notwug.txt 31 | > add f.txt 32 | <<< 33 | > commit "Reset f to notwug.txt" 34 | <<< 35 | > rm f.txt 36 | <<< 37 | > commit "given now empty." 38 | <<< 39 | > checkout B 40 | <<< 41 | + g.txt wug2.txt 42 | > add g.txt 43 | <<< 44 | > commit "Added g.txt" 45 | <<< 46 | > checkout master 47 | <<< 48 | > merge B 49 | Encountered a merge conflict. 50 | <<< 51 | = f.txt conflict3.txt 52 | + f.txt wug.txt 53 | > add f.txt 54 | <<< 55 | > commit "Reset f to wug.txt" 56 | <<< 57 | > merge given 58 | Encountered a merge conflict. 59 | <<< 60 | = f.txt conflict5.txt 61 | = g.txt wug2.txt 62 | -------------------------------------------------------------------------------- /testing/samples/test44-bai-merge.in: -------------------------------------------------------------------------------- 1 | # Test case for merge adapted from Haoran Bai 2 | > init 3 | <<< 4 | + A.txt a.txt 5 | + B.txt b.txt 6 | + C.txt c.txt 7 | + D.txt d.txt 8 | + E.txt e.txt 9 | > add A.txt 10 | <<< 11 | > add B.txt 12 | <<< 13 | > add C.txt 14 | <<< 15 | > add D.txt 16 | <<< 17 | > add E.txt 18 | <<< 19 | > commit "msg1" 20 | <<< 21 | = A.txt a.txt 22 | = B.txt b.txt 23 | = C.txt c.txt 24 | = D.txt d.txt 25 | = E.txt e.txt 26 | > branch branch1 27 | <<< 28 | > rm C.txt 29 | <<< 30 | > rm D.txt 31 | <<< 32 | + F.txt notf.txt 33 | + A.txt nota.txt 34 | > add A.txt 35 | <<< 36 | > add F.txt 37 | <<< 38 | > commit "msg2" 39 | <<< 40 | = A.txt nota.txt 41 | = B.txt b.txt 42 | * C.txt 43 | * D.txt 44 | = E.txt e.txt 45 | = F.txt notf.txt 46 | > checkout branch1 47 | <<< 48 | = A.txt a.txt 49 | = B.txt b.txt 50 | = C.txt c.txt 51 | = D.txt d.txt 52 | = E.txt e.txt 53 | * F.txt 54 | > rm C.txt 55 | <<< 56 | > rm E.txt 57 | <<< 58 | + B.txt notb.txt 59 | + G.txt g.txt 60 | > add B.txt 61 | <<< 62 | > add G.txt 63 | <<< 64 | > commit "msg3" 65 | <<< 66 | = A.txt a.txt 67 | = B.txt notb.txt 68 | * C.txt 69 | = D.txt d.txt 70 | * E.txt 71 | * F.txt 72 | = G.txt g.txt 73 | > merge master 74 | <<< 75 | = A.txt nota.txt 76 | = B.txt notb.txt 77 | * C.txt 78 | * D.txt 79 | * E.txt 80 | = F.txt notf.txt 81 | = G.txt g.txt 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is Project #3 for CS61B Spring2020, without skeleton code. **Only for Educational use!** Do not copy even for due days😊, Believe in yourself. You can make it.🦾🦾🦾 2 | 3 | In this project you'll be implementing a version-control system that mimics some of the basic features of the popular system Git. Ours is smaller and simpler, however, so we have named it Gitlet. 4 | 5 | A version-control system is essentially a backup system for related collections of files. The main functionality that Gitlet supports is: 6 | 7 | 1. Saving the contents of entire directories of files. In Gitlet, this is called *committing*, and the saved contents themselves are called *commits*. 8 | 2. Restoring a version of one or more files or entire commits. In Gitlet, this is called *checking out* those files or that commit. 9 | 3. Viewing the history of your backups. In Gitlet, you view this history in something called the *log*. 10 | 4. Maintaining related sequences of commits, called *branches*. 11 | 5. Merging changes made in one branch into another. 12 | 13 | You can find design-document in the same root named gitlet-design.md file. 14 | 15 | Official guidance: 16 | [CS61B Proj3]: http://inst.eecs.berkeley.edu/~cs61b/sp20/materials/proj/proj3/index.html 17 | 18 | **All rights reserved by University of Califorina, Berkeley.** 19 | 20 | -------------------------------------------------------------------------------- /gitlet/DumpObj.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | import java.io.File; 4 | 5 | /** A debugging class whose main program may be invoked as follows: 6 | * java gitlet.DumpObj FILE... 7 | * where each FILE is a file produced by Utils.writeObject (or any file 8 | * containing a serialized object). This will simply read FILE, 9 | * deserialize it, and call the dump method on the resulting Object. 10 | * The object must implement the gitlet.Dumpable interface for this 11 | * to work. For example, you might define your class like this: 12 | * 13 | * import java.io.Serializable; 14 | * import java.util.TreeMap; 15 | * class MyClass implements Serializeable, Dumpable { 16 | * ... 17 | * @Override 18 | * public void dump() { 19 | * System.out.printf("size: %d%nmapping: %s%n", _size, _mapping); 20 | * } 21 | * ... 22 | * int _size; 23 | * TreeMap _mapping = new TreeMap<>(); 24 | * } 25 | * 26 | * As illustrated, your dump method should print useful information from 27 | * objects of your class. 28 | * @author P. N. Hilfinger 29 | */ 30 | public class DumpObj { 31 | 32 | /** Deserialize and apply dump to the contents of each of the files 33 | * in FILES. */ 34 | public static void main(String... files) { 35 | for (String fileName : files) { 36 | Dumpable obj = Utils.readObject(new File(fileName), 37 | Dumpable.class); 38 | obj.dump(); 39 | System.out.println("---"); 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This makefile is defined to give you the following targets: 2 | # 3 | # default: The default target: Compiles the program in package db61b. 4 | # style: Run our style checker on the project source files. Requires that 5 | # the source files compile. 6 | # check: Compiles the db61b package, if needed, and then performs the 7 | # tests described in testing/Makefile. 8 | # clean: Remove regeneratable files (such as .class files) produced by 9 | # other targets and Emacs backup files. 10 | # 11 | # In other words, type 'make' to compile everything; 'make check' to 12 | # compile and test everything, and 'make clean' to clean things up. 13 | # 14 | # You can use this file without understanding most of it, of course, but 15 | # I strongly recommend that you try to figure it out, and where you cannot, 16 | # that you ask questions. The Lab Reader contains documentation. 17 | 18 | # Name of package containing main procedure 19 | PACKAGE = gitlet 20 | 21 | STYLEPROG = style61b 22 | 23 | # The name of the Python 3 program, used in the 'check' target. If your system 24 | # has a different name for this program (such as just "python"), run 25 | # the Makefile with 26 | # make PYTHON=python check 27 | PYTHON = python3 28 | 29 | # Flags to pass to tester.py. 30 | TESTER_FLAGS = 31 | 32 | RMAKE = "$(MAKE)" 33 | 34 | # Targets that don't correspond to files, but are to be treated as commands. 35 | .PHONY: default check integration unit clean style 36 | 37 | default: 38 | $(RMAKE) -C $(PACKAGE) default 39 | 40 | check: integration unit 41 | 42 | integration: default 43 | $(RMAKE) -C testing PYTHON=$(PYTHON) TESTER_FLAGS="$(TESTER_FLAGS)" check 44 | 45 | unit: default 46 | $(RMAKE) -C gitlet unit 47 | 48 | style: 49 | $(RMAKE) -C $(PACKAGE) STYLEPROG=$(STYLEPROG) style 50 | 51 | # 'make clean' will clean up stuff you can reconstruct. 52 | clean: 53 | $(RM) *~ 54 | $(RMAKE) -C $(PACKAGE) clean 55 | $(RMAKE) -C testing clean 56 | 57 | 58 | -------------------------------------------------------------------------------- /testing/samples/test37-reset1.in: -------------------------------------------------------------------------------- 1 | # Check reset command. 2 | I setup2.inc 3 | > branch other 4 | <<< 5 | + h.txt wug2.txt 6 | > add h.txt 7 | <<< 8 | > rm g.txt 9 | <<< 10 | > commit "Add h.txt and remove g.txt" 11 | <<< 12 | > checkout other 13 | <<< 14 | > rm f.txt 15 | <<< 16 | + k.txt wug3.txt 17 | > add k.txt 18 | <<< 19 | > commit "Add k.txt and remove f.txt" 20 | <<< 21 | > log 22 | === 23 | ${COMMIT_HEAD} 24 | Add k.txt and remove f.txt 25 | 26 | === 27 | ${COMMIT_HEAD} 28 | Two files 29 | 30 | === 31 | ${COMMIT_HEAD} 32 | initial commit 33 | 34 | <<<* 35 | D INIT "${3}" 36 | D TWO "${2}" 37 | D OTHER1 "${1}" 38 | 39 | > checkout master 40 | <<< 41 | > log 42 | === 43 | ${COMMIT_HEAD} 44 | Add h.txt and remove g.txt 45 | 46 | === 47 | ${COMMIT_HEAD} 48 | Two files 49 | 50 | === 51 | ${COMMIT_HEAD} 52 | initial commit 53 | <<<* 54 | D MASTER1 "${1}" 55 | + m.txt wug.txt 56 | > add m.txt 57 | <<< 58 | > reset ${TWO} 59 | <<< 60 | > status 61 | === Branches === 62 | \*master 63 | other 64 | 65 | === Staged Files === 66 | 67 | === Removed Files === 68 | 69 | === Modifications Not Staged For Commit === 70 | 71 | === Untracked Files === 72 | (m\.txt\n)?\s* 73 | <<<* 74 | 75 | > log 76 | === 77 | ${COMMIT_HEAD} 78 | Two files 79 | 80 | === 81 | ${COMMIT_HEAD} 82 | initial commit 83 | <<<* 84 | > checkout other 85 | <<< 86 | > log 87 | === 88 | ${COMMIT_HEAD} 89 | Add k.txt and remove f.txt 90 | 91 | === 92 | ${COMMIT_HEAD} 93 | Two files 94 | 95 | === 96 | ${COMMIT_HEAD} 97 | initial commit 98 | 99 | <<<* 100 | > checkout master 101 | <<< 102 | > log 103 | === 104 | ${COMMIT_HEAD} 105 | Two files 106 | 107 | === 108 | ${COMMIT_HEAD} 109 | initial commit 110 | <<<* 111 | > reset ${MASTER1} 112 | <<< 113 | > log 114 | === 115 | ${COMMIT_HEAD} 116 | Add h.txt and remove g.txt 117 | 118 | === 119 | ${COMMIT_HEAD} 120 | Two files 121 | 122 | === 123 | ${COMMIT_HEAD} 124 | initial commit 125 | 126 | <<<* 127 | 128 | -------------------------------------------------------------------------------- /gitlet/Makefile: -------------------------------------------------------------------------------- 1 | # This makefile is defined to give you the following targets: 2 | # 3 | # default: The default target: Compiles $(PROG) and whatever it 4 | # depends on. 5 | # style: Run our style checker on the project source files. Requires that 6 | # the source files compile. 7 | # check: Compile $(PROG), if needed, and then for each file, F.in, in 8 | # directory testing, use F.in as input to "java $(MAIN_CLASS)" and 9 | # compare the output to the contents of the file names F.out. 10 | # Report discrepencies. 11 | # clean: Remove all the .class files produced by java compilation, 12 | # all Emacs backup files, and testing output files. 13 | # 14 | # In other words, type 'gmake' to compile everything; 'gmake check' to 15 | # compile and test everything, and 'gmake clean' to clean things up. 16 | # 17 | # You can use this file without understanding most of it, of course, but 18 | # I strongly recommend that you try to figure it out, and where you cannot, 19 | # that you ask questions. The Lab Reader contains documentation. 20 | 21 | STYLEPROG = style61b 22 | 23 | JFLAGS = -g -Xlint:unchecked -Xlint:deprecation 24 | 25 | CLASSDIR = ../classes 26 | 27 | # See comment in ../Makefile 28 | PYTHON = python3 29 | 30 | RMAKE = "$(MAKE)" 31 | 32 | # A CLASSPATH value that (seems) to work on both Windows and Unix systems. 33 | # To Unix, it looks like ..:$(CLASSPATH):JUNK and to Windows like 34 | # JUNK;..;$(CLASSPATH). 35 | CPATH = "..:$(CLASSPATH):;..;$(CLASSPATH)" 36 | 37 | # All .java files in this directory. 38 | SRCS := $(wildcard *.java) 39 | 40 | .PHONY: default check clean style 41 | 42 | # As a convenience, you can compile a single Java file X.java in this directory 43 | # with 'make X.class' 44 | %.class: %.java 45 | javac $(JFLAGS) -cp $(CPATH) $< 46 | 47 | # First, and therefore default, target. 48 | default: sentinel 49 | 50 | style: default 51 | $(STYLEPROG) $(SRCS) 52 | 53 | check: 54 | $(RMAKE) -C .. PYTHON=$(PYTHON) check 55 | 56 | integration: 57 | $(RMAKE) -C .. PYTHON=$(PYTHON) integration 58 | 59 | unit: default 60 | java -ea -cp $(CPATH) gitlet.UnitTest 61 | 62 | # 'make clean' will clean up stuff you can reconstruct. 63 | clean: 64 | $(RM) *~ *.class sentinel 65 | 66 | ### DEPENDENCIES ### 67 | 68 | sentinel: $(SRCS) 69 | javac $(JFLAGS) -cp $(CPATH) $(SRCS) 70 | touch sentinel 71 | -------------------------------------------------------------------------------- /gitlet/Blob.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | import java.io.File; 4 | import java.io.Serializable; 5 | import java.time.ZonedDateTime; 6 | import java.time.format.DateTimeFormatter; 7 | import java.util.Arrays; 8 | import java.util.Locale; 9 | 10 | /** Class Blob for Gitlet. 11 | * @author Ruize Xu 12 | */ 13 | public class Blob implements Serializable { 14 | /** Name of the modified file. */ 15 | private String _name; 16 | 17 | /** HashID of the blob object. */ 18 | private String _hashID; 19 | 20 | /** Store the content which read from file using the 21 | * Util.readContents() method. */ 22 | private byte[] _contents; 23 | 24 | /** Store the content which read from file using the 25 | * Util.readContentsAsString() method. */ 26 | private String _contentsAsString; 27 | 28 | /** Store the time the blob object is formed. */ 29 | private String _timestamp; 30 | 31 | /** 32 | * Constructor. 33 | * @param name String the name of added file 34 | */ 35 | public Blob(String name) { 36 | File file = new File(name); 37 | _name = name; 38 | _contents = Utils.readContents(file); 39 | _contentsAsString = Utils.readContentsAsString(file); 40 | ZonedDateTime now = java.time.ZonedDateTime.now(); 41 | _timestamp = now.format(DateTimeFormatter.ofPattern 42 | ("EEE MMM d HH:mm:ss yyyy xxxx", Locale.ENGLISH)); 43 | _hashID = createHashId(); 44 | } 45 | 46 | /** 47 | * Create the unique hashID for the Blob object. 48 | * @return String the hashID generate by SHA1 algorithm 49 | */ 50 | private String createHashId() { 51 | String contentToString = Arrays.toString(_contents); 52 | String insideBlob = _name + contentToString; 53 | return Utils.sha1(insideBlob); 54 | } 55 | 56 | /** Return the Blob name. */ 57 | public String getName() { 58 | return _name; 59 | } 60 | 61 | /** Return the Blob hashID. */ 62 | public String getHashID() { 63 | return _hashID; 64 | } 65 | 66 | /** Return the Blob content as a byte array. */ 67 | public byte[] getContents() { 68 | return _contents; 69 | } 70 | 71 | /** Return the Blob content as String type. */ 72 | public String getContentsAsString() { 73 | return _contentsAsString; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | sentinel 3 | __pycache__ 4 | 5 | # Created by https://www.gitignore.io/api/java,eclipse,intellij,emacs,vim 6 | 7 | ### Java ### 8 | *.class 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | 18 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 19 | hs_err_pid* 20 | 21 | 22 | ### Eclipse ### 23 | *.pydevproject 24 | .metadata 25 | .gradle 26 | bin/ 27 | tmp/ 28 | *.tmp 29 | *.bak 30 | *.swp 31 | *~.nib 32 | local.properties 33 | .settings/ 34 | .loadpath 35 | 36 | # Eclipse Core 37 | .project 38 | 39 | # External tool builders 40 | .externalToolBuilders/ 41 | 42 | # Locally stored "Eclipse launch configurations" 43 | *.launch 44 | 45 | # CDT-specific 46 | .cproject 47 | 48 | # JDT-specific (Eclipse Java Development Tools) 49 | .classpath 50 | 51 | # Java annotation processor (APT) 52 | .factorypath 53 | 54 | # PDT-specific 55 | .buildpath 56 | 57 | # sbteclipse plugin 58 | .target 59 | 60 | # TeXlipse plugin 61 | .texlipse 62 | 63 | 64 | ### Intellij ### 65 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 66 | 67 | *.iml 68 | 69 | ## Directory-based project format: 70 | .idea/ 71 | # if you remove the above rule, at least ignore the following: 72 | 73 | # User-specific stuff: 74 | # .idea/workspace.xml 75 | # .idea/tasks.xml 76 | # .idea/dictionaries 77 | 78 | # Sensitive or high-churn files: 79 | # .idea/dataSources.ids 80 | # .idea/dataSources.xml 81 | # .idea/sqlDataSources.xml 82 | # .idea/dynamic.xml 83 | # .idea/uiDesigner.xml 84 | 85 | # Gradle: 86 | # .idea/gradle.xml 87 | # .idea/libraries 88 | 89 | # Mongo Explorer plugin: 90 | # .idea/mongoSettings.xml 91 | 92 | ## File-based project format: 93 | *.ipr 94 | *.iws 95 | 96 | ## Plugin-specific files: 97 | 98 | # IntelliJ 99 | /out/ 100 | 101 | # mpeltonen/sbt-idea plugin 102 | .idea_modules/ 103 | 104 | # JIRA plugin 105 | atlassian-ide-plugin.xml 106 | 107 | # Crashlytics plugin (for Android Studio and IntelliJ) 108 | com_crashlytics_export_strings.xml 109 | crashlytics.properties 110 | crashlytics-build.properties 111 | 112 | 113 | ### Emacs ### 114 | # -*- mode: gitignore; -*- 115 | *~ 116 | \#*\# 117 | /.emacs.desktop 118 | /.emacs.desktop.lock 119 | *.elc 120 | auto-save-list 121 | tramp 122 | .\#* 123 | 124 | # Org-mode 125 | .org-id-locations 126 | *_archive 127 | 128 | # flymake-mode 129 | *_flymake.* 130 | 131 | # eshell files 132 | /eshell/history 133 | /eshell/lastdir 134 | 135 | # elpa packages 136 | /elpa/ 137 | 138 | # reftex files 139 | *.rel 140 | 141 | # AUCTeX auto folder 142 | /auto/ 143 | 144 | # cask packages 145 | .cask/ 146 | 147 | 148 | ### Vim ### 149 | [._]*.s[a-w][a-z] 150 | [._]s[a-w][a-z] 151 | *.un~ 152 | Session.vim 153 | .netrwhist 154 | *~ 155 | 156 | 157 | -------------------------------------------------------------------------------- /gitlet/Commit.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | import java.io.Serializable; 4 | import java.time.ZonedDateTime; 5 | import java.time.format.DateTimeFormatter; 6 | import java.util.Arrays; 7 | import java.util.HashMap; 8 | import java.util.Locale; 9 | 10 | /** Class Commit for Gitlet. 11 | * @author Ruize Xu 12 | */ 13 | public class Commit implements Serializable { 14 | 15 | /** The input message for the commit command. */ 16 | private String _message; 17 | 18 | /** The time when the commit is generated. */ 19 | private String _timestamp; 20 | 21 | /** The current branch for this commit. */ 22 | private String _branch; 23 | 24 | /** The unique hash id for each commit. */ 25 | private String _uid; 26 | 27 | /** An array of Hashes of parents. */ 28 | private String[] _parentid; 29 | 30 | /** 31 | * A list of strings of hashes of Blobs that are being tracked. 32 | * Key is Blob's hashID, and value is the corresponding Blob object. 33 | */ 34 | private HashMap _blobs = new HashMap<>(); 35 | 36 | /** return message. */ 37 | public String getMessage() { 38 | return _message; 39 | } 40 | 41 | /** return timestamp. */ 42 | public String getTimestamp() { 43 | return _timestamp; 44 | } 45 | 46 | /** return branch of this commit. */ 47 | public String getBranchName() { 48 | return _branch; 49 | } 50 | 51 | /** return uid. */ 52 | public String getUid() { 53 | return _uid; 54 | } 55 | 56 | /** return the prarentid of current commit. */ 57 | public String[] getParentid() { 58 | return _parentid; 59 | } 60 | 61 | /** return the tracked blobs of current Commit. */ 62 | public HashMap getBlobs() { 63 | return _blobs; 64 | } 65 | 66 | /** 67 | * Return the first parentID. 68 | * @return String parentID 69 | */ 70 | public String getParentID() { 71 | if (_parentid != null) { 72 | return _parentid[0]; 73 | } 74 | return null; 75 | } 76 | 77 | /** 78 | * Return the String Array of all parentID. 79 | * @return String Array 80 | */ 81 | public String[] getAllParentID() { 82 | return _parentid; 83 | } 84 | 85 | /** Default constructor. */ 86 | public Commit() { 87 | 88 | } 89 | 90 | /** 91 | * Commit constructor called by init command. 92 | * @param message String the init message 93 | */ 94 | public Commit(String message) { 95 | _message = message; 96 | _timestamp = "Thu Jan 1 00:00:00 1970 +0000"; 97 | _uid = generateHash(); 98 | _branch = "master"; 99 | _blobs = null; 100 | _parentid = null; 101 | } 102 | 103 | /** 104 | * The Commit constructor. 105 | * @param message String the input message of commit command 106 | * @param parentid String array contains all parent hashID 107 | * @param branch String the current branch of the commit 108 | * @param blobs the tracked blobs of the commit 109 | */ 110 | public Commit(String message, String[] parentid, String branch, 111 | HashMap blobs) { 112 | _message = message; 113 | _branch = branch; 114 | _parentid = parentid; 115 | _blobs = blobs; 116 | ZonedDateTime now = ZonedDateTime.now(); 117 | _timestamp = now.format(DateTimeFormatter.ofPattern 118 | ("EEE MMM d HH:mm:ss yyyy xxxx", Locale.ENGLISH)); 119 | _uid = generateHash(); 120 | } 121 | 122 | /** 123 | * Hash generator using the method in Utils.sha1() method. 124 | * @return String hashID or we call it uid in Commit class 125 | */ 126 | private String generateHash() { 127 | String blobToString; 128 | String parentToString = Arrays.toString(_parentid); 129 | if (_blobs == null) { 130 | blobToString = ""; 131 | } else { 132 | blobToString = _blobs.toString(); 133 | } 134 | String contentOfHash = _message + _timestamp + _branch 135 | + parentToString + blobToString; 136 | return Utils.sha1(contentOfHash); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /gitlet/Main.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | import java.io.File; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | 7 | /** Driver class for Gitlet, the tiny stupid version-control system. 8 | * @author Ruize Xu 9 | */ 10 | public class Main { 11 | 12 | /** Usage: java gitlet.Main ARGS, where ARGS contains 13 | * .... */ 14 | public static void main(String... args) { 15 | try { 16 | if (args.length == 0) { 17 | Utils.message("Please enter a command."); 18 | throw new GitletException(); 19 | } 20 | ArrayList input = new ArrayList<>(Arrays.asList(args)); 21 | Repo repo = null; 22 | File tmpDir = new File(_cwdString + "/.gitlet"); 23 | if (tmpDir.exists()) { 24 | File mr = new File(".gitlet/repo"); 25 | if (mr.exists()) { 26 | repo = Utils.readObject(mr, Repo.class); 27 | } 28 | } 29 | if (input.get(0).equals("init")) { 30 | if (!tmpDir.exists()) { 31 | repo = new Repo(); 32 | File mr = new File(".gitlet/repo"); 33 | Utils.writeObject(mr, repo); 34 | } else { 35 | System.out.println("A Gitlet version-control " 36 | + "system already exists in the " 37 | + "current directory"); 38 | System.exit(0); 39 | } 40 | } 41 | if (validCommand(args[0])) { 42 | if (!tmpDir.exists()) { 43 | Utils.message("Not in an initialized Gitlet directory."); 44 | System.exit(0); 45 | } 46 | if (input.get(0).equals("checkout")) { 47 | if (input.size() == 2) { 48 | repo.checkout(input.get(1)); 49 | } else { 50 | input.remove(0); 51 | repo.checkout(input); 52 | } 53 | } else if (input.size() == 1) { 54 | emptyOperandCommand(input, repo); 55 | } else if (input.size() == 2) { 56 | oneOperandCommand(input, repo); 57 | } 58 | Utils.writeObject(new File(".gitlet/repo"), repo); 59 | } else { 60 | Utils.message(" No command with that name exists."); 61 | throw new GitletException(); 62 | } 63 | } catch (GitletException e) { 64 | System.exit(0); 65 | } 66 | } 67 | 68 | /** 69 | * Call the method with no input in Class Repo. 70 | * @param in Arraylist Input Args 71 | * @param repo Repo the main operation class 72 | */ 73 | private static void emptyOperandCommand(ArrayList in, Repo repo) { 74 | String command = in.remove(0); 75 | switch (command) { 76 | case "log": 77 | repo.log(); 78 | break; 79 | case "status": 80 | repo.status(); 81 | break; 82 | case "global-log": 83 | repo.globalLog(); 84 | break; 85 | default: 86 | } 87 | } 88 | 89 | /** 90 | * Call the method with one operand in Class Repo. 91 | * @param in Arraylist Input Args 92 | * @param repo Repo the main operation class 93 | */ 94 | private static void oneOperandCommand(ArrayList in, Repo repo) { 95 | String command = in.get(0); 96 | String operand = in.get(1); 97 | switch (command) { 98 | case "add": 99 | repo.add(operand); 100 | break; 101 | case "commit": 102 | repo.commit(operand); 103 | break; 104 | case "rm": 105 | repo.rm(operand); 106 | break; 107 | case "branch": 108 | repo.branch(operand); 109 | break; 110 | case "rm-branch": 111 | repo.rmbranch(operand); 112 | break; 113 | case "reset": 114 | repo.reset(operand); 115 | break; 116 | case "find": 117 | repo.find(operand); 118 | break; 119 | case "merge": 120 | repo.merge(operand); 121 | break; 122 | default: 123 | 124 | } 125 | } 126 | 127 | /** Takes in a string ARG word, will return whether or not 128 | * it is a valid command. */ 129 | private static boolean validCommand(String arg) { 130 | for (String command: _vaildCommands) { 131 | if (arg.equals(command)) { 132 | return true; 133 | } 134 | } 135 | return false; 136 | } 137 | 138 | /** Array of possible valid commands. */ 139 | private static String[] _vaildCommands = new String[] {"init", "add", 140 | "commit", "rm", "log", "global-log", "find", "status", "checkout", 141 | "branch", "rm-branch", "reset", "merge"}; 142 | 143 | /** 144 | * The current working directory, File type. 145 | */ 146 | private static File _cwd = new File(System.getProperty("user.dir")); 147 | 148 | /** 149 | * The current working directory, String type. 150 | */ 151 | private static String _cwdString = System.getProperty("user.dir"); 152 | 153 | } 154 | -------------------------------------------------------------------------------- /gitlet-design.md: -------------------------------------------------------------------------------- 1 | # Gitlet Design Document 2 | 3 | **Author**: Ruize Xu 4 | 5 | ## Classes and Data Structures 6 | ### Commit 7 | The class used to store the content of each commit command, 8 | include commit message, timestamp, parent commit and so on. 9 | #### Instance 10 | 1. String _message: Store the input message of the commit command. 11 | 12 | 2. String _timestamp: Store the current time when the new commit object was created. If current commit object does not have a parent commit, 13 | _timeStamp will be initialize as "00:00:00 UTC, Thursday, 1 January 1970". 14 | 15 | 3. HashMap\ _blobs: A list of strings of hashes of Blobs that are being tracked. 16 | 17 | 4. String _uid: The unique hash code 18 | 19 | 5. String[] _parentid: The array contains all the parent commit's hash code. 20 | 21 | ### Blob 22 | The class represent the blob object which record each modification of corresponding file. 23 | #### Instance 24 | 1. String _name: Name of the modified file. 25 | 26 | 2. String _hashID: hashID of the blob object. 27 | 28 | 3. byte[] _contents: Store the content which read from file using the Utils.readContents() method. 29 | 30 | 4. String _contentsAsString: Store the content which read from file using the Util.readContentsAsString() method. 31 | 32 | 5. String _timestamp: Store the time the blob object is formed. 33 | 34 | ### Repo 35 | The class is the major part of the whole gitlet, including all command implementation, and the staging area, the current branch, the structure of the whole gitlet system. 36 | 37 | #### Instance 38 | 39 | 1. final File cwd: The current working directory, File type. 40 | 41 | 2. final String cwdString: The current working directory, String type, mainly used for concat new directory according to cwd. 42 | 43 | 3. HashMap _branches: The structure is used to store the head of each branch, key for branchName, and corresponding value is uid of the exact commit of this branch. 44 | 45 | 4. String _head: The head pointer that corresponds to the branch that actually will be pointing at the commit that we want. 46 | 47 | 5. HashMap _stagingArea: Staging area used to store the blob object, key is file name, value is the Blob object, help us identify whether the file is changed. 48 | 49 | 6. ArrayList _removedFiles: Removed files are like the opposite of the Staging Area, these are files that WERE tracked before, and now, for the next commit, they're not going to be added. 50 | 51 | 7. HashMap cCommits: Global variable stores the data used for resurion in dfsForSplitCommit() method. 52 | 53 | 8. HashMap gCommits: Global variable stores the data used for resurion in dfsForSplitCommit() method. 54 | 55 | ### Main 56 | Driver class for Gitlet, the tiny stupid version-control system. 57 | 58 | #### Instance 59 | 60 | 1. String[] _vaildCommands: Array of possible valid commands. 61 | 62 | 2. File _cwd: The current working directory, File type. 63 | 64 | 3. String _cwdString: The current working directory, String type. 65 | 66 | ## Algorithms 67 | ### Commit class 68 | 1. Commit(): Default constructor. 69 | 70 | 2. Commit(String message): Commit constructor called by init command. 71 | 72 | 3. Commit(String message, String[] parentid, String branch, HashMap blobs): The general Commit constructor, called by the commit command. 73 | 74 | 4. private String generateHash(): Hash generator using the method in Utils.sha1() method. 75 | 76 | 77 | 78 | ### Blob class 79 | 80 | 5. Blob(String name): generate a Blob object according to the file name. 81 | 82 | 6. private String createHashId(): Create the unique hashID for the Blob object. Using the SHA1() algorithm. 83 | 84 | 7. public String getName(): Return the blob name. 85 | 86 | 8. public String getHashID(): Return the blob hashi]ID. 87 | 88 | 9. public byte[] getContents(): Return the Blob content as a byte array. 89 | 90 | 10. public String getContentsAsString(): Return the Blob content as String type. 91 | 92 | ### Repo class 93 | 1. Repo(): Constructor. 94 | 95 | 2. public void add(String filename): The add operation. 96 | 97 | 3. public void commit(String msg): The commit operation. Take in the message with commit command. 98 | 99 | 4. public void commit(String msg, String[] parents): The commit operation for merge. 100 | 101 | 5. public void log(): The log operation. 102 | 103 | 6. public void globalLog(): The global-log operation. 104 | 105 | 7. public void status(): The status operation. 106 | 107 | 8. public void stateDetect(String commitHash, ArrayList modify, ArrayList untrack, ArrayList delete): StateDetect function is to detect state information. Help for status in Modifications Not Staged For Commit & Untracked Files, need fix. 108 | 109 | 9. public void checkout(ArrayList args): The checkout opertion, takes in a Arraylist ARGS. 110 | 111 | 10. public void checkout(String branchName): This is the third use case for checkout. It takes in a branchName. 112 | 113 | 11. public void rm(String fileName): The remove opertion. Take in the name of file you want to delete. Using the Utils.restrictedDelete() method. 114 | 115 | 12. public void branch(String branchName): Create new branch named branchName. But do not change current branch head to the newly created branch. 116 | 117 | 13. public void rmbranch(String branchName): The remove branch operation. 118 | 119 | 14. public void reset(String uid): The reset operation. Take in the commit uid you want tio reset to, and refresh all files in the working directory according to the tracked blobs in the Commit object. 120 | 121 | 15. public void find(String message): The find operation. Find the corresponding commit with the definite message. And print the Commit uid in Terminal. 122 | 123 | 16. public void merge(String branchName): The merge operation. 124 | 125 | 17. private void mergeForSplit(String branchName, 126 | HashMap splitBlobs, HashMap currentBlobs, HashMap givenBlobs): Helper function for merge in first step, traverse the file in split commit, then merge it. 127 | 128 | 18. private void mergeForGiven(String branchName, 129 | HashMap splitBlobs, HashMap currentBlobs, HashMap givenBlobs): Helper function for merge in second step, traverse the file in split commit, then merge it. 130 | 131 | 19. private boolean isBlobInHashMap(String blobname, HashMap blobs): Helper function for check whether the exact blob with the input blobName in the HashMap blobs, return true is the blob in the HashMap, otherwise false. 132 | 133 | 20. private void checkoutFile(String branchName, HashMap blobs, String blobName): Calling the checkout method for merge the file between two branches. 134 | 135 | 21. private void mergeConflict(String branchName, String fileName): This method used to handle the conflict situation when merge two branches. 136 | 137 | 22. boolean isModified(String fileName, HashMap h, HashMap i): Helper Function for check whether the exact file has been changed from commit H to commit I. Return a boolean if the file with name F has been modified from commit H to commit I. 138 | 139 | 23. private String splitPoint(String currentBranch, String givenBranch): Takes in two branch names, BRANCH1 and BRANCH2. Returns the SHA ID of the common ancestor commit. 140 | 141 | 24. private void dfsForSplitCommit(Commit head, int dist, String branch): Helper function for find the nearest SplitPoint using the DFS. 142 | 143 | 25. private void checkForUntracked(File dir): This function takes in the present working directory PWD and will determine if there are untracked files that mean that this checkout or Merge operation can't continue. 144 | 145 | 26. public Commit uidToCommit(String uid): his method is used to find the corresponding Commit object according to the unique uid it contains. Return Commit object read from file. 146 | 147 | 27. private String shortToLong(String id): Takes in a shortened String ID and returns a String of the full length ID. Return The full size uid of the found Commit. 148 | 149 | 28. public String getHead(): Return the head commit's uid of current branch. 150 | 151 | ### Main class 152 | 1. public static void main(String... args): Usage: java gitlet.Main ARGS, where ARGS contains .... 153 | 154 | 2. private static void emptyOperandCommand(ArrayList in, Repo repo): Call the method with no input in Class Repo. 155 | 156 | 3. private static void oneOperandCommand(ArrayList in, Repo repo): Call the method with one operand in Class Repo. 157 | 158 | 4. private static boolean validCommand(String arg): Takes in a string ARG word, will return whether or not it is a valid command. 159 | 160 | ## Persistence 161 | In order to use the Gitlet system with separate commands like java gitlit.main \ \ --optional, we should make sure the whole system is persistant. 162 | 163 | As we call the main method in Main class every execution. We can write the file into the directory ".gitlet/repo" each time the main function near to the end point. The repo file actually is the Repo object recordingt the stage area, the untracked file and all other properties the gitlet system should content. In this way, we can make sure the gitlet system remains consistent for all future calls. 164 | 165 | Except for the repo file, there are two more directories inside the .gitlet directory. The commits folder used to store all the commits since the initial commit each time we call the commit function in the Repo class if there are new changes in our current working directory. And the staging folder used to store all the blob file, according to the hashid of Blob object. Each time we execute add \ successfully, Repo will generate a blob if the content of the file has changed since last commit. 166 | 167 | 168 | -------------------------------------------------------------------------------- /gitlet/Utils.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | import java.io.BufferedOutputStream; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.FilenameFilter; 8 | import java.io.IOException; 9 | import java.io.ObjectInputStream; 10 | import java.io.ObjectOutputStream; 11 | import java.io.Serializable; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.nio.file.Paths; 15 | import java.nio.charset.StandardCharsets; 16 | import java.security.MessageDigest; 17 | import java.security.NoSuchAlgorithmException; 18 | import java.util.Arrays; 19 | import java.util.Formatter; 20 | import java.util.List; 21 | 22 | 23 | /** Assorted utilities. 24 | * @author P. N. Hilfinger 25 | */ 26 | class Utils { 27 | 28 | /* SHA-1 HASH VALUES. */ 29 | 30 | /** The length of a complete SHA-1 UID as a hexadecimal numeral. */ 31 | static final int UID_LENGTH = 40; 32 | 33 | /** Returns the SHA-1 hash of the concatenation of VALS, which may 34 | * be any mixture of byte arrays and Strings. */ 35 | static String sha1(Object... vals) { 36 | try { 37 | MessageDigest md = MessageDigest.getInstance("SHA-1"); 38 | for (Object val : vals) { 39 | if (val instanceof byte[]) { 40 | md.update((byte[]) val); 41 | } else if (val instanceof String) { 42 | md.update(((String) val).getBytes(StandardCharsets.UTF_8)); 43 | } else { 44 | throw new IllegalArgumentException("improper type to sha1"); 45 | } 46 | } 47 | Formatter result = new Formatter(); 48 | for (byte b : md.digest()) { 49 | result.format("%02x", b); 50 | } 51 | return result.toString(); 52 | } catch (NoSuchAlgorithmException excp) { 53 | throw new IllegalArgumentException("System does not support SHA-1"); 54 | } 55 | } 56 | 57 | /** Returns the SHA-1 hash of the concatenation of the strings in 58 | * VALS. */ 59 | static String sha1(List vals) { 60 | return sha1(vals.toArray(new Object[vals.size()])); 61 | } 62 | 63 | /* FILE DELETION */ 64 | 65 | /** Deletes FILE if it exists and is not a directory. Returns true 66 | * if FILE was deleted, and false otherwise. Refuses to delete FILE 67 | * and throws IllegalArgumentException unless the directory designated by 68 | * FILE also contains a directory named .gitlet. */ 69 | static boolean restrictedDelete(File file) { 70 | if (!(new File(file.getParentFile(), ".gitlet")).isDirectory()) { 71 | throw new IllegalArgumentException("not .gitlet working directory"); 72 | } 73 | if (!file.isDirectory()) { 74 | return file.delete(); 75 | } else { 76 | return false; 77 | } 78 | } 79 | 80 | /** Deletes the file named FILE if it exists and is not a directory. 81 | * Returns true if FILE was deleted, and false otherwise. Refuses 82 | * to delete FILE and throws IllegalArgumentException unless the 83 | * directory designated by FILE also contains a directory named .gitlet. */ 84 | static boolean restrictedDelete(String file) { 85 | return restrictedDelete(new File(file)); 86 | } 87 | 88 | /* READING AND WRITING FILE CONTENTS */ 89 | 90 | /** Return the entire contents of FILE as a byte array. FILE must 91 | * be a normal file. Throws IllegalArgumentException 92 | * in case of problems. */ 93 | static byte[] readContents(File file) { 94 | if (!file.isFile()) { 95 | throw new IllegalArgumentException("must be a normal file"); 96 | } 97 | try { 98 | return Files.readAllBytes(file.toPath()); 99 | } catch (IOException excp) { 100 | throw new IllegalArgumentException(excp.getMessage()); 101 | } 102 | } 103 | 104 | /** Return the entire contents of FILE as a String. FILE must 105 | * be a normal file. Throws IllegalArgumentException 106 | * in case of problems. */ 107 | static String readContentsAsString(File file) { 108 | return new String(readContents(file), StandardCharsets.UTF_8); 109 | } 110 | 111 | /** Write the result of concatenating the bytes in CONTENTS to FILE, 112 | * creating or overwriting it as needed. Each object in CONTENTS may be 113 | * either a String or a byte array. Throws IllegalArgumentException 114 | * in case of problems. */ 115 | static void writeContents(File file, Object... contents) { 116 | try { 117 | if (file.isDirectory()) { 118 | throw 119 | new IllegalArgumentException("cannot overwrite directory"); 120 | } 121 | BufferedOutputStream str = 122 | new BufferedOutputStream(Files.newOutputStream(file.toPath())); 123 | for (Object obj : contents) { 124 | if (obj instanceof byte[]) { 125 | str.write((byte[]) obj); 126 | } else { 127 | str.write(((String) obj).getBytes(StandardCharsets.UTF_8)); 128 | } 129 | } 130 | str.close(); 131 | } catch (IOException | ClassCastException excp) { 132 | throw new IllegalArgumentException(excp.getMessage()); 133 | } 134 | } 135 | 136 | /** Return an object of type T read from FILE, casting it to EXPECTEDCLASS. 137 | * Throws IllegalArgumentException in case of problems. */ 138 | static T readObject(File file, 139 | Class expectedClass) { 140 | try { 141 | ObjectInputStream in = 142 | new ObjectInputStream(new FileInputStream(file)); 143 | T result = expectedClass.cast(in.readObject()); 144 | in.close(); 145 | return result; 146 | } catch (IOException | ClassCastException 147 | | ClassNotFoundException excp) { 148 | throw new IllegalArgumentException(excp.getMessage()); 149 | } 150 | } 151 | 152 | /** Write OBJ to FILE. */ 153 | static void writeObject(File file, Serializable obj) { 154 | writeContents(file, serialize(obj)); 155 | } 156 | 157 | /* DIRECTORIES */ 158 | 159 | /** Filter out all but plain files. */ 160 | private static final FilenameFilter PLAIN_FILES = 161 | new FilenameFilter() { 162 | @Override 163 | public boolean accept(File dir, String name) { 164 | return new File(dir, name).isFile(); 165 | } 166 | }; 167 | 168 | /** Returns a list of the names of all plain files in the directory DIR, in 169 | * lexicographic order as Java Strings. Returns null if DIR does 170 | * not denote a directory. */ 171 | static List plainFilenamesIn(File dir) { 172 | String[] files = dir.list(PLAIN_FILES); 173 | if (files == null) { 174 | return null; 175 | } else { 176 | Arrays.sort(files); 177 | return Arrays.asList(files); 178 | } 179 | } 180 | 181 | /** Returns a list of the names of all plain files in the directory DIR, in 182 | * lexicographic order as Java Strings. Returns null if DIR does 183 | * not denote a directory. */ 184 | static List plainFilenamesIn(String dir) { 185 | return plainFilenamesIn(new File(dir)); 186 | } 187 | 188 | /* OTHER FILE UTILITIES */ 189 | 190 | /** Return the concatentation of FIRST and OTHERS into a File designator, 191 | * analogous to the {@link java.nio.file.Paths .#get(String, String[])} 192 | * method. */ 193 | static File join(String first, String... others) { 194 | return Paths.get(first, others).toFile(); 195 | } 196 | 197 | /** Return the concatentation of FIRST and OTHERS into a File designator, 198 | * analogous to the {@link java.nio.file.Paths .#get(String, String[])} 199 | * method. */ 200 | static File join(File first, String... others) { 201 | return Paths.get(first.getPath(), others).toFile(); 202 | } 203 | 204 | 205 | /* SERIALIZATION UTILITIES */ 206 | 207 | /** Returns a byte array containing the serialized contents of OBJ. */ 208 | static byte[] serialize(Serializable obj) { 209 | try { 210 | ByteArrayOutputStream stream = new ByteArrayOutputStream(); 211 | ObjectOutputStream objectStream = new ObjectOutputStream(stream); 212 | objectStream.writeObject(obj); 213 | objectStream.close(); 214 | return stream.toByteArray(); 215 | } catch (IOException excp) { 216 | throw error("Internal error serializing commit."); 217 | } 218 | } 219 | 220 | 221 | 222 | /* MESSAGES AND ERROR REPORTING */ 223 | 224 | /** Return a GitletException whose message is composed from MSG and ARGS as 225 | * for the String.format method. */ 226 | static GitletException error(String msg, Object... args) { 227 | return new GitletException(String.format(msg, args)); 228 | } 229 | 230 | /** Print a message composed from MSG and ARGS as for the String.format 231 | * method, followed by a newline. */ 232 | static void message(String msg, Object... args) { 233 | System.out.printf(msg, args); 234 | System.out.println(); 235 | } 236 | 237 | /** Deserializes file from PATH return OBJECT. **/ 238 | static Object deserialize(Path path) { 239 | File tmp = path.toFile(); 240 | Object obj = null; 241 | try { 242 | ObjectInputStream inp = new ObjectInputStream( 243 | new FileInputStream(tmp)); 244 | obj = inp.readObject(); 245 | inp.close(); 246 | } catch (IOException | ClassNotFoundException excp) { 247 | excp.printStackTrace(); 248 | } 249 | return obj; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /testing/tester.py: -------------------------------------------------------------------------------- 1 | import sys, re 2 | from subprocess import \ 3 | check_output, PIPE, STDOUT, DEVNULL, CalledProcessError, TimeoutExpired 4 | from os.path import abspath, basename, dirname, exists, join, splitext 5 | from getopt import getopt, GetoptError 6 | from os import chdir, environ, getcwd, mkdir, remove, access, W_OK 7 | from shutil import copyfile, rmtree 8 | from math import log 9 | 10 | SHORT_USAGE = """\ 11 | Usage: python3 tester.py OPTIONS TEST.in ... 12 | 13 | OPTIONS may include 14 | --show=N Show details on up to N tests. 15 | --show=all Show details on all tests. 16 | --keep Keep test directories 17 | --progdir=DIR Directory or JAR files containing gitlet application 18 | --timeout=SEC Default number of seconds allowed to each execution 19 | of gitlet. 20 | --src=SRC Use SRC instead of "src" as the subdirectory containing 21 | files referenced by + and =. 22 | --tolerance=N Set the maximum allowed edit distance between program 23 | output and expected output to N (default 3). 24 | --verbose Print extra information about execution. 25 | """ 26 | 27 | USAGE = SHORT_USAGE + """\ 28 | 29 | For each TEST.in, change to an empty directory, and execute the instructions 30 | in TEST.in. Before executing an instruction, first replace any occurrence 31 | of ${VAR} with the current definition of VAR (see the D command below). 32 | Replace any occurrence of ${N} for non-negative decimal numeral N with 33 | the value of the Nth captured group in the last ">" command's expected 34 | output lines. Undefined if the last ">" command did not end in "<<<*", 35 | or did not have the indicated group. N=0 indicates the entire matched string. 36 | 37 | The instructions each have one of the following forms: 38 | 39 | # ... A comment, producing no effect. 40 | I FILE Include. Replace this statement with the contents of FILE, 41 | interpreted relative to the directory containing the .in file. 42 | C DIR Create, if necessary, and switch to a subdirectory named DIR under 43 | the main directory for this test. If DIR is missing, changes 44 | back to the default directory. This command is principally 45 | intended to let you set up remote repositories. 46 | T N Set the timeout for gitlet commands in the rest of this test to N 47 | seconds. 48 | + NAME F 49 | Copy the contents of src/F into a file named NAME. 50 | - NAME 51 | Delete the file named NAME. 52 | > COMMAND OPERANDS 53 | LINE1 54 | LINE2 55 | ... 56 | <<< 57 | Run gitlet.Main with COMMAND ARGUMENTS as its parameters. Compare 58 | its output with LINE1, LINE2, etc., reporting an error if there is 59 | "sufficient" discrepency. The <<< delimiter may be followed by 60 | an asterisk (*), in which case, the preceding lines are treated as 61 | Python regular expressions and matched accordingly. The directory 62 | or JAR file containing the gitlet.Main program is assumed to be 63 | in directory DIR specifed by --progdir (default is ..). 64 | = NAME F 65 | Check that the file named NAME is identical to src/F, and report an 66 | error if not. 67 | * NAME 68 | Check that the file NAME does not exist, and report an error if it 69 | does. 70 | E NAME 71 | Check that file or directory NAME exists, and report an error if it 72 | does not. 73 | D VAR "VALUE" 74 | Defines the variable VAR to have the literal value VALUE. VALUE is 75 | taken to be a raw Python string (as in r"VALUE"). Substitutions are 76 | first applied to VALUE. 77 | 78 | For each TEST.in, reports at most one error. Without the --show option, 79 | simply indicates tests passed and failed. If N is postive, also prints details 80 | of the first N failing tests. With --show=all, shows details of all failing 81 | tests. With --keep, keeps the directories created for the tests (with names 82 | TEST.dir). 83 | 84 | When finished, reports number of tests passed and failed, and the number of 85 | faulty TEST.in files.""" 86 | 87 | GITLET_COMMAND = "java -ea gitlet.Main" 88 | TIMEOUT = 10 89 | 90 | def Usage(): 91 | print(SHORT_USAGE, file=sys.stderr) 92 | sys.exit(1) 93 | 94 | Mat = None 95 | def Match(patn, s): 96 | global Mat 97 | Mat = re.match(patn, s) 98 | return Mat 99 | 100 | def Group(n): 101 | return Mat.group(n) 102 | 103 | def contents(filename): 104 | try: 105 | with open(filename) as inp: 106 | return inp.read() 107 | except FileNotFoundError: 108 | return None 109 | 110 | def editDistance(s1, s2): 111 | dist = [list(range(len(s2) + 1))] + \ 112 | [ [i] + [ 0 ] * len(s2) for i in range(1, len(s1) + 1) ] 113 | for i in range(1, len(s1) + 1): 114 | for j in range(1, len(s2) + 1): 115 | dist[i][j] = min(dist[i-1][j] + 1, 116 | dist[i][j-1] + 1, 117 | dist[i-1][j-1] + (s1[i-1] != s2[j-1])) 118 | return dist[len(s1)][len(s2)] 119 | 120 | def createTempDir(base): 121 | for n in range(100): 122 | name = "{}_{}".format(base, n) 123 | try: 124 | mkdir(name) 125 | return name 126 | except OSError: 127 | pass 128 | else: 129 | raise ValueError("could not create temp directory for {}".format(base)) 130 | 131 | def cleanTempDir(dir): 132 | rmtree(dir, ignore_errors=True) 133 | 134 | def doDelete(name, dir): 135 | try: 136 | remove(join(dir, name)) 137 | except OSError: 138 | pass 139 | 140 | def doCopy(dest, src, dir): 141 | try: 142 | doDelete(dest, dir) 143 | copyfile(join(src_dir, src), join(dir, dest)) 144 | except OSError: 145 | raise ValueError("file {} could not be copied to {}".format(src, dest)) 146 | 147 | def doExecute(cmnd, dir, timeout): 148 | here = getcwd() 149 | out = "" 150 | try: 151 | chdir(dir) 152 | full_cmnd = "{} {}".format(GITLET_COMMAND, cmnd) 153 | out = check_output(full_cmnd, shell=True, universal_newlines=True, 154 | stdin=DEVNULL, stderr=STDOUT, timeout=timeout) 155 | return "OK", out 156 | except CalledProcessError as excp: 157 | return ("java gitlet.Main exited with code {}".format(excp.args[0]), 158 | excp.output) 159 | except TimeoutExpired: 160 | return "timeout", None 161 | finally: 162 | chdir(here) 163 | 164 | def canonicalize(s): 165 | if s is None: 166 | return None 167 | return re.sub('\r', '', s) 168 | 169 | def fileExists(f, dir): 170 | return exists(join(dir, f)) 171 | 172 | def correctFileOutput(name, expected, dir): 173 | userData = canonicalize(contents(join(dir, name))) 174 | stdData = canonicalize(contents(join(src_dir, expected))) 175 | return userData == stdData 176 | 177 | def correctProgramOutput(expected, actual, last_groups, is_regexp): 178 | expected = re.sub(r'[ \t]+\n', '\n', '\n'.join(expected)) 179 | expected = re.sub(r'(?m)^[ \t]+', ' ', expected) 180 | actual = re.sub(r'[ \t]+\n', '\n', actual) 181 | actual = re.sub(r'(?m)^[ \t]+', ' ', actual) 182 | 183 | last_groups[:] = (actual,) 184 | if is_regexp: 185 | try: 186 | if not Match(expected.rstrip() + r"\Z", actual) \ 187 | and not Match(expected.rstrip() + r"\Z", actual.rstrip()): 188 | return False 189 | except: 190 | raise ValueError("bad pattern") 191 | last_groups[:] += Mat.groups() 192 | elif editDistance(expected.rstrip(), actual.rstrip()) > output_tolerance: 193 | return False 194 | return True 195 | 196 | def reportDetails(test, included_files, line_num): 197 | if show is None: 198 | return 199 | if show <= 0: 200 | print(" Limit on error details exceeded.") 201 | return 202 | direct = dirname(test) 203 | 204 | print(" Error on line {} of {}".format(line_num, basename(test))) 205 | 206 | for base in [basename(test)] + included_files: 207 | full = join(dirname(test), base) 208 | print(("-" * 20 + " {} " + "-" * 20).format(base)) 209 | text_lines = list(enumerate(re.split(r'\n\r?', contents(full))))[:-1] 210 | fmt = "{{:{}d}}. {{}}".format(round(log(len(text_lines), 10))) 211 | text = '\n'.join(map(lambda p: fmt.format(p[0] + 1, p[1]), text_lines)) 212 | print(text) 213 | print("-" * (42 + len(base))) 214 | 215 | def chop_nl(s): 216 | if s and s[-1] == '\n': 217 | return s[:-1] 218 | else: 219 | return s 220 | 221 | def line_reader(f, prefix): 222 | n = 0 223 | try: 224 | with open(f) as inp: 225 | while True: 226 | L = inp.readline() 227 | if L == '': 228 | return 229 | n += 1 230 | included_file = yield (prefix + str(n), L) 231 | if included_file: 232 | yield None 233 | yield from line_reader(included_file, prefix + str(n) + ".") 234 | except FileNotFoundError: 235 | raise ValueError("file {} not found".format(f)) 236 | 237 | def doTest(test): 238 | last_groups = [] 239 | base = splitext(basename(test))[0] 240 | print("{}:".format(base), end=" ") 241 | cdir = tmpdir = createTempDir(base) 242 | if verbose: 243 | print("Testing directory: {}".format(tmpdir)) 244 | timeout = TIMEOUT 245 | defns = {} 246 | 247 | def do_substs(L): 248 | c = 0 249 | L0 = None 250 | while L0 != L and c < 10: 251 | c += 1 252 | L0 = L 253 | L = re.sub(r'\$\{(.*?)\}', subst_var, L) 254 | return L 255 | 256 | def subst_var(M): 257 | key = M.group(1) 258 | if Match(r'\d+$', key): 259 | try: 260 | return last_groups[int(key)] 261 | except IndexError: 262 | raise ValueError("FAILED (nonexistent group: {{{}}})" 263 | .format(key)) 264 | elif M.group(1) in defns: 265 | return defns[M.group(1)] 266 | else: 267 | raise ValueError("undefined substitution: ${{{}}}".format(M.group(1))) 268 | 269 | try: 270 | line_num = None 271 | inp = line_reader(test, '') 272 | included_files = [] 273 | while True: 274 | line_num, line = next(inp, (line_num, '')) 275 | if line == "": 276 | print("OK") 277 | return True 278 | if not Match(r'\s*#', line): 279 | line = do_substs(line) 280 | if verbose: 281 | print("+ {}".format(line.rstrip())) 282 | if Match(r'\s*#', line) or Match(r'\s+$', line): 283 | pass 284 | elif Match(r'I\s+(\S+)', line): 285 | inp.send(join(dirname(test), Group(1))) 286 | included_files.append(Group(1)) 287 | elif Match(r'C\s*(\S*)', line): 288 | if Group(1) == "": 289 | cdir = tmpdir 290 | else: 291 | cdir = join(tmpdir, Group(1)) 292 | if not exists(cdir): 293 | mkdir(cdir) 294 | elif Match(r'T\s*(\S+)', line): 295 | try: 296 | timeout = float(Group(1)) 297 | except: 298 | ValueError("bad time: {}".format(line)) 299 | elif Match(r'\+\s*(\S+)\s+(\S+)', line): 300 | doCopy(Group(1), Group(2), cdir) 301 | elif Match(r'-\s*(\S+)', line): 302 | doDelete(Group(1), cdir) 303 | elif Match(r'>\s*(.*)', line): 304 | cmnd = Group(1) 305 | expected = [] 306 | while True: 307 | line_num, L = next(inp, (line_num, '')) 308 | if L == '': 309 | raise ValueError("unterminated command: {}" 310 | .format(line)) 311 | L = L.rstrip() 312 | if Match(r'<<<(\*?)', L): 313 | is_regexp = Group(1) 314 | break 315 | expected.append(do_substs(L)) 316 | msg, out = doExecute(cmnd, cdir, timeout) 317 | if verbose: 318 | if out: 319 | print(re.sub(r'(?m)^', '- ', chop_nl(out))) 320 | if msg == "OK": 321 | if not correctProgramOutput(expected, out, last_groups, 322 | is_regexp): 323 | msg = "incorrect output" 324 | if msg != "OK": 325 | print("ERROR ({})".format(msg)) 326 | reportDetails(test, included_files, line_num) 327 | return False 328 | elif Match(r'=\s*(\S+)\s+(\S+)', line): 329 | if not correctFileOutput(Group(1), Group(2), cdir): 330 | print("ERROR (file {} has incorrect content)" 331 | .format(Group(1))) 332 | reportDetails(test, included_files, line_num) 333 | return False 334 | elif Match(r'\*\s*(\S+)', line): 335 | if fileExists(Group(1), cdir): 336 | print("ERROR (file {} present)".format(Group(1))) 337 | reportDetails(test, included_files, line_num) 338 | return False 339 | elif Match(r'E\s*(\S+)', line): 340 | if not fileExists(Group(1), cdir): 341 | print("ERROR (file or directory {} not present)" 342 | .format(Group(1))) 343 | reportDetails(test, included_files, line_num) 344 | return False 345 | elif Match(r'(?s)D\s*([a-zA-Z_][a-zA-Z_0-9]*)\s*"(.*)"\s*$', line): 346 | defns[Group(1)] = Group(2) 347 | else: 348 | raise ValueError("bad test line at {}".format(line_num)) 349 | finally: 350 | if not keep: 351 | cleanTempDir(tmpdir) 352 | 353 | if __name__ == "__main__": 354 | show = None 355 | keep = False 356 | prog_dir = None 357 | verbose = False 358 | src_dir = 'src' 359 | output_tolerance = 3 360 | 361 | try: 362 | opts, files = \ 363 | getopt(sys.argv[1:], '', 364 | ['show=', 'keep', 'progdir=', 'verbose', 'src=', 365 | 'tolerance=']) 366 | for opt, val in opts: 367 | if opt == '--show': 368 | val = val.lower() 369 | if re.match(r'-?\d+', val): 370 | show = int(val) 371 | elif val == 'all': 372 | show = val 373 | else: 374 | Usage() 375 | elif opt == "--keep": 376 | keep = True 377 | elif opt == "--progdir": 378 | prog_dir = val 379 | elif opt == "--src": 380 | src_dir = abspath(val) 381 | elif opt == "--verbose": 382 | verbose = True 383 | elif opt == "--tolerance": 384 | output_tolerance = int(val) 385 | if prog_dir is None: 386 | prog_dir = abspath(getcwd()) 387 | k = 10 388 | while k > 0 and access(prog_dir, W_OK): 389 | k -= 1 390 | if exists(join(prog_dir, 'gitlet', 'Main.class')): 391 | break 392 | prog_dir = dirname(prog_dir) 393 | else: 394 | print("Could not find gitlet.Main.", file=sys.stderr) 395 | sys.exit(1) 396 | except GetoptError: 397 | Usage() 398 | if not files: 399 | print(USAGE) 400 | sys.exit(0) 401 | 402 | ON_WINDOWS = Match(r'.*\\', join('a', 'b')) 403 | if ON_WINDOWS: 404 | environ['CLASSPATH'] = "{};{}".format(prog_dir, environ['CLASSPATH']) 405 | else: 406 | environ['CLASSPATH'] = "{}:{}".format(prog_dir, environ['CLASSPATH']) 407 | GITLET_COMMAND = 'exec ' + GITLET_COMMAND 408 | 409 | num_tests = len(files) 410 | errs = 0 411 | fails = 0 412 | 413 | for test in files: 414 | try: 415 | if not exists(test): 416 | num_tests -= 1 417 | elif not doTest(test): 418 | errs += 1 419 | if type(show) is int: 420 | show -= 1 421 | except ValueError as excp: 422 | print("FAILED ({})".format(excp.args[0])) 423 | fails += 1 424 | 425 | print() 426 | print("Ran {} tests. ".format(num_tests), end="") 427 | if errs == fails == 0: 428 | print("All passed.") 429 | else: 430 | print("{} passed.".format(num_tests - errs - fails)) 431 | sys.exit(1) 432 | -------------------------------------------------------------------------------- /gitlet/Repo.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | import java.io.File; 4 | import java.io.Serializable; 5 | import java.nio.file.Files; 6 | import java.nio.file.Paths; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.HashMap; 10 | import java.util.Objects; 11 | 12 | 13 | /** The main class repo for Gitlet. It contains all commands' implementation. 14 | * @author Ruize Xu 15 | */ 16 | public class Repo implements Serializable { 17 | 18 | /** 19 | * The current working directory, File type. 20 | */ 21 | private final File cwd = new File(System.getProperty("user.dir")); 22 | 23 | /** 24 | * The current working directory, String type. 25 | */ 26 | private final String cwdString = System.getProperty("user.dir"); 27 | 28 | /** 29 | * The structure is used to store the head of each branch, 30 | * key for branchName, and corresponding value is uid of the 31 | * exact commit of this branch. 32 | */ 33 | private HashMap _branches; 34 | 35 | /** 36 | * The head pointer that corresponds to the branch that actually will be 37 | * pointing at the commit that we want. 38 | */ 39 | private String _head; 40 | 41 | /** 42 | * Staging area used to store the blob object, key is file name, 43 | * value is the Blob object, help us identify whether the file is 44 | * changed. 45 | */ 46 | private HashMap _stagingArea; 47 | 48 | /** 49 | * Removed files are like the opposite of the Staging Area, 50 | * these are files that WERE tracked before, and now, for the 51 | * next commit, they're not going to be added. 52 | */ 53 | private ArrayList _removedFiles; 54 | 55 | /** 56 | * Global variable stores the data used for 57 | * resurion in dfsForSplitCommit() method. 58 | */ 59 | private HashMap cCommits = new HashMap(); 60 | 61 | /** 62 | * Global variable stores the data used for 63 | * resurion in dfsForSplitCommit() method. 64 | */ 65 | private HashMap gCommits = new HashMap(); 66 | 67 | /** 68 | * Return the head commit's uid of current branch. 69 | * @return String uid 70 | */ 71 | public String getHead() { 72 | return _branches.get(_head); 73 | } 74 | 75 | /** 76 | * Constructor. 77 | */ 78 | public Repo() { 79 | if (!Files.exists(Paths.get(".gitlet"))) { 80 | Commit initial = new Commit("initial commit"); 81 | File gitlet = new File(".gitlet"); 82 | File commits = new File(".gitlet/commits"); 83 | File staging = new File(".gitlet/staging"); 84 | gitlet.mkdir(); 85 | commits.mkdir(); 86 | staging.mkdir(); 87 | String id = initial.getUid(); 88 | File initialFile = new File(".gitlet/commits/" + id); 89 | Utils.writeContents(initialFile, (Object) Utils.serialize(initial)); 90 | _head = "master"; 91 | _branches = new HashMap(); 92 | _branches.put("master", initial.getUid()); 93 | _stagingArea = new HashMap(); 94 | _removedFiles = new ArrayList(); 95 | } else { 96 | System.out.println("A Gitlet version-control " 97 | + "system already exists in the " 98 | + "current directory"); 99 | } 100 | } 101 | 102 | /** 103 | * The add operation. 104 | * @param filename String name of the file you added 105 | */ 106 | public void add(String filename) { 107 | if (!new File(filename).exists()) { 108 | Utils.message("File does not exist."); 109 | throw new GitletException(); 110 | } 111 | _removedFiles.remove(filename); 112 | Blob blob = new Blob(filename); 113 | String blobHashID = blob.getHashID(); 114 | Commit lastCommit = uidToCommit(getHead()); 115 | HashMap files = lastCommit.getBlobs(); 116 | File stagingblob = new File(".gitlet/staging/" + blobHashID); 117 | boolean alreadyAdded = false; 118 | boolean emptyBlobs = false; 119 | if (files == null) { 120 | emptyBlobs = true; 121 | } else { 122 | for (Blob temp : files.values()) { 123 | if (temp.getHashID().equals(blobHashID)) { 124 | alreadyAdded = true; 125 | break; 126 | } 127 | } 128 | } 129 | if (!alreadyAdded || emptyBlobs) { 130 | _stagingArea.put(filename, blob); 131 | String contents = Utils.readContentsAsString(new File(filename)); 132 | Utils.writeContents(stagingblob, contents); 133 | } else { 134 | if (stagingblob.exists()) { 135 | _stagingArea.remove(filename); 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * The commit operation. 142 | * @param msg String the commit message 143 | */ 144 | public void commit(String msg) { 145 | if (msg.trim().equals("")) { 146 | Utils.message("Please enter a commit message."); 147 | throw new GitletException(); 148 | } 149 | Commit lastCommit = uidToCommit(getHead()); 150 | HashMap trackedFiles = lastCommit.getBlobs(); 151 | if (trackedFiles == null) { 152 | trackedFiles = new HashMap(); 153 | } 154 | if (_stagingArea.size() != 0 || _removedFiles.size() != 0) { 155 | for (String fileName : _stagingArea.keySet()) { 156 | trackedFiles.put(fileName, _stagingArea.get(fileName)); 157 | } 158 | for (String fileName : _removedFiles) { 159 | trackedFiles.remove(fileName); 160 | } 161 | } else { 162 | Utils.message("No changes added to the commit."); 163 | throw new GitletException(); 164 | } 165 | String[] parent = new String[]{lastCommit.getUid()}; 166 | String branch = _head; 167 | Commit newCommit = new Commit(msg, parent, branch, trackedFiles); 168 | String s = newCommit.getUid(); 169 | File newCommFile = new File(".gitlet/commits/" + s); 170 | Utils.writeObject(newCommFile, newCommit); 171 | 172 | _stagingArea = new HashMap(); 173 | _removedFiles = new ArrayList(); 174 | _branches.put(_head, newCommit.getUid()); 175 | } 176 | 177 | /** 178 | * The commit operation for merge. 179 | * @param msg String the commit message 180 | * @param parents Array of the hashID of current Commit's parent 181 | */ 182 | public void commit(String msg, String[] parents) { 183 | if (msg.trim().equals("")) { 184 | Utils.message("Please enter a commit message."); 185 | throw new GitletException(); 186 | } 187 | Commit lastCommit = uidToCommit(getHead()); 188 | HashMap trackedFiles = lastCommit.getBlobs(); 189 | if (trackedFiles == null) { 190 | trackedFiles = new HashMap(); 191 | } 192 | if (_stagingArea.size() != 0 || _removedFiles.size() != 0) { 193 | for (String fileName : _stagingArea.keySet()) { 194 | trackedFiles.put(fileName, _stagingArea.get(fileName)); 195 | } 196 | for (String fileName : _removedFiles) { 197 | trackedFiles.remove(fileName); 198 | } 199 | } else { 200 | Utils.message("No changes added to the commit."); 201 | throw new GitletException(); 202 | } 203 | String branch = lastCommit.getBranchName(); 204 | Commit newCommit = new Commit(msg, parents, branch, trackedFiles); 205 | String s = newCommit.getUid(); 206 | File newCommFile = new File(".gitlet/commits/" + s); 207 | Utils.writeObject(newCommFile, newCommit); 208 | 209 | _stagingArea = new HashMap(); 210 | _removedFiles = new ArrayList(); 211 | _branches.put(_head, newCommit.getUid()); 212 | } 213 | 214 | /** 215 | * The log operation. 216 | */ 217 | public void log() { 218 | String head = getHead(); 219 | while (head != null) { 220 | Commit first = uidToCommit(head); 221 | if (first.getParentid() != null && first.getParentid().length > 1) { 222 | System.out.println("==="); 223 | System.out.println("commit " + head); 224 | String short1 = first.getParentid()[0].substring(0, 7); 225 | String short2 = first.getParentid()[1].substring(0, 7); 226 | System.out.println("Merge: " + short1 + " " + short2); 227 | System.out.println("Date: " + first.getTimestamp()); 228 | System.out.println(first.getMessage()); 229 | System.out.println(); 230 | } else { 231 | System.out.println("==="); 232 | System.out.println("commit " + head); 233 | System.out.println("Date: " + first.getTimestamp()); 234 | System.out.println(first.getMessage()); 235 | System.out.println(); 236 | } 237 | head = first.getParentID(); 238 | } 239 | } 240 | 241 | /** 242 | * the global-log operation. 243 | */ 244 | public void globalLog() { 245 | File commitDir = new File(".gitlet/commits"); 246 | for (File commitFile : Objects.requireNonNull(commitDir.listFiles())) { 247 | String uid = commitFile.getName(); 248 | Commit temp = uidToCommit(uid); 249 | if (temp.getParentid() != null && temp.getParentid().length > 1) { 250 | System.out.println("==="); 251 | System.out.println("commit " + uid); 252 | String short1 = temp.getParentid()[0].substring(0, 7); 253 | String short2 = temp.getParentid()[1].substring(0, 7); 254 | System.out.println("Merge: " + short1 + " " + short2); 255 | System.out.println("Date: " + temp.getTimestamp()); 256 | System.out.println(temp.getMessage()); 257 | System.out.println(); 258 | } else { 259 | System.out.println("==="); 260 | System.out.println("commit " + uid); 261 | System.out.println("Date: " + temp.getTimestamp()); 262 | System.out.println(temp.getMessage()); 263 | System.out.println(); 264 | } 265 | } 266 | 267 | } 268 | 269 | /** 270 | * The status operation. 271 | */ 272 | public void status() { 273 | System.out.println("=== Branches ==="); 274 | Object[] keys = _branches.keySet().toArray(); 275 | Arrays.sort(keys); 276 | for (Object branch : keys) { 277 | if (branch.equals(_head)) { 278 | System.out.println("*" + branch); 279 | } else { 280 | System.out.println(branch); 281 | } 282 | } 283 | System.out.println(); 284 | System.out.println("=== Staged Files ==="); 285 | Object[] stages = _stagingArea.keySet().toArray(); 286 | Arrays.sort(stages); 287 | for (Object staged : stages) { 288 | System.out.println(staged); 289 | } 290 | System.out.println(); 291 | System.out.println("=== Removed Files ==="); 292 | Object[] removeds = _removedFiles.toArray(); 293 | Arrays.sort(removeds); 294 | for (Object removed : removeds) { 295 | System.out.println(removed); 296 | } 297 | 298 | 299 | ArrayList modify = new ArrayList(); 300 | ArrayList untrack = new ArrayList(); 301 | ArrayList deleted = new ArrayList(); 302 | String head = _branches.get(_head); 303 | stateDetect(head, modify, untrack, deleted); 304 | 305 | System.out.println("\n=== Modifications Not Staged For Commit ==="); 306 | 307 | System.out.println("\n=== Untracked Files ==="); 308 | System.out.println(); 309 | 310 | 311 | } 312 | 313 | /** StateDetect function is to detect state information. 314 | * @param commitHash is path of the commit state. 315 | * @param modify is modified files. 316 | * @param untrack is untracked files. 317 | * @param delete is deleted files.*/ 318 | public void stateDetect(String commitHash, ArrayList modify, 319 | ArrayList untrack, ArrayList delete) { 320 | HashMap curentBlobs = uidToCommit(commitHash).getBlobs(); 321 | ArrayList cwdDirAll = new ArrayList<>(); 322 | for (File file : Objects.requireNonNull(cwd.listFiles())) { 323 | if (!file.isDirectory()) { 324 | cwdDirAll.add(file); 325 | } 326 | } 327 | boolean untrackedFlag = true; 328 | 329 | if (!cwdDirAll.isEmpty()) { 330 | if (curentBlobs == null) { 331 | untrack.addAll(cwdDirAll); 332 | } else { 333 | for (File file : cwdDirAll) { 334 | String contents = Utils.readContentsAsString(file); 335 | for (Blob blob : curentBlobs.values()) { 336 | if (blob.getName().equals(file.getName())) { 337 | untrackedFlag = false; 338 | if (!blob.getContentsAsString().equals(contents)) { 339 | modify.add(file); 340 | } 341 | } 342 | if (untrackedFlag) { 343 | untrack.add(file); 344 | } 345 | } 346 | } 347 | } 348 | } else { 349 | if (curentBlobs != null) { 350 | for (Blob blob : curentBlobs.values()) { 351 | File temp = new File(blob.getName()); 352 | delete.add(temp); 353 | } 354 | } 355 | } 356 | } 357 | 358 | /** 359 | * The checkout opertion, takes in a Arraylist ARGS. 360 | * @param args ArrayList of the input _operand 361 | */ 362 | public void checkout(ArrayList args) { 363 | String commID; 364 | String fileName; 365 | if (args.size() == 2 && args.get(0).equals("--")) { 366 | fileName = args.get(1); 367 | commID = getHead(); 368 | } else if (args.size() == 3 && args.get(1).equals("--")) { 369 | commID = args.get(0); 370 | fileName = args.get(2); 371 | } else { 372 | Utils.message("Incorrect operands"); 373 | throw new GitletException(); 374 | } 375 | commID = shortToLong(commID); 376 | Commit comm = uidToCommit(commID); 377 | HashMap trackedFiles = comm.getBlobs(); 378 | boolean find = false; 379 | for (Blob blob : trackedFiles.values()) { 380 | if (blob.getName().equals(fileName)) { 381 | File f = new File(fileName); 382 | String p = ".gitlet/staging/"; 383 | String blobFileName = p + blob.getHashID(); 384 | File g = new File(blobFileName); 385 | String contents = Utils.readContentsAsString(g); 386 | Utils.writeContents(f, contents); 387 | find = true; 388 | } 389 | } 390 | if (!find) { 391 | Utils.message("File does not exist in that commit."); 392 | throw new GitletException(); 393 | } 394 | } 395 | 396 | /** 397 | * This is the third use case for checkout. It takes in a branchName. 398 | * @param branchName String The name of branch to change to 399 | */ 400 | public void checkout(String branchName) { 401 | if (!_branches.containsKey(branchName)) { 402 | Utils.message("No such branch exists."); 403 | throw new GitletException(); 404 | } 405 | if (_head.equals(branchName)) { 406 | String s = "No need to checkout the current branch."; 407 | Utils.message(s); 408 | throw new GitletException(); 409 | } 410 | String commID = _branches.get(branchName); 411 | Commit comm = uidToCommit(commID); 412 | HashMap blobs = comm.getBlobs(); 413 | checkForUntracked(cwd); 414 | 415 | for (File file : Objects.requireNonNull(cwd.listFiles())) { 416 | if (file.getName().equals(".DS_Store") 417 | || file.getName().equals(".gitignore") 418 | || file.getName().equals("proj3.iml") 419 | || file.getName().equals("Makefile")) { 420 | continue; 421 | } 422 | if (file.isDirectory()) { 423 | continue; 424 | } 425 | if (!file.getName().equals(".gitlet")) { 426 | if (!Utils.restrictedDelete(file)) { 427 | Utils.message("Can not delete file" + file.getName()); 428 | throw new GitletException(); 429 | } 430 | } 431 | } 432 | if (blobs != null) { 433 | for (Blob blob : blobs.values()) { 434 | String fileDir = blob.getName(); 435 | String blobhash = blob.getHashID(); 436 | File blobfile = new File(".gitlet/staging/" + blobhash); 437 | File file = new File(fileDir); 438 | String contents = Utils.readContentsAsString(blobfile); 439 | Utils.writeContents(file, contents); 440 | } 441 | } 442 | _stagingArea = new HashMap(); 443 | _removedFiles = new ArrayList(); 444 | _head = branchName; 445 | } 446 | 447 | /** 448 | * The remove opertion. 449 | * @param fileName String the name of file you want to remove. 450 | */ 451 | public void rm(String fileName) { 452 | File file = new File(fileName); 453 | Commit lastCommit = uidToCommit(getHead()); 454 | HashMap trackedFiles = lastCommit.getBlobs(); 455 | boolean flag = false; 456 | if (trackedFiles != null) { 457 | for (String blobHash : trackedFiles.keySet()) { 458 | Blob temp = trackedFiles.get(blobHash); 459 | String blobname = temp.getName(); 460 | if (blobname.equals(fileName)) { 461 | flag = true; 462 | break; 463 | } 464 | } 465 | } 466 | if (!file.exists() && !flag) { 467 | Utils.message("File does not exist."); 468 | throw new GitletException(); 469 | } 470 | boolean changed = false; 471 | if (_stagingArea.containsKey(fileName)) { 472 | _stagingArea.remove(fileName); 473 | changed = true; 474 | } 475 | if (flag) { 476 | _removedFiles.add(fileName); 477 | File toRemove = new File(fileName); 478 | Utils.restrictedDelete(toRemove); 479 | changed = true; 480 | } 481 | if (!changed) { 482 | Utils.message("No reason to remove the file."); 483 | throw new GitletException(); 484 | } 485 | } 486 | 487 | /** 488 | * Create new branch named branchName. 489 | * @param branchName String 490 | */ 491 | public void branch(String branchName) { 492 | if (!_branches.containsKey(branchName)) { 493 | _branches.put(branchName, getHead()); 494 | } else { 495 | Utils.message("A branch with that name already exists."); 496 | throw new GitletException(); 497 | } 498 | } 499 | 500 | /** 501 | * The remove branch operation. 502 | * @param branchName String the branch you want to remove 503 | */ 504 | public void rmbranch(String branchName) { 505 | if (!_branches.containsKey(branchName)) { 506 | Utils.message("A branch with that name does not exist."); 507 | throw new GitletException(); 508 | } 509 | if (_head.equals(branchName)) { 510 | Utils.message("Cannot remove the current branch."); 511 | throw new GitletException(); 512 | } else { 513 | _branches.remove(branchName); 514 | } 515 | } 516 | 517 | /** 518 | * The reset operation. 519 | * @param uid String the hashID of the commit you want to reset to 520 | */ 521 | public void reset(String uid) { 522 | uid = shortToLong(uid); 523 | Commit c = uidToCommit(uid); 524 | HashMap blobs = c.getBlobs(); 525 | checkForUntracked(cwd); 526 | for (File file : Objects.requireNonNull(cwd.listFiles())) { 527 | if (!file.isDirectory()) { 528 | if (file.getName().equals(".gitignore") 529 | || file.getName().equals("proj3.iml") 530 | || file.getName().equals("Makefile")) { 531 | continue; 532 | } 533 | String fileName = file.getName(); 534 | boolean find = false; 535 | for (Blob b : blobs.values()) { 536 | if (fileName.equals(b.getName())) { 537 | find = true; 538 | break; 539 | } 540 | } 541 | if (!find) { 542 | if (!Utils.restrictedDelete(file)) { 543 | Utils.message("Can not delete file" + file.getName()); 544 | throw new GitletException(); 545 | } 546 | } 547 | } 548 | } 549 | for (Blob blob : blobs.values()) { 550 | String blobhash = blob.getHashID(); 551 | File blobfile = new File(".gitlet/staging/" + blobhash); 552 | File file = new File(blob.getName()); 553 | String contents = Utils.readContentsAsString(blobfile); 554 | Utils.writeContents(file, contents); 555 | } 556 | _stagingArea = new HashMap(); 557 | _branches.put(_head, uid); 558 | } 559 | 560 | /** 561 | * The find operation. 562 | * @param message the commit message you want to find 563 | */ 564 | public void find(String message) { 565 | File commitDir = new File(".gitlet/commits"); 566 | boolean flag = false; 567 | for (File commitFile 568 | : Objects.requireNonNull(commitDir.listFiles())) { 569 | String fileName = commitFile.getName(); 570 | Commit temp = uidToCommit(fileName); 571 | if (temp.getMessage().equals(message)) { 572 | System.out.println(fileName); 573 | flag = true; 574 | } 575 | } 576 | if (!flag) { 577 | Utils.message("Found no commit with that message."); 578 | throw new GitletException(); 579 | } 580 | } 581 | 582 | /** 583 | * The merge operation. 584 | * @param branchName String name of branch 585 | */ 586 | public void merge(String branchName) { 587 | if (!_branches.containsKey(branchName)) { 588 | Utils.message("A branch with that name does not exist."); 589 | throw new GitletException(); 590 | } 591 | if (_stagingArea.size() != 0 || _removedFiles.size() != 0) { 592 | Utils.message("You have uncommitted changes."); 593 | throw new GitletException(); 594 | } 595 | if (branchName.equals(_head)) { 596 | Utils.message("Cannot merge a branch with itself."); 597 | throw new GitletException(); 598 | } 599 | String splitCommitHash = splitPoint(_head, branchName); 600 | if (splitCommitHash.equals(_branches.get(branchName))) { 601 | Utils.message("Given branch is an ancestor of the current branch."); 602 | throw new GitletException(); 603 | } 604 | if (splitCommitHash.equals(_branches.get(_head))) { 605 | checkout(branchName); 606 | _branches.put(_head, _branches.get(branchName)); 607 | Utils.message("Current branch fast-forwarded."); 608 | throw new GitletException(); 609 | } 610 | checkForUntracked(cwd); 611 | Commit splitCommit = uidToCommit(splitCommitHash); 612 | Commit currentHead = uidToCommit(getHead()); 613 | Commit givenHead = uidToCommit(_branches.get(branchName)); 614 | HashMap splitBlobs = splitCommit.getBlobs(); 615 | HashMap currentBlobs = currentHead.getBlobs(); 616 | HashMap givenBlobs = givenHead.getBlobs(); 617 | 618 | mergeForSplit(branchName, splitBlobs, currentBlobs, givenBlobs); 619 | 620 | mergeForGiven(branchName, splitBlobs, currentBlobs, givenBlobs); 621 | 622 | String[] parents = new String[]{getHead(), _branches.get(branchName)}; 623 | commit("Merged " + branchName + " into " + _head + ".", parents); 624 | } 625 | 626 | /** 627 | * Helper function for merge in first step, traverse the 628 | * file in split commit, then merge it. 629 | * @param branchName String name of branch 630 | * @param splitBlobs HashMap all tracked blobs in split Commit 631 | * @param currentBlobs HashMap all tracked blobs in current branch head 632 | * @param givenBlobs HashMap all tracked blobs in given branch head 633 | */ 634 | private void mergeForSplit(String branchName, 635 | HashMap splitBlobs, 636 | HashMap currentBlobs, 637 | HashMap givenBlobs) { 638 | if (splitBlobs != null) { 639 | for (Blob blob : splitBlobs.values()) { 640 | String blobName = blob.getName(); 641 | boolean isModifiedInCurrent = isModified(blobName, 642 | splitBlobs, currentBlobs); 643 | boolean isModifiedInGiven = isModified(blobName, 644 | splitBlobs, givenBlobs); 645 | boolean isModifiedBetween = isModified(blobName, 646 | currentBlobs, givenBlobs); 647 | boolean isInCurrent = isBlobInHashMap(blobName, currentBlobs); 648 | boolean isInGiven = isBlobInHashMap(blobName, givenBlobs); 649 | if (isInCurrent && !isInGiven) { 650 | if (isModifiedInCurrent) { 651 | mergeConflict(branchName, blobName); 652 | } else { 653 | rm(blobName); 654 | Utils.restrictedDelete(blobName); 655 | } 656 | } 657 | if (isInCurrent && isInGiven) { 658 | if (isModifiedBetween && isModifiedInCurrent 659 | && isModifiedInGiven) { 660 | mergeConflict(branchName, blobName); 661 | break; 662 | } 663 | if (isModifiedInGiven) { 664 | checkoutFile(branchName, givenBlobs, blobName); 665 | } 666 | } 667 | if (!isInCurrent && isInGiven) { 668 | if (isModifiedInGiven) { 669 | mergeConflict(branchName, blobName); 670 | break; 671 | } 672 | } 673 | 674 | } 675 | } 676 | } 677 | 678 | /** 679 | * Helper function for merge in second step, traverse the 680 | * file in given branch, then merge it. 681 | * @param branchName String name of branch 682 | * @param splitBlobs HashMap all tracked blobs in split Commit 683 | * @param currentBlobs HashMap all tracked blobs in current branch head 684 | * @param givenBlobs HashMap all tracked blobs in given branch head 685 | */ 686 | private void mergeForGiven(String branchName, 687 | HashMap splitBlobs, 688 | HashMap currentBlobs, 689 | HashMap givenBlobs) { 690 | if (givenBlobs != null) { 691 | for (Blob blob : givenBlobs.values()) { 692 | String blobName = blob.getName(); 693 | boolean isInSplit = isBlobInHashMap(blobName, splitBlobs); 694 | boolean isInCurrent = isBlobInHashMap(blobName, currentBlobs); 695 | boolean isModifiedBetween = isModified(blobName, 696 | currentBlobs, givenBlobs); 697 | if (!isInCurrent) { 698 | if (!isInSplit) { 699 | checkoutFile(branchName, givenBlobs, blobName); 700 | } 701 | } else { 702 | if (!isInSplit) { 703 | if (isModifiedBetween) { 704 | mergeConflict(branchName, blobName); 705 | break; 706 | } 707 | } 708 | } 709 | 710 | } 711 | } 712 | } 713 | 714 | /** 715 | * Helper function for check whether the exact blob with the 716 | * input blobName in the HashMap blobs. 717 | * @param blobname String name of blob you wan to check 718 | * @param blobs HashMap key is hashID, value is the Blob object 719 | * @return true is the blob in the HashMap, otherwise false. 720 | */ 721 | private boolean isBlobInHashMap(String blobname, 722 | HashMap blobs) { 723 | boolean flag = false; 724 | if (blobs != null) { 725 | for (Blob temp : blobs.values()) { 726 | if (temp.getName().equals(blobname)) { 727 | flag = true; 728 | break; 729 | } 730 | } 731 | } 732 | return flag; 733 | } 734 | 735 | /** 736 | * Calling the checkout method for merge the file between two branches. 737 | * @param branchName String the name of branch where the file in 738 | * @param blobs HashMap the blobs 739 | * @param blobName String the name of the blob 740 | */ 741 | private void checkoutFile(String branchName, 742 | HashMap blobs, String blobName) { 743 | ArrayList args = new ArrayList<>(); 744 | args.add(_branches.get(branchName)); 745 | args.add("--"); 746 | args.add(blobName); 747 | checkout(args); 748 | _stagingArea.put(blobName, blobs.get(blobName)); 749 | } 750 | 751 | /** 752 | * This method used to handle the conflict situation when 753 | * merge two branches. 754 | * @param branchName String the given branch name 755 | * @param fileName String the file which is conflict 756 | */ 757 | private void mergeConflict(String branchName, String fileName) { 758 | Commit splitCommit = uidToCommit(splitPoint(branchName, _head)); 759 | Commit currentHead = uidToCommit(getHead()); 760 | Commit givenHead = uidToCommit(_branches.get(branchName)); 761 | HashMap splitBlobs = splitCommit.getBlobs(); 762 | HashMap currentBlobs = currentHead.getBlobs(); 763 | HashMap givenBlobs = givenHead.getBlobs(); 764 | String cContents = ""; 765 | for (Blob blob : currentBlobs.values()) { 766 | if (blob.getName().equals(fileName)) { 767 | cContents = blob.getContentsAsString(); 768 | break; 769 | } 770 | } 771 | String gContents = ""; 772 | for (Blob blob : givenBlobs.values()) { 773 | if (blob.getName().equals(fileName)) { 774 | gContents = blob.getContentsAsString(); 775 | break; 776 | } 777 | } 778 | String contents = "<<<<<<< HEAD\n"; 779 | contents += cContents; 780 | contents += "=======\n" + gContents; 781 | contents += ">>>>>>>\n"; 782 | Utils.writeContents(new File(fileName), contents); 783 | add(fileName); 784 | Utils.message("Encountered a merge conflict."); 785 | } 786 | 787 | /** 788 | * Helper Function for check whether the exact file has been changed 789 | * from commit H to commit I. 790 | * @param fileName String the name of file you want to check 791 | * @param h HashMap tracked blobs in commit H 792 | * @param i HashMap tracked blobs in commit i 793 | * @return a boolean if the file with name F has been modified from 794 | * commit H to commit I. 795 | */ 796 | boolean isModified(String fileName, HashMap h, 797 | HashMap i) { 798 | String b1 = "", b2 = ""; 799 | for (Blob blob : h.values()) { 800 | if (blob.getName().equals(fileName)) { 801 | b1 = blob.getHashID(); 802 | } 803 | } 804 | for (Blob blob : i.values()) { 805 | if (blob.getName().equals(fileName)) { 806 | b2 = blob.getHashID(); 807 | } 808 | } 809 | return !b1.equals(b2); 810 | } 811 | 812 | /** 813 | * Takes in two branch names, BRANCH1 and BRANCH2. Returns the 814 | * SHA ID of the common ancestor commit. 815 | * @param currentBranch String the name of current branch 816 | * @param givenBranch String the name of given branch 817 | * @return String the uid of the found Split Commit 818 | */ 819 | private String splitPoint(String currentBranch, String givenBranch) { 820 | String head1hash = _branches.get(currentBranch); 821 | String head2hash = _branches.get(givenBranch); 822 | Commit head1 = uidToCommit(head1hash); 823 | Commit head2 = uidToCommit(head2hash); 824 | 825 | cCommits.put(head1, 0); 826 | gCommits.put(head2, 0); 827 | Commit splitCommit = new Commit(); 828 | 829 | dfsForSplitCommit(head1, 0, "branch1"); 830 | dfsForSplitCommit(head2, 0, "branch2"); 831 | int dist = 100; 832 | 833 | for (Commit current : cCommits.keySet()) { 834 | for (Commit given : gCommits.keySet()) { 835 | if (current.getUid().equals(given.getUid())) { 836 | if (cCommits.get(current) < dist) { 837 | splitCommit = current; 838 | dist = cCommits.get(current); 839 | } 840 | } 841 | } 842 | } 843 | cCommits.clear(); 844 | gCommits.clear(); 845 | return splitCommit.getUid(); 846 | } 847 | 848 | /** 849 | * Helper function for find the nearest SplitPoint using the DFS. 850 | * @param head Commit the start point of our DFS 851 | * @param dist Integer the distance between head to current Commit 852 | * @param branch String of current branch 853 | */ 854 | private void dfsForSplitCommit(Commit head, int dist, String branch) { 855 | if (head == null) { 856 | return; 857 | } 858 | if (head.getAllParentID() == null) { 859 | return; 860 | } 861 | String[] headParent = head.getAllParentID(); 862 | if (branch.equals("branch1")) { 863 | for (String s : headParent) { 864 | Commit temp = uidToCommit(s); 865 | dist += 1; 866 | cCommits.put(temp, dist); 867 | dfsForSplitCommit(temp, dist, "branch1"); 868 | } 869 | } else { 870 | for (String s : headParent) { 871 | Commit temp = uidToCommit(s); 872 | dist += 1; 873 | gCommits.put(temp, dist); 874 | dfsForSplitCommit(temp, dist, "branch2"); 875 | } 876 | } 877 | } 878 | 879 | 880 | /** 881 | * This function takes in the present working directory 882 | * PWD and will determine if there are untracked files 883 | * that mean that this checkout or Merge operation can't 884 | * continue. 885 | * @param dir File of current working directory 886 | */ 887 | private void checkForUntracked(File dir) { 888 | Commit lastCommit = uidToCommit(getHead()); 889 | HashMap trackedFiles = lastCommit.getBlobs(); 890 | String s = "There is an untracked file in the way; delete it, " 891 | + "or add and commit it first."; 892 | for (File f : Objects.requireNonNull(dir.listFiles())) { 893 | if (f.getName().equals(".DS_Store") 894 | || f.getName().equals(".gitignore") 895 | || f.getName().equals("proj3.iml") 896 | || f.getName().equals("Makefile")) { 897 | continue; 898 | } 899 | if (f.isDirectory()) { 900 | continue; 901 | } 902 | if (trackedFiles == null) { 903 | if (Objects.requireNonNull(dir.listFiles()).length > 1) { 904 | Utils.message(s); 905 | throw new GitletException(); 906 | } 907 | } else { 908 | boolean notTracked = !trackedFiles.containsKey(f.getName()); 909 | boolean notStaging = !_stagingArea.containsKey(f.getName()); 910 | boolean notRoot = !f.getName().equals(".gitlet"); 911 | if (notRoot && notTracked && notStaging) { 912 | Utils.message(s); 913 | throw new GitletException(); 914 | } 915 | } 916 | } 917 | } 918 | 919 | /** 920 | * This method is used to find the corresponding Commit object 921 | * according to the unique uid it contains. 922 | * @param uid String the uid for each Commit object 923 | * @return Commit object read from file 924 | */ 925 | public Commit uidToCommit(String uid) { 926 | File f = new File(".gitlet/commits/" + uid); 927 | if (f.exists()) { 928 | return Utils.readObject(f, Commit.class); 929 | } else { 930 | Utils.message("No commit with that id exists."); 931 | throw new GitletException(); 932 | } 933 | } 934 | 935 | /** 936 | * Takes in a shortened String ID and returns a String 937 | * of the full length ID. 938 | * @param id String input of the shorten id 939 | * @return The full size uid of the found Commit 940 | */ 941 | private String shortToLong(String id) { 942 | if (id.length() == Utils.UID_LENGTH) { 943 | return id; 944 | } 945 | File commitFolder = new File(".gitlet/commits"); 946 | File[] commits = commitFolder.listFiles(); 947 | for (File file : commits) { 948 | if (file.getName().contains(id)) { 949 | return file.getName(); 950 | } 951 | } 952 | Utils.message("No commit with that id exists."); 953 | throw new GitletException(); 954 | } 955 | } 956 | --------------------------------------------------------------------------------