├── testing ├── src │ ├── a.txt │ ├── b.txt │ ├── c.txt │ ├── d.txt │ ├── e.txt │ ├── f.txt │ ├── g.txt │ ├── nota.txt │ ├── notb.txt │ ├── notf.txt │ ├── wug.txt │ ├── wug2.txt │ ├── notwug.txt │ ├── wug3.txt │ ├── conflict2.txt │ └── conflict1.txt └── Makefile ├── gitlet ├── Dumpable.java ├── GitletException.java ├── DumpObj.java ├── Blob.java ├── Makefile ├── Refs.java ├── Main.java ├── Commit.java ├── Utils.java └── Repository.java ├── Makefile ├── .gitignore └── README.md /testing/src/a.txt: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /testing/src/b.txt: -------------------------------------------------------------------------------- 1 | b 2 | -------------------------------------------------------------------------------- /testing/src/c.txt: -------------------------------------------------------------------------------- 1 | c 2 | -------------------------------------------------------------------------------- /testing/src/d.txt: -------------------------------------------------------------------------------- 1 | d 2 | -------------------------------------------------------------------------------- /testing/src/e.txt: -------------------------------------------------------------------------------- 1 | e 2 | -------------------------------------------------------------------------------- /testing/src/f.txt: -------------------------------------------------------------------------------- 1 | not f 2 | -------------------------------------------------------------------------------- /testing/src/g.txt: -------------------------------------------------------------------------------- 1 | is g 2 | -------------------------------------------------------------------------------- /testing/src/nota.txt: -------------------------------------------------------------------------------- 1 | not a 2 | -------------------------------------------------------------------------------- /testing/src/notb.txt: -------------------------------------------------------------------------------- 1 | not b 2 | -------------------------------------------------------------------------------- /testing/src/notf.txt: -------------------------------------------------------------------------------- 1 | not f 2 | -------------------------------------------------------------------------------- /testing/src/wug.txt: -------------------------------------------------------------------------------- 1 | This is a wug. 2 | -------------------------------------------------------------------------------- /testing/src/wug2.txt: -------------------------------------------------------------------------------- 1 | Another wug. 2 | -------------------------------------------------------------------------------- /testing/src/notwug.txt: -------------------------------------------------------------------------------- 1 | This is not a wug. 2 | -------------------------------------------------------------------------------- /testing/src/wug3.txt: -------------------------------------------------------------------------------- 1 | And yet another wug. 2 | -------------------------------------------------------------------------------- /testing/src/conflict2.txt: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | Another wug. 3 | ======= 4 | >>>>>>> 5 | -------------------------------------------------------------------------------- /testing/src/conflict1.txt: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | Another wug. 3 | ======= 4 | This is not a wug. 5 | >>>>>>> 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 student_tests/*.in *.in 23 | 24 | .PHONY: default check clean std 25 | 26 | # First, and therefore default, target. 27 | default: 28 | $(RMAKE) -C .. default 29 | #$(RMAKE) PYTHON=$(PYTHON) check 30 | 31 | 32 | check: 33 | @echo "Testing application gitlet.Main..." 34 | $(TESTER) $(TESTER_FLAGS) $(TESTS) 35 | 36 | # 'make clean' will clean up stuff you can reconstruct. 37 | clean: 38 | $(RM) -r */*~ *~ __pycache__ 39 | -------------------------------------------------------------------------------- /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 | # check: Compiles the gitlet package, if needed, and then performs the 5 | # tests described in testing/Makefile. 6 | # clean: Remove regeneratable files (such as .class files) produced by 7 | # other targets and Emacs backup files. 8 | # 9 | # In other words, type 'make' to compile everything; 'make check' to 10 | # compile and test everything, and 'make clean' to clean things up. 11 | # 12 | # You can use this file without understanding most of it, of course, but 13 | # I strongly recommend that you try to figure it out, and where you cannot, 14 | # that you ask questions. The Lab Reader contains documentation. 15 | 16 | # Name of package containing main procedure 17 | PACKAGE = gitlet 18 | 19 | # The name of the Python 3 program, used in the 'check' target. If your system 20 | # has a different name for this program (such as just "python"), run 21 | # the Makefile with 22 | # make PYTHON=python check 23 | PYTHON = python3 24 | 25 | # Flags to pass to tester.py. 26 | TESTER_FLAGS = 27 | 28 | RMAKE = "$(MAKE)" 29 | 30 | # Targets that don't correspond to files, but are to be treated as commands. 31 | .PHONY: default check clean 32 | 33 | default: 34 | $(RMAKE) -C $(PACKAGE) default 35 | 36 | check: default 37 | $(RMAKE) -C testing PYTHON=$(PYTHON) TESTER_FLAGS="$(TESTER_FLAGS)" check 38 | 39 | # 'make clean' will clean up stuff you can reconstruct. 40 | clean: 41 | $(RM) *~ 42 | $(RMAKE) -C $(PACKAGE) clean 43 | $(RMAKE) -C testing clean 44 | 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gitlet/Blob.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | import java.io.File; 4 | import java.io.Serializable; 5 | 6 | import static gitlet.Refs.*; 7 | import static gitlet.Repository.*; 8 | import static gitlet.Utils.*; 9 | 10 | public class Blob implements Serializable { 11 | private String content; 12 | private File filePath; 13 | private String hashName; 14 | 15 | public Blob(String content, String hashName) { 16 | this.content = content; 17 | this.hashName = hashName; 18 | this.filePath = join(BLOBS_FOLDER, hashName); 19 | } 20 | 21 | 22 | public File getFilePath() { 23 | return filePath; 24 | } 25 | 26 | 27 | 28 | /** 29 | * 将blob对象保存进 BLOB_FOLDER文件,内容就是blob文件的content 30 | */ 31 | public void saveBlob() { 32 | if (!filePath.exists()) { 33 | // 如果这个blob原先不存在,则进行blob的储存 34 | writeContents(filePath, this.content); 35 | } 36 | 37 | } 38 | 39 | 40 | 41 | /** 42 | * 根据blobName获取到Blob的内容,其中blobName是一个hash值 43 | * 若是没有这个文件,返回null 44 | * 45 | * @return Blob的内容 46 | */ 47 | public static String getBlobContentFromName(String blobName) { 48 | /* 获取commit文件 */ 49 | String blobContent = null; 50 | File blobFile = join(BLOBS_FOLDER, blobName); 51 | if (blobFile.isFile() && blobFile.exists()) { 52 | blobContent = readContentsAsString(blobFile); 53 | } 54 | 55 | 56 | return blobContent; 57 | 58 | } 59 | 60 | /** 61 | * 将blob.content中的内容覆盖进file文件中 62 | */ 63 | public static void overWriteFileWithBlob(File file, String content) { 64 | writeContents(file, content); 65 | } 66 | 67 | } 68 | 69 | -------------------------------------------------------------------------------- /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 | # check: Compile $(PROG), if needed, and then for each file, F.in, in 6 | # directory testing, use F.in as input to "java $(MAIN_CLASS)" and 7 | # compare the output to the contents of the file names F.out. 8 | # Report discrepencies. 9 | # clean: Remove all the .class files produced by java compilation, 10 | # all Emacs backup files, and testing output files. 11 | # 12 | # In other words, type 'make' to compile everything; 'make check' to 13 | # compile and test everything, and 'make clean' to clean things up. 14 | # 15 | # You can use this file without understanding most of it, of course, but 16 | # I strongly recommend that you try to figure it out, and where you cannot, 17 | # that you ask questions. 18 | 19 | JFLAGS = -g -Xlint:unchecked -Xlint:deprecation 20 | 21 | CLASSDIR = ../classes 22 | 23 | # See comment in ../Makefile 24 | PYTHON = python3 25 | 26 | RMAKE = "$(MAKE)" 27 | 28 | # A CLASSPATH value that (seems) to work on both Windows and Unix systems. 29 | # To Unix, it looks like ..:$(CLASSPATH):JUNK and to Windows like 30 | # JUNK;..;$(CLASSPATH). 31 | 32 | LIB = ../../library-sp21/javalib/* 33 | 34 | 35 | CPATH = "$(LIB):..:$(CLASSPATH):;$(LIB);..;$(CLASSPATH)" 36 | 37 | # All .java files in this directory. 38 | SRCS := $(wildcard *.java) 39 | 40 | .PHONY: default check clean 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 | check: 51 | $(RMAKE) -C .. PYTHON=$(PYTHON) check 52 | 53 | integration: 54 | $(RMAKE) -C .. PYTHON=$(PYTHON) integration 55 | 56 | # 'make clean' will clean up stuff you can reconstruct. 57 | clean: 58 | $(RM) *~ *.class sentinel 59 | 60 | ### DEPENDENCIES ### 61 | 62 | sentinel: $(SRCS) 63 | javac $(JFLAGS) -cp $(CPATH) $(SRCS) 64 | touch sentinel 65 | -------------------------------------------------------------------------------- /gitlet/Refs.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | import java.io.File; 4 | 5 | import static gitlet.Repository.*; 6 | import static gitlet.Utils.*; 7 | 8 | /** 9 | * Represent the reference point of HEAD, REMOTE and so on; 10 | */ 11 | public class Refs { 12 | /* The current working directory. */ 13 | public static final File CWD = new File(System.getProperty("user.dir")); 14 | /* The .gitlet directory. */ 15 | public static final File GITLET_DIR = join(CWD, ".gitlet"); 16 | 17 | /* the objects directory */ 18 | static final File OBJECTS_FOLDER = join(GITLET_DIR, "objects"); 19 | static final File COMMIT_FOLDER = join(OBJECTS_FOLDER, "commits"); 20 | static final File BLOBS_FOLDER = join(OBJECTS_FOLDER, "blobs"); 21 | 22 | /* The refs directory. */ 23 | public static final File REFS_DIR = join(GITLET_DIR, "refs"); 24 | public static final File HEAD_DIR = join(REFS_DIR, "heads"); 25 | 26 | /* the current .gitlet/HEAD file */ 27 | public static final File HEAD_POINT = join(REFS_DIR, "HEAD"); 28 | 29 | /* the stage directory */ 30 | public static final File ADD_STAGE_DIR = join(GITLET_DIR, "addstage"); 31 | public static final File REMOVE_STAGE_DIR = join(GITLET_DIR, "removestage"); 32 | 33 | 34 | /** 35 | * 创建一个文件:路径是join(HEAD_DIR, branchName) 36 | * 向其中写入hashName 37 | * 38 | * @param branchName: 此branch的名字 39 | * @param hashName: 写入branch的内容 40 | */ 41 | public static void saveBranch(String branchName, String hashName) { 42 | // save the file of the head of a given branch 43 | File branchHead = join(HEAD_DIR, branchName); 44 | writeContents(branchHead, hashName); 45 | 46 | } 47 | 48 | 49 | 50 | /** 51 | * 在HEAD文件中写入当前branch的hash值, 52 | * Save the point to HEAD into .gitlet/refs/HEAD folder 53 | * 54 | * @param branchHeadCommitHash 想要指向的commit的hashName,也就是写入HEAD的内容 55 | */ 56 | public static void saveHEAD(String branchName, String branchHeadCommitHash) { 57 | writeContents(HEAD_POINT, branchName + ":" + branchHeadCommitHash); 58 | } 59 | 60 | /** 61 | * 从HEAD文件中直接获取当前branch的名字 62 | * 63 | * @return 64 | */ 65 | public static String getHeadBranchName() { 66 | String headContent = readContentsAsString(HEAD_POINT); 67 | String[] splitContent = headContent.split(":"); 68 | String branchName = splitContent[0]; 69 | return branchName; 70 | } 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /gitlet/Main.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | import static gitlet.Repository.*; 4 | import static gitlet.Utils.message; 5 | import static java.lang.System.exit; 6 | 7 | /** 8 | * Driver class for Gitlet, a subset of the Git version-control system. 9 | * 10 | * @author thanyi 11 | */ 12 | public class Main { 13 | 14 | /** 15 | * Usage: java gitlet.Main ARGS, where ARGS contains 16 | * ... 17 | */ 18 | public static void main(String[] args) { 19 | // check if args is empty. 20 | checkArgsEmpty(args); 21 | String firstArg = args[0]; 22 | try { 23 | switch (firstArg) { 24 | case "init": 25 | if (args.length != 1) { 26 | throw new GitletException("Incorrect operands."); 27 | } 28 | initPersistence(); 29 | break; 30 | case "add": 31 | String addFileName = args[1]; 32 | addStage(addFileName); 33 | break; 34 | case "commit": 35 | String commitMsg = args[1]; 36 | commitFile(commitMsg); 37 | break; 38 | case "rm": 39 | String removeFile = args[1]; 40 | removeStage(removeFile); 41 | break; 42 | case "log": 43 | if (args.length != 1) { 44 | throw new GitletException("Incorrect operands."); 45 | } 46 | printLog(); 47 | break; 48 | case "global-log": 49 | if (args.length != 1) { 50 | throw new GitletException("Incorrect operands."); 51 | } 52 | printGlobalLog(); 53 | break; 54 | case "find": 55 | String findMsg = args[1]; 56 | findCommit(findMsg); 57 | break; 58 | case "status": 59 | if (args.length != 1) { 60 | throw new GitletException("Incorrect operands."); 61 | } 62 | showStatus(); 63 | break; 64 | 65 | case "checkout": 66 | if (args.length == 1) { 67 | throw new GitletException("Incorrect operands."); 68 | } 69 | checkOut(args); 70 | break; 71 | case "branch": 72 | if (args.length != 2) { 73 | throw new GitletException("Incorrect operands."); 74 | } 75 | createBranch(args[1]); 76 | break; 77 | case "rm-branch": 78 | // java gitlet.Main rm-branch [branch name] 79 | if (args.length != 2) { 80 | throw new GitletException("Incorrect operands."); 81 | } 82 | removeBranch(args[1]); 83 | break; 84 | case "reset": 85 | // java gitlet.Main reset [commit id] 86 | if (args.length != 2) { 87 | throw new GitletException("Incorrect operands."); 88 | } 89 | reset(args[1]); 90 | break; 91 | 92 | case "merge": 93 | if (args.length != 2) { 94 | throw new GitletException("Incorrect operands."); 95 | } 96 | mergeBranch(args[1]); 97 | break; 98 | default: 99 | throw new GitletException("No command with that name exists."); 100 | } 101 | } catch (GitletException e) { 102 | System.err.println(e.getMessage()); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !*/ 3 | !**.java 4 | !**.txt 5 | !**.json 6 | !**/pom.xml 7 | !.fatweleminus 8 | !.gitignore 9 | !library-sp21 10 | !foods.csv 11 | *.class 12 | *.log 13 | *.ctxt 14 | .mtj.tmp/ 15 | *.jar 16 | *.war 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | **/out/ 22 | hs_err_pid* 23 | *~ 24 | .fuse_hidden* 25 | .directory 26 | .Trash-* 27 | .nfs* 28 | *.DS_Store 29 | .AppleDouble 30 | .LSOverride 31 | Icon 32 | ._* 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | Thumbs.db 46 | ehthumbs.db 47 | ehthumbs_vista.db 48 | Desktop.ini 49 | $RECYCLE.BIN/ 50 | *.cab 51 | *.msi 52 | *.msm 53 | *.msp 54 | *.lnk 55 | 56 | 57 | *.pyc 58 | .DS_Store 59 | node_modules/ 60 | .ropeproject 61 | spec/ 62 | # Created by https://www.gitignore.io/api/java,python,osx,windows,intellij,eclipse,bluej,netbeans,vim,emacs,microsoftoffice 63 | 64 | ### Java ### 65 | *.class 66 | 67 | # Mobile Tools for Java (J2ME) 68 | .mtj.tmp/ 69 | 70 | # Package Files # 71 | *.jar 72 | *.war 73 | *.ear 74 | 75 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 76 | hs_err_pid* 77 | 78 | 79 | ### Python ### 80 | # Byte-compiled / optimized / DLL files 81 | __pycache__/ 82 | *.py[cod] 83 | *$py.class 84 | 85 | # C extensions 86 | *.so 87 | 88 | # Distribution / packaging 89 | .Python 90 | env/ 91 | build/ 92 | develop-eggs/ 93 | dist/ 94 | downloads/ 95 | eggs/ 96 | .eggs/ 97 | lib/ 98 | lib64/ 99 | parts/ 100 | sdist/ 101 | var/ 102 | *.egg-info/ 103 | .installed.cfg 104 | *.egg 105 | 106 | # PyInstaller 107 | # Usually these files are written by a python script from a template 108 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 109 | *.manifest 110 | *.spec 111 | 112 | # Installer logs 113 | pip-log.txt 114 | pip-delete-this-directory.txt 115 | 116 | # Unit test / coverage reports 117 | htmlcov/ 118 | .tox/ 119 | .coverage 120 | .coverage.* 121 | .cache 122 | nosetests.xml 123 | coverage.xml 124 | *,cover 125 | .hypothesis/ 126 | 127 | # Translations 128 | *.mo 129 | *.pot 130 | 131 | # Django stuff: 132 | *.log 133 | 134 | # Sphinx documentation 135 | docs/_build/ 136 | 137 | # PyBuilder 138 | target/ 139 | 140 | 141 | ### OSX ### 142 | .DS_Store 143 | .AppleDouble 144 | .LSOverride 145 | 146 | # Icon must end with two \r 147 | Icon 148 | 149 | 150 | # Thumbnails 151 | ._* 152 | 153 | # Files that might appear in the root of a volume 154 | .DocumentRevisions-V100 155 | .fseventsd 156 | .Spotlight-V100 157 | .TemporaryItems 158 | .Trashes 159 | .VolumeIcon.icns 160 | 161 | # Directories potentially created on remote AFP share 162 | .AppleDB 163 | .AppleDesktop 164 | Network Trash Folder 165 | Temporary Items 166 | .apdisk 167 | 168 | 169 | ### Windows ### 170 | # Windows image file caches 171 | Thumbs.db 172 | ehthumbs.db 173 | 174 | # Folder config file 175 | Desktop.ini 176 | 177 | # Recycle Bin used on file shares 178 | $RECYCLE.BIN/ 179 | 180 | # Windows Installer files 181 | *.cab 182 | *.msi 183 | *.msm 184 | *.msp 185 | 186 | # Windows shortcuts 187 | *.lnk 188 | 189 | 190 | ### Intellij ### 191 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 192 | 193 | *.iml 194 | 195 | ## Directory-based project format: 196 | .idea/ 197 | # if you remove the above rule, at least ignore the following: 198 | 199 | # User-specific stuff: 200 | # .idea/workspace.xml 201 | # .idea/tasks.xml 202 | # .idea/dictionaries 203 | # .idea/shelf 204 | 205 | # Sensitive or high-churn files: 206 | # .idea/dataSources.ids 207 | # .idea/dataSources.xml 208 | # .idea/sqlDataSources.xml 209 | # .idea/dynamic.xml 210 | # .idea/uiDesigner.xml 211 | 212 | # Gradle: 213 | # .idea/gradle.xml 214 | # .idea/libraries 215 | 216 | # Mongo Explorer plugin: 217 | # .idea/mongoSettings.xml 218 | 219 | ## File-based project format: 220 | *.ipr 221 | *.iws 222 | 223 | ## Plugin-specific files: 224 | 225 | # IntelliJ 226 | out/ 227 | 228 | # mpeltonen/sbt-idea plugin 229 | .idea_modules/ 230 | 231 | # JIRA plugin 232 | atlassian-ide-plugin.xml 233 | 234 | # Crashlytics plugin (for Android Studio and IntelliJ) 235 | com_crashlytics_export_strings.xml 236 | crashlytics.properties 237 | crashlytics-build.properties 238 | fabric.properties 239 | 240 | 241 | ### Eclipse ### 242 | *.pydevproject 243 | .metadata 244 | bin/ 245 | tmp/ 246 | *.tmp 247 | *.bak 248 | *.swp 249 | *~.nib 250 | local.properties 251 | .settings/ 252 | .loadpath 253 | 254 | # Eclipse Core 255 | .project 256 | 257 | # External tool builders 258 | .externalToolBuilders/ 259 | 260 | # Locally stored "Eclipse launch configurations" 261 | *.launch 262 | 263 | # CDT-specific 264 | .cproject 265 | 266 | # JDT-specific (Eclipse Java Development Tools) 267 | .classpath 268 | 269 | # Java annotation processor (APT) 270 | .factorypath 271 | 272 | # PDT-specific 273 | .buildpath 274 | 275 | # sbteclipse plugin 276 | .target 277 | 278 | # TeXlipse plugin 279 | .texlipse 280 | 281 | # STS (Spring Tool Suite) 282 | .springBeans 283 | 284 | 285 | ### bluej ### 286 | # BlueJ: A Java IDE for programming beginners. http://bluej.org 287 | 288 | # Editor preferences for a class etc, see http://lists.bluej.org/pipermail/bluej-discuss/2003-May/002351.html 289 | *.ctxt 290 | 291 | # Duplicate of project file, see http://www.hs-owl.de/fb5/labor/it/de/if1/vl/IF1JavaOhneBluej.pdf, page 4 292 | bluej.pkh 293 | 294 | ### NetBeans ### 295 | nbproject/private/ 296 | build/ 297 | nbbuild/ 298 | dist/ 299 | nbdist/ 300 | nbactions.xml 301 | .nb-gradle/ 302 | 303 | 304 | ### Vim ### 305 | [._]*.s[a-w][a-z] 306 | [._]s[a-w][a-z] 307 | *.un~ 308 | Session.vim 309 | .netrwhist 310 | *~ 311 | 312 | 313 | ### Emacs ### 314 | # -*- mode: gitignore; -*- 315 | *~ 316 | \#*\# 317 | /.emacs.desktop 318 | /.emacs.desktop.lock 319 | *.elc 320 | auto-save-list 321 | tramp 322 | .\#* 323 | 324 | # Org-mode 325 | .org-id-locations 326 | *_archive 327 | 328 | # flymake-mode 329 | *_flymake.* 330 | 331 | # eshell files 332 | /eshell/history 333 | /eshell/lastdir 334 | 335 | # elpa packages 336 | /elpa/ 337 | 338 | # reftex files 339 | *.rel 340 | 341 | # AUCTeX auto folder 342 | /auto/ 343 | 344 | # cask packages 345 | .cask/ 346 | 347 | 348 | ### MicrosoftOffice ### 349 | *.tmp 350 | 351 | # Word temporary 352 | ~$*.doc* 353 | 354 | # Excel temporary 355 | ~$*.xls* 356 | 357 | # Excel Backup File 358 | *.xlk 359 | 360 | # PowerPoint temporary 361 | ~$*.ppt* 362 | 363 | # Project 3 things 364 | #proj3/img 365 | #proj3/target 366 | #*.osm 367 | 368 | # Project 1B things 369 | # words 370 | 371 | # Capers Lab Things (SP21) 372 | !Makefile 373 | !lab6/testing/* 374 | !lab6/testing/our/* 375 | 376 | # Project 3 Image/Audio/Video Files 377 | !proj3/**/*.png 378 | !proj3/**/*.jpeg 379 | !proj3/**/*.jpg 380 | !proj3/**/*.mp3 381 | !proj3/**/*.mp4 382 | !proj3/**/*.gif 383 | !proj3/**/*.wav 384 | 385 | # Project Repo Image/Audio/Video Files 386 | !byow/**/*.png 387 | !byow/**/*.jpeg 388 | !byow/**/*.jpg 389 | !byow/**/*.mp3 390 | !byow/**/*.mp4 391 | !byow/**/*.gif 392 | !byow/**/*.wav 393 | -------------------------------------------------------------------------------- /gitlet/Commit.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | 4 | import java.io.File; 5 | import java.io.Serializable; 6 | import java.util.*; 7 | 8 | import static gitlet.Refs.*; 9 | import static gitlet.Repository.*; 10 | import static gitlet.Utils.*; 11 | import static java.lang.System.exit; 12 | 13 | /** 14 | * Represents a gitlet commit object. 15 | * does at a high level. 16 | * 17 | * @author ethanyi 18 | */ 19 | public class Commit implements Serializable { 20 | /** 21 | *

22 | * List all instance variables of the Commit class here with a useful 23 | * comment above them describing what that variable represents and how that 24 | * variable is used. We've provided one example for `message`. 25 | */ 26 | 27 | /* The message of this Commit. */ 28 | private String message; 29 | /* The parent of commit, null if it's the first commit */ 30 | private String directParent; 31 | private String otherParent; 32 | /* the timestamp of commit*/ 33 | private Date timestamp; 34 | /* the contents of commit files*/ 35 | private HashMap blobMap = new HashMap<>(); 36 | 37 | 38 | public Commit(String message, Date timestamp, String directparent, 39 | String blobFileName, String blobHashName) { 40 | this.message = message; 41 | this.timestamp = timestamp; 42 | this.directParent = directparent; 43 | if (blobFileName == null || blobFileName.isEmpty()) { 44 | this.blobMap = new HashMap<>(); 45 | } else { 46 | this.blobMap.put(blobFileName, blobHashName); 47 | } 48 | } 49 | 50 | public Commit(Commit directparent) { 51 | this.message = directparent.message; 52 | this.timestamp = directparent.timestamp; 53 | this.directParent = directparent.directParent; 54 | this.blobMap = directparent.blobMap; 55 | } 56 | 57 | 58 | 59 | 60 | /** 61 | * To save commit into files in COMMIT_FOLDER, persists the status of object. 62 | */ 63 | public void saveCommit() { 64 | // get the uid of this 65 | String hashname = this.getHashName(); 66 | 67 | // write obj to files 68 | File commitFile = new File(COMMIT_FOLDER, hashname); 69 | writeObject(commitFile, this); 70 | } 71 | 72 | 73 | /** 74 | * @param blobName blob的hashname 75 | */ 76 | public void addBlob(String fileName, String blobName) { 77 | this.blobMap.put(fileName, blobName); 78 | } 79 | 80 | public void removeBlob(String fileName) { 81 | this.blobMap.remove(fileName); 82 | } 83 | 84 | 85 | public String getHashName() { 86 | return sha1(this.message, dateToTimeStamp(this.timestamp), this.directParent); 87 | } 88 | 89 | public void setDirectParent(String directParent) { 90 | this.directParent = directParent; 91 | } 92 | 93 | public String getDirectParent() { 94 | return directParent; 95 | } 96 | 97 | public Date getTimestamp() { 98 | return timestamp; 99 | } 100 | 101 | public void setTimestamp(Date timestamp) { 102 | this.timestamp = timestamp; 103 | } 104 | 105 | 106 | public HashMap getBlobMap() { 107 | return blobMap; 108 | } 109 | 110 | public String getMessage() { 111 | return message; 112 | } 113 | 114 | public void setMessage(String message) { 115 | this.message = message; 116 | } 117 | 118 | 119 | public String getOtherParent() { 120 | return otherParent; 121 | } 122 | 123 | public void setOtherParent(String otherParent) { 124 | this.otherParent = otherParent; 125 | } 126 | /* ======================== 以上为getter和setter ======================*/ 127 | 128 | /** 129 | * 用于获取HEAD指针指向的Commit对象 130 | * 131 | * @return 132 | */ 133 | public static Commit getHeadCommit() { 134 | /* 获取HEAD指针,这个指针指向目前最新的commit */ 135 | String headContent = readContentsAsString(HEAD_POINT); 136 | String headHashName = headContent.split(":")[1]; 137 | File commitFile = join(COMMIT_FOLDER, headHashName); 138 | /* 获取commit文件 */ 139 | Commit commit = readObject(commitFile, Commit.class); 140 | 141 | return commit; 142 | 143 | } 144 | 145 | /** 146 | * 用于获取branches文件夹中分支文件指向的Commit对象 147 | * 148 | * @return 149 | */ 150 | public static Commit getBranchHeadCommit(String branchName, String errorMsg) { 151 | 152 | 153 | File brancheFile = join(HEAD_DIR, branchName); 154 | if (!brancheFile.exists()) { 155 | System.out.println(errorMsg); 156 | exit(0); 157 | } 158 | 159 | /* 获取HEAD指针,这个指针指向目前最新的commit */ 160 | String headHashName = readContentsAsString(brancheFile); 161 | 162 | 163 | File commitFile = join(COMMIT_FOLDER, headHashName); 164 | /* 获取commit文件 */ 165 | Commit commit = readObject(commitFile, Commit.class); 166 | 167 | return commit; 168 | 169 | } 170 | 171 | /** 172 | * 通过hashname来获取Commit对象 173 | * 174 | * @param hashName commit自己的hashName 175 | * @return 176 | */ 177 | public static Commit getCommit(String hashName) { 178 | List commitFiles = plainFilenamesIn(COMMIT_FOLDER); 179 | /* 如果在commit文件夹中不存在此文件 */ 180 | if (!commitFiles.contains(hashName)) { 181 | return null; 182 | } 183 | File commitFile = join(COMMIT_FOLDER, hashName); 184 | Commit commit = readObject(commitFile, Commit.class); 185 | return commit; 186 | } 187 | 188 | 189 | /** 190 | * 给定一个commitId,返回一个相对应的commit对象,若是没有这个commit对象,则返回null 191 | * 192 | * @param commitId 193 | * @return commit或者null 194 | */ 195 | public static Commit getCommitFromId(String commitId) { 196 | Commit commit = null; 197 | /* 查找对应的commit */ 198 | 199 | /* 直接从commit文件夹中依次寻找 */ 200 | String resCommitId = null; 201 | List commitFileNames = plainFilenamesIn(COMMIT_FOLDER); 202 | /* 用于应对前缀的情况 */ 203 | for (String commitFileName : commitFileNames) { 204 | if (commitFileName.startsWith(commitId)) { 205 | resCommitId = commitFileName; 206 | break; 207 | } 208 | } 209 | 210 | if (resCommitId == null) { 211 | return null; 212 | } else { 213 | File commitFile = join(COMMIT_FOLDER, resCommitId); 214 | commit = readObject(commitFile, Commit.class); 215 | } 216 | 217 | return commit; 218 | } 219 | 220 | /** 221 | * 获取两个分支的共同节点,仅从directParents搜索 222 | * 223 | * @param commitA 224 | * @param commitB 225 | * @return 226 | */ 227 | public static Commit getSplitCommit(Commit commitA, Commit commitB) { 228 | 229 | Commit p1 = commitA, p2 = commitB; 230 | /* 用于遍历提交链 */ 231 | Deque dequecommitA = new ArrayDeque<>(); 232 | Deque dequecommitB = new ArrayDeque<>(); 233 | /* 用于保存访问过的节点 */ 234 | HashSet visitedInCommitA = new HashSet<>(); 235 | HashSet visitedInCommitB = new HashSet<>(); 236 | 237 | dequecommitA.add(p1); 238 | dequecommitB.add(p2); 239 | 240 | while (!dequecommitA.isEmpty() || !dequecommitB.isEmpty()) { 241 | if (!dequecommitA.isEmpty()) { 242 | /* commitA 的队列中存在可遍历对象 */ 243 | Commit currA = dequecommitA.poll(); 244 | if (visitedInCommitB.contains(currA.getHashName())) { 245 | return currA; 246 | } 247 | visitedInCommitA.add(currA.getHashName()); 248 | addParentsToDeque(currA, dequecommitA); 249 | } 250 | 251 | if (!dequecommitB.isEmpty()) { 252 | Commit currB = dequecommitB.poll(); 253 | if (visitedInCommitA.contains(currB.getHashName())) { 254 | return currB; 255 | } 256 | visitedInCommitB.add(currB.getHashName()); 257 | addParentsToDeque(currB, dequecommitB); 258 | } 259 | } 260 | 261 | 262 | // 如果没有找到,就是null 263 | return null; 264 | 265 | } 266 | 267 | /** 268 | * 将此节点的父节点(或者是两个父节点)放入队列中 269 | * 270 | * @param commit 271 | * @param dequeCommit 272 | */ 273 | private static void addParentsToDeque(Commit commit, Queue dequeCommit) { 274 | if (!commit.getDirectParent().isEmpty()) { 275 | dequeCommit.add(getCommitFromId(commit.getDirectParent())); 276 | } 277 | 278 | if (commit.getOtherParent() != null) { 279 | dequeCommit.add(getCommitFromId(commit.getOtherParent())); 280 | } 281 | } 282 | 283 | } 284 | -------------------------------------------------------------------------------- /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.Paths; 14 | import java.nio.charset.StandardCharsets; 15 | import java.security.MessageDigest; 16 | import java.security.NoSuchAlgorithmException; 17 | import java.util.Arrays; 18 | import java.util.Formatter; 19 | import java.util.List; 20 | 21 | 22 | /** Assorted utilities. 23 | * 24 | * Give this file a good read as it provides several useful utility functions 25 | * to save you some time. 26 | * 27 | * @author P. N. Hilfinger 28 | */ 29 | class Utils { 30 | 31 | /** The length of a complete SHA-1 UID as a hexadecimal numeral. */ 32 | static final int UID_LENGTH = 40; 33 | 34 | /* SHA-1 HASH VALUES. */ 35 | 36 | /** Returns the SHA-1 hash of the concatenation of VALS, which may 37 | * be any mixture of byte arrays and Strings. */ 38 | static String sha1(Object... vals) { 39 | try { 40 | MessageDigest md = MessageDigest.getInstance("SHA-1"); 41 | for (Object val : vals) { 42 | if (val instanceof byte[]) { 43 | md.update((byte[]) val); 44 | } else if (val instanceof String) { 45 | md.update(((String) val).getBytes(StandardCharsets.UTF_8)); 46 | } else { 47 | throw new IllegalArgumentException("improper type to sha1"); 48 | } 49 | } 50 | Formatter result = new Formatter(); 51 | for (byte b : md.digest()) { 52 | result.format("%02x", b); 53 | } 54 | return result.toString(); 55 | } catch (NoSuchAlgorithmException excp) { 56 | throw new IllegalArgumentException("System does not support SHA-1"); 57 | } 58 | } 59 | 60 | /** Returns the SHA-1 hash of the concatenation of the strings in 61 | * VALS. */ 62 | static String sha1(List vals) { 63 | return sha1(vals.toArray(new Object[vals.size()])); 64 | } 65 | 66 | /* FILE DELETION */ 67 | 68 | /** Deletes FILE if it exists and is not a directory. Returns true 69 | * if FILE was deleted, and false otherwise. Refuses to delete FILE 70 | * and throws IllegalArgumentException unless the directory designated by 71 | * FILE also contains a directory named .gitlet. */ 72 | static boolean restrictedDelete(File file) { 73 | if (!(new File(file.getParentFile(), ".gitlet")).isDirectory()) { 74 | throw new IllegalArgumentException("not .gitlet working directory"); 75 | } 76 | if (!file.isDirectory()) { 77 | return file.delete(); 78 | } else { 79 | return false; 80 | } 81 | } 82 | 83 | /** Deletes the file named FILE if it exists and is not a directory. 84 | * Returns true if FILE was deleted, and false otherwise. Refuses 85 | * to delete FILE and throws IllegalArgumentException unless the 86 | * directory designated by FILE also contains a directory named .gitlet. */ 87 | static boolean restrictedDelete(String file) { 88 | return restrictedDelete(new File(file)); 89 | } 90 | 91 | /* READING AND WRITING FILE CONTENTS */ 92 | 93 | /** Return the entire contents of FILE as a byte array. FILE must 94 | * be a normal file. Throws IllegalArgumentException 95 | * in case of problems. */ 96 | static byte[] readContents(File file) { 97 | if (!file.isFile()) { 98 | throw new IllegalArgumentException("must be a normal file"); 99 | } 100 | try { 101 | return Files.readAllBytes(file.toPath()); 102 | } catch (IOException excp) { 103 | throw new IllegalArgumentException(excp.getMessage()); 104 | } 105 | } 106 | 107 | /** Return the entire contents of FILE as a String. FILE must 108 | * be a normal file. Throws IllegalArgumentException 109 | * in case of problems. */ 110 | static String readContentsAsString(File file) { 111 | return new String(readContents(file), StandardCharsets.UTF_8); 112 | } 113 | 114 | /** Write the result of concatenating the bytes in CONTENTS to FILE, 115 | * creating or overwriting it as needed. Each object in CONTENTS may be 116 | * either a String or a byte array. Throws IllegalArgumentException 117 | * in case of problems. */ 118 | static void writeContents(File file, Object... contents) { 119 | try { 120 | if (file.isDirectory()) { 121 | throw 122 | new IllegalArgumentException("cannot overwrite directory"); 123 | } 124 | BufferedOutputStream str = 125 | new BufferedOutputStream(Files.newOutputStream(file.toPath())); 126 | for (Object obj : contents) { 127 | if (obj instanceof byte[]) { 128 | str.write((byte[]) obj); 129 | } else { 130 | str.write(((String) obj).getBytes(StandardCharsets.UTF_8)); 131 | } 132 | } 133 | str.close(); 134 | } catch (IOException | ClassCastException excp) { 135 | throw new IllegalArgumentException(excp.getMessage()); 136 | } 137 | } 138 | 139 | /** Return an object of type T read from FILE, casting it to EXPECTEDCLASS. 140 | * Throws IllegalArgumentException in case of problems. */ 141 | static T readObject(File file, 142 | Class expectedClass) { 143 | try { 144 | ObjectInputStream in = 145 | new ObjectInputStream(new FileInputStream(file)); 146 | T result = expectedClass.cast(in.readObject()); 147 | in.close(); 148 | return result; 149 | } catch (IOException | ClassCastException 150 | | ClassNotFoundException excp) { 151 | throw new IllegalArgumentException(excp.getMessage()); 152 | } 153 | } 154 | 155 | /** Write OBJ to FILE. */ 156 | static void writeObject(File file, Serializable obj) { 157 | writeContents(file, serialize(obj)); 158 | } 159 | 160 | /* DIRECTORIES */ 161 | 162 | /** Filter out all but plain files. */ 163 | private static final FilenameFilter PLAIN_FILES = 164 | new FilenameFilter() { 165 | @Override 166 | public boolean accept(File dir, String name) { 167 | return new File(dir, name).isFile(); 168 | } 169 | }; 170 | 171 | /** Returns a list of the names of all plain files in the directory DIR, in 172 | * lexicographic order as Java Strings. Returns null if DIR does 173 | * not denote a directory. */ 174 | static List plainFilenamesIn(File dir) { 175 | String[] files = dir.list(PLAIN_FILES); 176 | if (files == null) { 177 | return null; 178 | } else { 179 | Arrays.sort(files); 180 | return Arrays.asList(files); 181 | } 182 | } 183 | 184 | /** Returns a list of the names of all plain files in the directory DIR, in 185 | * lexicographic order as Java Strings. Returns null if DIR does 186 | * not denote a directory. */ 187 | static List plainFilenamesIn(String dir) { 188 | return plainFilenamesIn(new File(dir)); 189 | } 190 | 191 | /* OTHER FILE UTILITIES */ 192 | 193 | /** Return the concatentation of FIRST and OTHERS into a File designator, 194 | * analogous to the {@link java.nio.file.Paths#get(String, String[])} 195 | * method. */ 196 | static File join(String first, String... others) { 197 | return Paths.get(first, others).toFile(); 198 | } 199 | 200 | /** Return the concatentation of FIRST and OTHERS into a File designator, 201 | * analogous to the {@link java.nio.file.Paths#get(String, String[])} 202 | * method. */ 203 | static File join(File first, String... others) { 204 | return Paths.get(first.getPath(), others).toFile(); 205 | } 206 | 207 | 208 | /* SERIALIZATION UTILITIES */ 209 | 210 | /** Returns a byte array containing the serialized contents of OBJ. */ 211 | static byte[] serialize(Serializable obj) { 212 | try { 213 | ByteArrayOutputStream stream = new ByteArrayOutputStream(); 214 | ObjectOutputStream objectStream = new ObjectOutputStream(stream); 215 | objectStream.writeObject(obj); 216 | objectStream.close(); 217 | return stream.toByteArray(); 218 | } catch (IOException excp) { 219 | throw error("Internal error serializing commit."); 220 | } 221 | } 222 | 223 | 224 | 225 | /* MESSAGES AND ERROR REPORTING */ 226 | 227 | /** Return a GitletException whose message is composed from MSG and ARGS as 228 | * for the String.format method. */ 229 | static GitletException error(String msg, Object... args) { 230 | return new GitletException(String.format(msg, args)); 231 | } 232 | 233 | /** Print a message composed from MSG and ARGS as for the String.format 234 | * method, followed by a newline. */ 235 | static void message(String msg, Object... args) { 236 | System.out.printf(msg, args); 237 | System.out.println(); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gitlet Design Document 2 | 3 | 这是针对CS61B: Data Structures, Spring 2021版本的gitlet构建,Java 版本控制系统 Git 的独立实现。Gitlet 支持 Git 的大多数本地功能:添加、提交、签出、日志,以及分支操作,包括 merge 和 rebase 4 | 5 | ### **Name**: ethanyi9 6 | 7 | 目前所有本地指令构建均已完成 8 | 9 | 10 | ## Classes and Data Structures 11 | 12 | ### Class 1: Commit 13 | 14 | 对于需要**提交**的文件的类,此类使用最多,每次进行处理的时候都会构建commit对象 15 | 16 | #### Instance Variables 17 | 18 | - `String message`: 保存commit提交的评论 19 | - `Date timestamp`: 提交时间,第一个是 `Date(0)`,根据Date对象进行 20 | - `String directParent`: 这个commit的上一个commit。 21 | - `String otherParent`:若是存在`merge`操作,则会使用此变量,记录`merge [branchName]` 中的`branch commit`为上一个节点 22 | - `HashMap blobMap`: 文件内容的hashMap,key为track文件的文件名,value是其对应的blob的hash名 23 | 24 | #### Methods 25 | getter和setter方法不做讲解,讲解其中其余的方法以及其他类可用的静态方法 26 | 27 | 成员方法: 28 | - `getHashName`: 获取commit的sha-1 hash值,sha-1包括的内容是message, timestamp, directParent 29 | - `saveCommit()`: 将对象保存进`join(COMMIT_FOLDER, hashname)`中,文件名为commit的hash名 30 | - `addBlob(String fileName, String blobName)`:保存blobMap中键值对 31 | - `removeBlob(String fileName)`:删除blobMap中的指定键值对 32 | 33 | 静态方法: 34 | - `getHeadCommit()`:用于获取HEAD指针指向的Commit对象 35 | - `getBranchHeadCommit(String branchName, String error_msg)`:用于获取branches文件夹中分支文件指向的Commit对象,error_msg参数是当不存在此branch时需要提供的错误信息 36 | - `getCommit(String hashName)`:通过hashname来获取Commit对象,如果在commit文件夹中不存在此文件则返回null 37 | - `getCommitFromId(String commitId)`:给定一个commitId,返回一个相对应的commit对象,若是没有这个commit对象,则返回null,与getCommit()的区别是支持前缀搜索 38 | - `getSplitCommit(Commit commitA, Commit commitB)`:使用BFS方法查找commitA和commitB的最近的split Commit,不知道什么是split Commit的请翻阅文档 39 | 40 | ### Class 2 Refs 41 | 42 | 关于文件指针的类 43 | #### Instance Variables 44 | 45 | - `REFS_DIR`: ".gitlet/refs"文件夹 46 | - `HEAD_DIR`: ".gitlet/refs/heads" 文件夹 47 | - `HEAD_CONTENT_PATH`: ".gitlet/HEAD" 文件 48 | 49 | ... 50 | 51 | 52 | #### Methods 53 | - `saveBranch(String branchName, String hashName)`: 创建一个文件:路径是`join(HEAD_DIR, branchName)`,向其中写入`hashName`,也就是`commitId` 54 | - `saveHEAD(String branchName, String branchHeadCommitHash)`: 在HEAD文件中写入当前branch的hash值,格式是`branchName + ":" + branchHeadCommitHash` 55 | - `getHeadBranchName()`:从HEAD文件中直接获取当前branch的名字 56 | 57 | ### Class 3 Blob 58 | 用于Blob存储相关的类 59 | #### Instance Variables 60 | - `private String content`: blob中保存的内容 61 | - `public File filePath`: blob文件的自身路径 62 | - `private String hashName`: blob文件名,以hash为值 63 | #### Methods 64 | 65 | - `saveBlob()`: 将blob对象保存进 BLOB_FOLDER文件,内容就是blob文件的content 66 | 67 | 静态方法: 68 | - `getBlobContentFromName(String blobName)`:根据blobName获取到Blob的内容,其中blobName是一个hash值,若是没有这个文件,返回null 69 | - `overWriteFileWithBlob(File file, String content)`:将blob.content中的内容覆盖进file文件中 70 | 71 | 72 | 73 | 74 | ## Algorithms 75 | 76 | ### init 77 | 78 | `java gitlet.Main init` 79 | 80 | 创建一个文件夹环境 81 | 82 | ``` 83 | .gitlet (folder) 84 | |── objects (folder) // 存储commit对象文件 85 | |-- commits 86 | |-- blobs 87 | |── refs (folder) 88 | |── heads (folder) //指向目前的branch 89 | |-- master (file) 90 | |-- other file //表示其他分支的路径 91 | |-- HEAD (file) // 保存HEAD指针的对应hashname 92 | |-- addstage (folder) // 暂存区文件夹 93 | |-- removestage (folder) 94 | ``` 95 | 96 | ### add 97 | 98 | `java gitlet.Main add [file name]` 99 | 100 | 将指定的文件放入addstage文件夹中,将文件内容创建为blob文件,以内容的hash值作为文件名保存在objects/blobs文件夹中 101 | 102 | 将当前存在的文件副本添加到暂存stage区域。 103 | 104 | 暂存已暂存的文件会用新内容覆盖暂存区域中的上一个条目。 105 | 暂存区域应位于 .gitlet 中的某个位置。 106 | 107 | 如果文件的当前工作版本与当前commit中的版本相同,请不要暂存要添加的文件, 108 | 如果它已经存在,将其从暂存区域中删除(当文件被更改、添加,然后更改回其原始版本时,可能会发生这种情况)。 109 | ``` 110 | .gitlet (folder) 111 | |── objects (folder) 112 | |-- commits 113 | |-- blobs 114 | |-- <----- 加入的file.txt文件内容 115 | |── refs (folder) 116 | |── heads (folder) 117 | |-- master (file) 118 | |-- other file 119 | |-- HEAD (file) 120 | |-- addstage (folder) 121 | |-- file.txt <----- 保存blob文件的路径 122 | |-- removestage (folder) 123 | 124 | file.txt <----- 加入的文件 125 | ``` 126 | 127 | ### commit 128 | 129 | `java gitlet.Main commit [message]` 130 | 131 | 将addstage和removestage中的文件一个个进行响应操作,addStage中的进行添加,removeStage中的进行删除 132 | 133 | 将跟踪文件的快照保存在当前提交和暂存区域中,以便以后可以恢复它们,从而创建新提交。 134 | 135 | 提交将仅更新它正在跟踪的文件的内容,这些文件在提交时已暂存以进行添加, 136 | 137 | 在这种情况下,提交现在将包含暂存的文件版本,而不是从其父级获取的版本。提交将保存并开始跟踪任何已暂存以供添加但未被其父级跟踪的文件。 138 | 139 | 最后,在当前提交中跟踪的文件可能会在新提交中被取消跟踪,因为 rm 命令会暂存以将其删除(如下)。 140 | 141 | ``` 142 | .gitlet (folder) 143 | |── objects (folder) 144 | |-- commits 145 | | -- <----- 添加进的commit文件,内容是对应的blob文件名 146 | |-- blobs 147 | |-- 148 | |── refs (folder) 149 | |── heads (folder) 150 | |-- master (file) 151 | |-- other file 152 | |-- HEAD (file) 153 | |-- addstage (folder) 154 | |-- file.txt 155 | |-- removestage (folder) 156 | file.txt <----- commit的文件 157 | ``` 158 | 159 | 160 | ### rm 161 | 162 | `java gitlet.Main rm [file name]` 163 | 164 | 如果文件当前已暂存以进行添加,请取消暂存文件。 165 | 166 | 如果在当前提交中跟踪了该文件,请暂存该文件以供删除,如果用户尚未从工作目录中删除该文件,则从工作目录删除从此文件。 167 | 168 | (如果在commit中跟踪此文件,则可以对它remove,但如果没有跟踪就不能删除) 169 | 170 | 171 | ``` 172 | .gitlet (folder) 173 | |── objects (folder) 174 | |-- commits 175 | | -- 176 | |-- blobs 177 | |-- 178 | |── refs (folder) 179 | |── heads (folder) 180 | |-- master (file) 181 | |-- other file 182 | |-- HEAD (file) 183 | |-- addstage (folder) <----- 若是在addstage中有则删除 184 | |-- removestage (folder) 185 | |-- file.txt <----- 添加 186 | file.txt <----- 若是在被track状态,则进行删除;若不是在track,就不能删除 187 | ``` 188 | 189 | 190 | ### log 191 | 192 | `java gitlet.Main log` 193 | 194 | 输出log,内容是从当前HEAD指向的commit以及其所有的parents 195 | 格式如下: 196 | ```shell 197 | === 198 | commit a0da1ea5a15ab613bf9961fd86f010cf74c7ee48 199 | Date: Thu Nov 9 20:00:05 2017 -0800 200 | A commit message. 201 | 202 | === 203 | commit 3e8bf1d794ca2e9ef8a4007275acf3751c7170ff 204 | Date: Thu Nov 9 17:01:33 2017 -0800 205 | Another commit message. 206 | 207 | === 208 | commit e881c9575d180a215d1a636545b8fd9abfb1d2bb 209 | Date: Wed Dec 31 16:00:00 1969 -0800 210 | initial commit 211 | 212 | ``` 213 | 214 | ### global-log 215 | 216 | `java gitlet.Main global-log` 217 | 218 | 输出所有的commit文件 219 | 220 | 221 | ### find 222 | 223 | `java gitlet.Main find [commit message]` 224 | 225 | 输出所有的commit文件 226 | 227 | 打印出具有给定提交消息的所有提交的 ID,每行一个。 228 | 229 | 如果有多个这样的提交,它会在单独的行上打印 id。 230 | 231 | 提交消息是单个操作数;例如 `java gitlet.Main find "initial commit"` 232 | 233 | ### status 234 | 235 | `java gitlet.Main status` 236 | 237 | 显示当前存在的分支,并用 * 标记当前分支。还显示已暂存以进行添加或删除的文件。格式如下: 238 | 239 | ```shell 240 | === Branches === 241 | *master 242 | other-branch 243 | 244 | === Staged Files === 245 | wug.txt 246 | wug2.txt 247 | 248 | === Removed Files === 249 | goodbye.txt 250 | 251 | === Modifications Not Staged For Commit === 252 | junk.txt (deleted) 253 | wug3.txt (modified) 254 | 255 | === Untracked Files === 256 | random.stuff 257 | ``` 258 | 后面两行算是附加分,若是不想实现可以先用空白代替 259 | 260 | 261 | ### checkout 262 | 263 | `java gitlet.Main checkout -- [file name]` 264 | 265 | 获取 head commit 中存在的文件版本,并将其放入工作目录中,覆盖已经存在的文件版本(如果有)。文件的新版本不会暂存。 266 | 267 | `java gitlet.Main checkout [commit id] -- [file name]` 268 | 269 | 获取提交中具有给定 ID 的文件版本,并将其放入工作目录中,覆盖已经存在的文件版本(如果有)。文件的新版本不会暂存。 270 | 271 | `java gitlet.Main checkout [branch name]` 272 | 273 | 获取给定分支 head 处提交中的所有文件,并将它们放在工作目录中,覆盖已经存在的文件版本(如果存在)。此外,在此命令结束时,给定的分支现在将被视为当前分支 (HEAD)。在当前分支中跟踪但不存在于签出分支中的任何文件都将被删除。除非签出的分支是当前分支,否则暂存区域将被清除 274 | 275 | 276 | ### branch 277 | 278 | `java gitlet.Main branch [branch name]` 279 | 280 | 创建一个具有给定名称的新分支,并将其指向当前头部提交。分支只不过是对提交节点的引用(SHA-1 标识符)的名称。此命令不会立即切换到新创建的分支(就像在真实的 Git 中一样)。在调用 branch 之前,您的代码应该使用名为 “master” 的默认分支运行。 281 | 282 | ``` 283 | .gitlet (folder) 284 | |── objects (folder) 285 | |-- commits 286 | | -- 287 | |-- blobs 288 | |-- 289 | |── refs (folder) 290 | |── heads (folder) 291 | |-- master (file) 292 | |-- other file <----- 指向当前头部提交 293 | |-- HEAD (file) 294 | |-- addstage (folder) 295 | |-- removestage (folder) 296 | file.txt 297 | ``` 298 | 299 | ### rm-branch 300 | 301 | `java gitlet.Main rm-branch [branch name]` 302 | 303 | 删除具有给定名称的分支。这仅意味着删除与分支关联的指针(也就是文件);它并不意味着删除在分支下创建的所有提交,或类似的东西。 304 | 305 | ``` 306 | .gitlet (folder) 307 | |── objects (folder) 308 | |-- commits 309 | | -- 310 | |-- blobs 311 | |-- 312 | |── refs (folder) 313 | |── heads (folder) 314 | |-- master (file) 315 | |-- other file <----- 将此文件删除 316 | |-- HEAD (file) 317 | |-- addstage (folder) 318 | |-- removestage (folder) 319 | file.txt 320 | ``` 321 | 322 | 323 | ### reset 324 | `java gitlet.Main reset [commit id]` 325 | 326 | checkout到给定提交跟踪的所有文件。 327 | 删除`[commit id]`中不存在的跟踪文件。 328 | 此外,将当前分支的 head 移动到该提交节点`[commit id]`。 329 | 330 | `[commit id]`可以缩写为 checkout。暂存区域已清理。 331 | 该命令本质上是签出一个任意提交,该提交也会更改当前分支head。 332 | 333 | ### merge 334 | 335 | `java gitlet.Main merge [branch name]` 336 | 337 | 将提交的`[branch name]`合并至当前分支 338 | 339 | 情况有以下几种: 340 | 1. other:被修改 HEAD:未被修改 ---> working DIR: other, 并且需要被add 341 | 2. other:未被修改 HEAD:被修改 ---> working DIR: HEAD 342 | 3. other:被修改 HEAD:被修改 ---> 343 | 1. (一致的修改) working DIR: HEAD, 相当于什么都不做 344 | 2. (不一致的修改) working DIR: Conflict 345 | 4. split:不存在 other:不存在 HEAD:被添加 ---> working DIR: HEAD 346 | 5. split:不存在 other:被添加 HEAD:不存在 ---> working DIR: other, 并且需要被add 347 | 6. other:被删除 HEAD:未被修改 ---> working DIR: 被删除,同时被暂存于removal 348 | 7. other:未被修改 HEAD:被删除 ---> working DIR: 被删除 349 | 350 | ## Persistence 351 | ``` 352 | .gitlet (folder) 353 | |── objects (folder) 354 | |-- commits 355 | | -- 356 | |-- blobs 357 | |-- 358 | |── refs (folder) 359 | |── heads (folder) 360 | |-- master (file) 361 | |-- other file 362 | |-- HEAD (file) 363 | |-- addstage (folder) 364 | |-- removestage (folder) 365 | file.txt 366 | ``` 367 | 368 | ## Usage 369 | 370 | 编译 371 | ```shell 372 | $ make 373 | ``` 374 | 375 | 进行check检测 376 | 377 | ```shell 378 | $ make check 379 | ``` 380 | 381 | 使用相关指令 382 | 383 | ``` 384 | java gitlet.Main init 385 | 386 | java gitlet.Main add [file name] 387 | 388 | ... 389 | 390 | ``` 391 | 392 | 393 | 394 | 395 | 396 | -------------------------------------------------------------------------------- /gitlet/Repository.java: -------------------------------------------------------------------------------- 1 | package gitlet; 2 | 3 | import java.io.File; 4 | import java.text.DateFormat; 5 | import java.text.SimpleDateFormat; 6 | import java.util.*; 7 | 8 | import static gitlet.Commit.*; 9 | import static gitlet.Refs.*; 10 | import static gitlet.Utils.*; 11 | import static gitlet.Blob.*; 12 | import static java.lang.System.exit; 13 | 14 | 15 | /** 16 | * Represents a gitlet repository. 17 | * does at a high level. 18 | * 19 | * @author ethanyi 20 | */ 21 | public class Repository { 22 | /** 23 | *

24 | * List all instance variables of the Repository class here with a useful 25 | * comment above them describing what that variable represents and how that 26 | * variable is used. We've provided two examples for you. 27 | *

28 | *

29 | * the path we created as below: 30 | *

31 | * .gitlet (folder) 32 | * |── objects (folder) // 存储commit对象文件 33 | * |-- commits 34 | * |-- blobs 35 | * |── refs (folder) 36 | * |── heads (folder) //指向目前的branch 37 | * |-- master (file) 38 | * |-- other file //表示其他分支的路径 39 | * |-- HEAD (file) // 保存HEAD指针的对应hashname 40 | * |-- addstage (folder) // 暂存区文件夹 41 | * |-- removestage (folder) 42 | */ 43 | 44 | 45 | /** 46 | * initialize the folder instructure 47 | */ 48 | public static void setupPersistence() { 49 | GITLET_DIR.mkdirs(); 50 | COMMIT_FOLDER.mkdirs(); 51 | BLOBS_FOLDER.mkdirs(); 52 | REFS_DIR.mkdirs(); 53 | HEAD_DIR.mkdirs(); 54 | ADD_STAGE_DIR.mkdirs(); 55 | REMOVE_STAGE_DIR.mkdirs(); 56 | 57 | } 58 | 59 | /** 60 | * Check if the ARGS of java gitlet.Main is empty 61 | * 62 | * @param args 63 | */ 64 | public static void checkArgsEmpty(String[] args) { 65 | if (args.length == 0) { 66 | System.out.println("Please enter a command."); 67 | exit(0); 68 | } 69 | } 70 | 71 | /** 72 | * To get Date obj a format to transform the object to String. 73 | * 74 | * @param date a Date obj 75 | * @return timestamp in standrad format 76 | */ 77 | public static String dateToTimeStamp(Date date) { 78 | DateFormat dateFormat = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy Z", Locale.US); 79 | return dateFormat.format(date); 80 | } 81 | 82 | 83 | // /** 84 | // * To save some Commit objects into files . 85 | // * 86 | // * @apiNote 暂时没有用到 87 | // */ 88 | // public void saveObject2File(File path, Commit obj) { 89 | // // get the uid of this 90 | // String hashname = obj.getHashName(); 91 | // 92 | // // write obj to files 93 | // File commitFile = new File(COMMIT_FOLDER, hashname); 94 | // writeObject(commitFile, obj); 95 | // } 96 | 97 | 98 | public static void printCommitLog(Commit commit) { 99 | System.out.println("==="); 100 | System.out.println("commit " + commit.getHashName()); 101 | System.out.println("Date: " + dateToTimeStamp(commit.getTimestamp())); 102 | System.out.println(commit.getMessage()); 103 | System.out.print("\n"); 104 | } 105 | 106 | /** 107 | * @param field 打印的标题区域 108 | * @param files 文件夹中的所有文件 109 | * @param branchName 指定的branchName 110 | */ 111 | public static void printStatusPerField(String field, Collection files, 112 | String branchName) { 113 | System.out.println("=== " + field + " ==="); 114 | if (field.equals("Branches")) { 115 | for (var file : files) { 116 | // 如果是head文件 117 | if (file.equals(branchName)) { 118 | System.out.println("*" + file); 119 | } else { 120 | System.out.println(file); 121 | } 122 | } 123 | } else { 124 | for (var file : files) { 125 | System.out.println(file); 126 | } 127 | } 128 | 129 | System.out.print("\n"); 130 | } 131 | 132 | 133 | /** 134 | * 对Modifications Not Staged For Commit这个领域的输出 135 | * 136 | * @param field 打印的标题区域 137 | * @param modifiedFiles 标记为modified的文件 138 | * @param deletedFiles 标记为deleted的文件 139 | */ 140 | public static void printStatusWithStatus(String field, Collection modifiedFiles, 141 | Collection deletedFiles) { 142 | System.out.println("=== " + field + " ==="); 143 | 144 | for (var file : modifiedFiles) { 145 | System.out.println(file + " " + "(modified)"); 146 | } 147 | for (var file : deletedFiles) { 148 | System.out.println(file + " " + "(deleted)"); 149 | } 150 | 151 | System.out.print("\n"); 152 | } 153 | 154 | 155 | public static boolean untrackFileExists(Commit commit) { 156 | List workFileNames = plainFilenamesIn(CWD); 157 | Set currTrackSet = commit.getBlobMap().keySet(); 158 | /* 先检测CWD中是否存在未被current branch跟踪的文件 */ 159 | 160 | for (String workFile : workFileNames) { 161 | if (!currTrackSet.contains(workFile)) { 162 | return true; 163 | } 164 | } 165 | return false; 166 | } 167 | 168 | 169 | /** 170 | * 用于处理两边文件都被修改的情况 171 | * 172 | * @param headCommit 173 | * @param otherHeadCommit 174 | */ 175 | public static void processConflict(Commit headCommit, Commit otherHeadCommit, 176 | String splitTrackName) { 177 | String otherBlobFile = ""; 178 | String otherBlobContent = ""; 179 | 180 | String headBlobFile = ""; 181 | String headBlobContent = ""; 182 | 183 | HashMap headCommitBolbMap = headCommit.getBlobMap(); 184 | HashMap otherHeadCommitBolbMap = otherHeadCommit.getBlobMap(); 185 | 186 | /* 打印冲突 */ 187 | message("Encountered a merge conflict."); 188 | /* 获取*/ 189 | if (otherHeadCommitBolbMap.containsKey(splitTrackName)) { 190 | otherBlobFile = otherHeadCommitBolbMap.get(splitTrackName); 191 | otherBlobContent = getBlobContentFromName(otherBlobFile); 192 | } 193 | 194 | if (headCommitBolbMap.containsKey(splitTrackName)) { 195 | headBlobFile = headCommitBolbMap.get(splitTrackName); 196 | headBlobContent = getBlobContentFromName(headBlobFile); 197 | } 198 | 199 | /* 修改workFile中的内容*/ 200 | StringBuilder resContent = new StringBuilder(); 201 | resContent.append("<<<<<<< HEAD\n"); 202 | resContent.append(headBlobContent); 203 | resContent.append("=======" + "\n"); 204 | resContent.append(otherBlobContent); 205 | resContent.append(">>>>>>>" + "\n"); 206 | 207 | String resContentString = resContent.toString(); 208 | writeContents(join(CWD, splitTrackName), resContentString); 209 | addStage(splitTrackName); 210 | } 211 | 212 | /* ---------------------- 功能函数实现 --------------------- */ 213 | 214 | /** 215 | * java gitlet.Main init 216 | */ 217 | public static void initPersistence() { 218 | // if .gitlet dir existed 219 | if (GITLET_DIR.exists()) { 220 | System.out.println("A Gitlet version-control " 221 | + "system already exists in the current directory."); 222 | exit(0); 223 | } 224 | // create the folders in need 225 | setupPersistence(); 226 | // create timestamp,Commit and save commit into files 227 | Date timestampInit = new Date(0); 228 | Commit initialCommit = new Commit("initial commit", timestampInit, 229 | "", null, null); 230 | initialCommit.saveCommit(); 231 | 232 | // save the hashname to heads dir 233 | String commitHashName = initialCommit.getHashName(); 234 | String branchName = "master"; 235 | saveBranch(branchName, commitHashName); 236 | 237 | // 将此时的HEAD指针指向commit中的代表head的文件 238 | saveHEAD("master", commitHashName); 239 | 240 | } 241 | 242 | /** 243 | * java gitlet.Main add [file name] 244 | * 245 | * @param addFileName 246 | * @apiNote 这个函数用于实现git add 247 | */ 248 | public static void addStage(String addFileName) throws GitletException{ 249 | /* 如果文件名是空 */ 250 | if (addFileName == null || addFileName.isEmpty()) { 251 | throw new GitletException("Please enter a file name."); 252 | } 253 | 254 | File fileAdded = join(CWD, addFileName); 255 | /* 如果在工作目录中不存在此文件 */ 256 | 257 | if (!fileAdded.exists()) { 258 | throw new GitletException("File does not exist."); 259 | } 260 | String fileAddedContent = readContentsAsString(fileAdded); 261 | 262 | Commit headCommit = getHeadCommit(); 263 | HashMap headCommitBlobMap = headCommit.getBlobMap(); 264 | 265 | /* 如果这个文件已经被track */ 266 | if (headCommitBlobMap.containsKey(addFileName)) { 267 | String fileAddedInHash = headCommit.getBlobMap().get(addFileName); 268 | String commitContent = getBlobContentFromName(fileAddedInHash); 269 | 270 | /* 如果暂存内容和想要添加内容一致,则不将其纳入暂存区, 271 | 同时将其从暂存区删除(如果存在),同时将其从removal区移除 */ 272 | if (commitContent.equals(fileAddedContent)) { 273 | List filesAdd = plainFilenamesIn(ADD_STAGE_DIR); 274 | List filesRm = plainFilenamesIn(REMOVE_STAGE_DIR); 275 | /* 如果在暂存区存在,从暂存区删除 */ 276 | if (filesAdd.contains(addFileName)) { 277 | join(ADD_STAGE_DIR, addFileName).delete(); 278 | } 279 | /* 如果在removal area存在,从中删除 */ 280 | if (filesRm.contains(addFileName)) { 281 | join(REMOVE_STAGE_DIR, addFileName).delete(); 282 | } 283 | 284 | return; //直接退出 285 | } 286 | 287 | } 288 | 289 | /* 将文件放入暂存区,blob文件名是内容的hash值,内容是源文件内容 */ 290 | String fileContent = readContentsAsString(fileAdded); 291 | String blobName = sha1(fileContent); 292 | 293 | Blob blobAdd = new Blob(fileContent, blobName); // 使用blob进行对象化管理 294 | blobAdd.saveBlob(); 295 | 296 | /* 不管原先是否存在,都会执行写逻辑*/ 297 | /* addStage中写入指针,文件名是addFileName, 内容是暂存区保存的路径 */ 298 | File blobPoint = join(ADD_STAGE_DIR, addFileName); 299 | writeContents(blobPoint, blobAdd.getFilePath().getName()); 300 | } 301 | 302 | /** 303 | * java gitlet.Main commit [message] 304 | */ 305 | public static void commitFile(String commitMsg) throws GitletException { 306 | /* 获取addstage中的filename和hashname */ 307 | List addStageFiles = plainFilenamesIn(ADD_STAGE_DIR); 308 | List removeStageFiles = plainFilenamesIn(REMOVE_STAGE_DIR); 309 | /* 错误的情况,直接返回 */ 310 | if (addStageFiles.isEmpty() && removeStageFiles.isEmpty()) { 311 | throw new GitletException("No changes added to the commit."); 312 | } 313 | if (commitMsg == null || commitMsg.isEmpty()) { 314 | throw new GitletException("Please enter a commit message."); 315 | } 316 | 317 | 318 | /* 获取最新的commit*/ 319 | Commit oldCommit = getHeadCommit(); 320 | 321 | /* 创建新的commit,newCommit根据oldCommit进行调整*/ 322 | Commit newCommit = new Commit(oldCommit); 323 | newCommit.setDirectParent(oldCommit.getHashName()); // 指定父节点 324 | newCommit.setTimestamp(new Date(System.currentTimeMillis())); // 修改新一次的commit的时间戳为目前时间 325 | newCommit.setMessage(commitMsg); // 修改新一次的commit的时间戳为目前时间 326 | // newCommit.setBranchName(oldCommit.getBranchName()); // 在log或者status中需要展示本次commit的分支 327 | 328 | 329 | /* 对每一个addstage中的fileName进行其路径的读取,保存进commit的blobMap */ 330 | for (String stageFileName : addStageFiles) { 331 | String hashName = readContentsAsString(join(ADD_STAGE_DIR, stageFileName)); 332 | newCommit.addBlob(stageFileName, hashName); // 在newCommit中更新blob 333 | join(ADD_STAGE_DIR, stageFileName).delete(); 334 | } 335 | 336 | HashMap blobMap = newCommit.getBlobMap(); 337 | 338 | /* 对每一个rmstage中的fileName进行其路径的读取,删除commit的blobMap中对应的值 */ 339 | for (String stageFileName : removeStageFiles) { 340 | if (blobMap.containsKey(stageFileName)) { 341 | newCommit.removeBlob(stageFileName); // 在newCommit中删除removeStage中的blob 342 | } 343 | join(REMOVE_STAGE_DIR, stageFileName).delete(); 344 | } 345 | 346 | newCommit.saveCommit(); 347 | 348 | /* 更新HEAD指针和当前branch head指针 */ 349 | saveHEAD(getHeadBranchName(), newCommit.getHashName()); 350 | saveBranch(getHeadBranchName(), newCommit.getHashName()); 351 | 352 | } 353 | 354 | /** 355 | * 根据commit重载的方法,作用是为了进行merge时候的自动commit 356 | * 357 | * @param commitMsg 358 | * @param branchName 359 | */ 360 | public static void commitFileForMerge(String commitMsg, String branchName) throws GitletException { 361 | /* 获取addstage中的filename和hashname */ 362 | List addStageFiles = plainFilenamesIn(ADD_STAGE_DIR); 363 | List removeStageFiles = plainFilenamesIn(REMOVE_STAGE_DIR); 364 | /* 错误的情况,直接返回 */ 365 | if (addStageFiles.isEmpty() && removeStageFiles.isEmpty()) { 366 | throw new GitletException("No changes added to the commit."); 367 | } 368 | 369 | if (commitMsg == null) { 370 | throw new GitletException("Please enter a commit message."); 371 | } 372 | 373 | /* 获取最新的commit*/ 374 | Commit oldCommit = getHeadCommit(); 375 | Commit branchHeadCommit = getBranchHeadCommit(branchName, null); 376 | 377 | /* 创建新的commit,newCommit根据oldCommit进行调整*/ 378 | Commit newCommit = new Commit(oldCommit); 379 | newCommit.setDirectParent(oldCommit.getHashName()); // 指定父节点 380 | newCommit.setTimestamp(new Date(System.currentTimeMillis())); // 修改新一次的commit的时间戳为目前时间 381 | newCommit.setMessage(commitMsg); // 修改新一次的commit的时间戳为目前时间 382 | newCommit.setOtherParent(branchHeadCommit.getHashName()); // 指定另一个父节点 383 | 384 | /* 对每一个addstage中的fileName进行其路径的读取,保存进commit的blobMap */ 385 | for (String stageFileName : addStageFiles) { 386 | String hashName = readContentsAsString(join(ADD_STAGE_DIR, stageFileName)); 387 | newCommit.addBlob(stageFileName, hashName); // 在newCommit中更新blob 388 | join(ADD_STAGE_DIR, stageFileName).delete(); 389 | } 390 | 391 | HashMap blobMap = newCommit.getBlobMap(); 392 | 393 | /* 对每一个rmstage中的fileName进行其路径的读取,删除commit的blobMap中对应的值 */ 394 | for (String stageFileName : removeStageFiles) { 395 | if (blobMap.containsKey(stageFileName)) { 396 | join(BLOBS_FOLDER, blobMap.get(stageFileName)).delete(); // 删除blobs中的文件 397 | newCommit.removeBlob(stageFileName); // 在newCommit中删除removeStage中的blob 398 | } 399 | join(REMOVE_STAGE_DIR, stageFileName).delete(); 400 | } 401 | 402 | newCommit.saveCommit(); 403 | 404 | /* 更新HEAD指针和master指针 */ 405 | saveHEAD(getHeadBranchName(), newCommit.getHashName()); 406 | saveBranch(getHeadBranchName(), newCommit.getHashName()); 407 | } 408 | 409 | /** 410 | * java gitlet.Main rm [file name] 411 | * 412 | * @param removeFileName 指定删除的文件名 413 | */ 414 | public static void removeStage(String removeFileName) { 415 | /* 如果文件名是空或者如果工作区没有这个文件 */ 416 | if (removeFileName == null || removeFileName.isEmpty()) { 417 | System.out.println("Please enter a file name."); 418 | exit(0); 419 | } 420 | 421 | /* 如果在暂存目录中不存在此文件,同时在在commit中不存在此文件 */ 422 | Commit headCommit = getHeadCommit(); 423 | HashMap blobMap = headCommit.getBlobMap(); 424 | List addStageFiles = plainFilenamesIn(ADD_STAGE_DIR); 425 | 426 | if (!blobMap.containsKey(removeFileName)) { 427 | if (!addStageFiles.contains(removeFileName)) { 428 | System.out.println("No reason to remove the file."); 429 | exit(0); 430 | } 431 | 432 | } 433 | 434 | /* 如果addStage中存在,则删除 */ 435 | File addStageFile = join(ADD_STAGE_DIR, removeFileName); 436 | if (addStageFile.exists()) { 437 | addStageFile.delete(); 438 | } 439 | 440 | /* 当此文件正被track中 */ 441 | if (blobMap.containsKey(removeFileName)) { 442 | /* 添加进removeStage */ 443 | File remoteFilePoint = new File(REMOVE_STAGE_DIR, removeFileName); 444 | writeContents(remoteFilePoint, ""); 445 | 446 | /* 删除工作目录下文件,注意仅在这个文件被track的时候进行删除 */ 447 | File fileDeleted = new File(CWD, removeFileName); 448 | restrictedDelete(fileDeleted); 449 | } 450 | 451 | } 452 | 453 | /** 454 | * java gitlet.Main log 455 | */ 456 | public static void printLog() { 457 | 458 | Commit headCommit = getHeadCommit(); 459 | Commit commit = headCommit; 460 | 461 | while (!commit.getDirectParent().equals("")) { 462 | printCommitLog(commit); 463 | commit = getCommit(commit.getDirectParent()); 464 | } 465 | /* 打印最开始的一项*/ 466 | printCommitLog(commit); 467 | } 468 | 469 | 470 | /** 471 | * java gitlet.Main global-log 472 | * 473 | * @apiNote 这是不关注分支,只是把文件夹中的内容都打印出来了 474 | */ 475 | public static void printGlobalLog() { 476 | 477 | List commitFiles = plainFilenamesIn(COMMIT_FOLDER); 478 | for (String commitFileName : commitFiles) { 479 | Commit commit = getCommit(commitFileName); 480 | printCommitLog(commit); 481 | } 482 | } 483 | 484 | /** 485 | * java gitlet.Main find [commit message] 486 | */ 487 | public static void findCommit(String commitMsg) { 488 | Commit headCommit = getHeadCommit(); 489 | Commit commit = headCommit; 490 | boolean found = false; 491 | /* 如果msg相等就break,或者是到达初始提交就退出 */ 492 | 493 | // note: 这个方法有bug,无法找出不在branch中的commit 494 | // while (!commit.getDirectParent().isEmpty()) { 495 | // 496 | // if (commit.getMessage().equals(commitMsg)) { 497 | // found = true; 498 | // System.out.println(commit.getHashName()); 499 | // } 500 | // commit = getCommit(commit.getDirectParent()); 501 | // } 502 | // /* 检查最后一个提交 */ 503 | // if (commit.getMessage().equals(commitMsg)) { 504 | // found = true; 505 | // System.out.println(commit.getHashName()); 506 | // } 507 | 508 | /* 直接从commit文件夹中依次寻找 */ 509 | List commitFiles = plainFilenamesIn(COMMIT_FOLDER); 510 | for (String commitFile : commitFiles) { 511 | Commit commit1 = getCommit(commitFile); 512 | if (commit1.getMessage().equals(commitMsg)) { 513 | message(commit1.getHashName()); 514 | found = true; 515 | } 516 | } 517 | 518 | if (!found) { 519 | System.out.println("Found no commit with that message."); 520 | } 521 | 522 | } 523 | 524 | 525 | /** 526 | * java gitlet.Main status 527 | */ 528 | public static void showStatus() { 529 | File gitletFile = join(CWD, ".gitlet"); 530 | if (!gitletFile.exists()) { 531 | message("Not in an initialized Gitlet directory."); 532 | exit(0); 533 | } 534 | /* 获取当前分支名 */ 535 | Commit headCommit = getHeadCommit(); 536 | String branchName = getHeadBranchName(); 537 | 538 | List filesInHead = plainFilenamesIn(HEAD_DIR); 539 | List filesInAdd = plainFilenamesIn(ADD_STAGE_DIR); 540 | List filesInRm = plainFilenamesIn(REMOVE_STAGE_DIR); 541 | HashMap blobMap = headCommit.getBlobMap(); 542 | Set trackFileSet = blobMap.keySet(); // commit中跟踪着的文件名 543 | LinkedList modifiedFilesList = new LinkedList<>(); 544 | LinkedList deletedFilesList = new LinkedList<>(); 545 | LinkedList untrackFilesList = new LinkedList<>(); 546 | 547 | printStatusPerField("Branches", filesInHead, branchName); 548 | printStatusPerField("Staged Files", filesInAdd, branchName); 549 | printStatusPerField("Removed Files", filesInRm, branchName); 550 | 551 | /* 开始进行:Modifications Not Staged For Commit */ 552 | /* 暂存已经添加,但内容与工作目录中的内容不同 */ 553 | for (String fileAdd : filesInAdd) { 554 | /* 如果文件在暂存区存在,但是在工作区不存在,则直接加入modifiedFilesList */ 555 | if (!join(CWD, fileAdd).exists()) { 556 | deletedFilesList.add(fileAdd); 557 | continue; 558 | } 559 | String workFileContent = readContentsAsString(join(CWD, fileAdd)); 560 | String addStageBlobName = readContentsAsString(join(ADD_STAGE_DIR, fileAdd)); 561 | String addStageFileContent = readContentsAsString(join(BLOBS_FOLDER, addStageBlobName)); 562 | if (!workFileContent.equals(addStageFileContent)) { 563 | // 当工作区和addStage中文件内容不一致,则进入modifiedFilesList 564 | modifiedFilesList.add(fileAdd); 565 | } 566 | } 567 | 568 | /* 在当前commit中跟踪,在工作目录中更改,但未暂存 */ 569 | for (String trackFile : trackFileSet) { 570 | if (trackFile.isEmpty() || trackFile == null) { 571 | continue; 572 | } 573 | File workFile = join(CWD, trackFile); 574 | File fileInRmStage = join(REMOVE_STAGE_DIR, trackFile); 575 | if (!workFile.exists()) { // 当工作区文件直接不存在的情况 576 | if (!fileInRmStage.exists()) { 577 | deletedFilesList.add(trackFile); // 在rmStage中无此文件,同时工作区也没有这个文件 578 | } 579 | continue; 580 | } 581 | if (!filesInAdd.contains(trackFile)) { // 当addStage中没有此文件 582 | String workFileContent = readContentsAsString(workFile); 583 | String blobFileContent = readContentsAsString(join(BLOBS_FOLDER, 584 | blobMap.get(trackFile))); 585 | if (!workFileContent.equals(blobFileContent)) { 586 | // 当正在track的文件被修改,但addStage中无此文件,则进入modifiedFilesList 587 | modifiedFilesList.add(trackFile); 588 | } 589 | } 590 | } 591 | printStatusWithStatus("Modifications Not Staged For Commit", 592 | modifiedFilesList, deletedFilesList); 593 | /* 开始进行:Untracked Files */ 594 | List workFiles = plainFilenamesIn(CWD); 595 | for (String workFile : workFiles) { 596 | if (!filesInAdd.contains(workFile) 597 | && !filesInRm.contains(workFile) 598 | && !trackFileSet.contains(workFile)) { 599 | untrackFilesList.add(workFile); 600 | continue; 601 | } 602 | if (filesInRm.contains(workFile)) { 603 | untrackFilesList.add(workFile); 604 | } 605 | } 606 | printStatusPerField("Untracked Files", untrackFilesList, branchName); 607 | } 608 | 609 | 610 | /** 611 | * java gitlet.Main checkout -- [file name] 612 | * java gitlet.Main checkout [commit id] -- [file name] 613 | * java gitlet.Main checkout [branch name] 614 | * 615 | * @param args 616 | */ 617 | public static void checkOut(String[] args) { 618 | String fileName; 619 | if (args.length == 2) { 620 | // git checkout branchName 621 | checkoutBranch(args[1]); 622 | } else if (args.length == 4) { 623 | // git checkout [commit id] -- [file name] 624 | if (!args[2].equals("--")) { 625 | message("Incorrect operands."); 626 | exit(0); 627 | } 628 | /* 获取到Blob对象 */ 629 | fileName = args[3]; 630 | String commitId = args[1]; 631 | Commit commit = getHeadCommit(); 632 | 633 | /* 是否可以进行对objects文件夹的重构,实现hashMap结构 634 | 使得时间效率上不是线性, 而不是依靠链表查找? */ 635 | if (getCommitFromId(commitId) == null) { 636 | System.out.println("No commit with that id exists."); 637 | exit(0); 638 | } else { 639 | commit = getCommitFromId(commitId); 640 | } 641 | 642 | if (!commit.getBlobMap().containsKey(fileName)) { 643 | System.out.println("File does not exist in that commit."); 644 | exit(0); 645 | } 646 | String blobName = commit.getBlobMap().get(fileName); 647 | String targetBlobContent = getBlobContentFromName(blobName); 648 | 649 | /* 将Blob对象中的内容覆盖working directory中的内容 */ 650 | File fileInWorkDir = join(CWD, fileName); 651 | overWriteFileWithBlob(fileInWorkDir, targetBlobContent); 652 | 653 | } else if (args.length == 3) { 654 | // git checkout -- [file name] 655 | /* 获取到Blob对象中的内容 */ 656 | fileName = args[2]; 657 | Commit headCommit = getHeadCommit(); 658 | if (!headCommit.getBlobMap().containsKey(fileName)) { 659 | System.out.println("File does not exist in that commit."); 660 | exit(0); 661 | } 662 | String blobName = headCommit.getBlobMap().get(fileName); 663 | String targetBlobContent = getBlobContentFromName(blobName); 664 | 665 | /* 将Blob对象中的内容覆盖working directory中的内容 */ 666 | File fileInWorkDir = join(CWD, fileName); 667 | overWriteFileWithBlob(fileInWorkDir, targetBlobContent); 668 | 669 | } 670 | } 671 | 672 | /** 673 | * 仅针对checkout的 674 | * java gitlet.Main checkout [branch name]情况 675 | * 676 | * @param branchName 677 | */ 678 | public static void checkoutBranch(String branchName) { 679 | Commit headCommit = getHeadCommit(); 680 | 681 | if (branchName.equals(getHeadBranchName())) { 682 | System.out.println("No need to checkout the current branch."); 683 | exit(0); 684 | } 685 | // 获取branchName的head对应的commit 686 | Commit branchHeadCommit = getBranchHeadCommit(branchName, "No such branch exists"); 687 | HashMap branchHeadBlobMap = branchHeadCommit.getBlobMap(); 688 | Set fileNameSet = branchHeadBlobMap.keySet(); 689 | 690 | List workFileNames = plainFilenamesIn(CWD); 691 | 692 | if (untrackFileExists(headCommit)) { 693 | System.out.println("There is an untracked file in the way; " 694 | + "delete it, or add and commit it first."); 695 | exit(0); 696 | } 697 | 698 | /* 检测完后清空CWD文件夹 */ 699 | for (String workFile : workFileNames) { 700 | restrictedDelete(join(CWD, workFile)); 701 | } 702 | 703 | /* 将fileNameSet中每一个跟踪的文件重写入工作文件夹中 */ 704 | for (var trackedfileName : fileNameSet) { 705 | // 每一个trackedfileName是一个commit中跟踪的fileName 706 | File workFile = join(CWD, trackedfileName); 707 | String blobHash = branchHeadBlobMap.get(trackedfileName); // 文件对应的blobName 708 | String blobFromNameContent = getBlobContentFromName(blobHash); 709 | writeContents(workFile, blobFromNameContent); 710 | } 711 | 712 | /* 将目前给定的分支视作当前分支 */ 713 | saveHEAD(branchName, branchHeadCommit.getHashName()); 714 | } 715 | 716 | 717 | /** 718 | * java gitlet.Main branch [branch name] 719 | */ 720 | public static void createBranch(String branchName) { 721 | Commit headCommit = getHeadCommit(); 722 | List fileNameinHeadDir = plainFilenamesIn(HEAD_DIR); 723 | if (fileNameinHeadDir.contains(branchName)) { 724 | message("A branch with that name already exists."); 725 | exit(0); 726 | } 727 | 728 | saveBranch(branchName, headCommit.getHashName()); 729 | 730 | } 731 | 732 | 733 | /** 734 | * java gitlet.Main rm-branch [branch name] 735 | */ 736 | public static void removeBranch(String branchName) { 737 | /* 检测是否有相关Branch */ 738 | File brancheFile = join(HEAD_DIR, branchName); 739 | if (!brancheFile.exists()) { 740 | System.out.println("A branch with that name does not exist."); 741 | exit(0); 742 | } 743 | /* 检测Branch是否为curr branch */ 744 | Commit headCommit = getHeadCommit(); 745 | if (getHeadBranchName().equals(branchName)) { 746 | System.out.println("Cannot remove the current branch."); 747 | exit(0); 748 | } 749 | /* 删除这个branch的指针文件 */ 750 | File branchHeadPoint = join(HEAD_DIR, branchName); 751 | branchHeadPoint.delete(); 752 | } 753 | 754 | /** 755 | * java gitlet.Main reset [commit id] 756 | * 757 | * @apiNote java gitlet.Main reset [commit id] 将文件内容全部转化为[commit id]中的文件 758 | */ 759 | public static void reset(String commitId) { 760 | 761 | if (getCommitFromId(commitId) == null) { 762 | System.out.println("No commit with that id exists."); 763 | exit(0); 764 | } 765 | Commit headCommit = getHeadCommit(); 766 | Commit commit = getCommitFromId(commitId); // 将要reset的commit 767 | HashMap commitBlobMap = commit.getBlobMap(); 768 | 769 | /* 先检测CWD中是否存在未被current branch跟踪的文件 */ 770 | List workFileNames = plainFilenamesIn(CWD); 771 | /* 先检测CWD中是否存在未被current branch跟踪的文件 */ 772 | if (untrackFileExists(headCommit)) { 773 | Set currTrackSet = headCommit.getBlobMap().keySet(); 774 | Set resetTrackSet = commit.getBlobMap().keySet(); 775 | boolean isUntrackInBoth = false; 776 | 777 | /* workfile没有在headCommit中也没有在commit中,将其从addstage中剔除,但在CWD中保存 */ 778 | for (String workFile : workFileNames) { 779 | if (!currTrackSet.contains(workFile) && !resetTrackSet.contains(workFile)) { 780 | removeStage(workFile); 781 | isUntrackInBoth = true; 782 | break; 783 | } 784 | } 785 | if (!isUntrackInBoth) { 786 | // message("There is an untracked file in the way; " 787 | // + "delete it, or add and commit it first."); 788 | // exit(0); 789 | throw new GitletException("There is an untracked file in the way; " 790 | + "delete it, or add and commit it first."); 791 | } 792 | 793 | } 794 | 795 | /* 检测完后清空CWD文件夹 */ 796 | for (String workFile : workFileNames) { 797 | restrictedDelete(join(CWD, workFile)); 798 | } 799 | 800 | /* 将fileNameSet中每一个跟踪的文件重写入工作文件夹中 */ 801 | for (var trackedfileName : commit.getBlobMap().keySet()) { 802 | // 每一个trackedfileName是一个commit中跟踪的fileName 803 | File workFile = join(CWD, trackedfileName); 804 | String blobHash = commitBlobMap.get(trackedfileName); // 文件对应的blobName 805 | String blobFromNameContent = getBlobContentFromName(blobHash); 806 | writeContents(workFile, blobFromNameContent); 807 | } 808 | 809 | /* 同时将其branchHEAD指向commit*/ 810 | saveBranch(getHeadBranchName(), commitId); 811 | /* 将目前给定的HEAD指针指向这个commit */ 812 | saveHEAD(getHeadBranchName(), commitId); 813 | } 814 | 815 | 816 | /** 817 | * Merges files from the given branch into the current branch. 818 | * If the split point is the same commit as the given branch, 819 | * then we do nothing; the merge is complete, and the operation ends with the message: 820 | * Given branch is an ancestor of the current branch. 821 | *

822 | * If the split point is the current branch, then the effect is to check out the given branch, 823 | * and the operation ends after printing the message: Current branch fast-forwarded. 824 | * Otherwise, we continue with the steps below. 825 | * 826 | * @apiNote : 827 | * 1. other:被修改 HEAD:未被修改 ---> working DIR: other, 并且需要被add 828 | * 2. other:未被修改 HEAD:被修改 ---> working DIR: HEAD 829 | * 3. other:被修改 HEAD:被修改 ---> (一致的修改) working DIR: HEAD, 相当于什么都不做 830 | * |-> (不一致的修改) working DIR: Conflict 831 | * 4. split:不存在 other:不存在 HEAD:被添加 ---> working DIR: HEAD 832 | * 5. split:不存在 other:被添加 HEAD:不存在 ---> working DIR: other, 并且需要被add 833 | * 6. other:被删除 HEAD:未被修改 ---> working DIR: 被删除,同时被暂存于removal 834 | * 7. other:未被修改 HEAD:被删除 ---> working DIR: 被删除 835 | */ 836 | public static void mergeBranch(String branchName) { 837 | checkSafetyInMerge(branchName); 838 | Commit headCommit = getHeadCommit(); 839 | Commit otherHeadCommit = getBranchHeadCommit(branchName, 840 | "A branch with that name does not exist."); // 如果不存在这个branch,则报错 841 | /* 获取当前splitCommit对象 */ 842 | Commit splitCommit = getSplitCommit(headCommit, otherHeadCommit); 843 | if (splitCommit.getHashName().equals(otherHeadCommit.getHashName())) { 844 | throw new GitletException("Given branch is an ancestor of the current branch."); 845 | } 846 | 847 | HashMap splitCommitBolbMap = splitCommit.getBlobMap(); 848 | Set splitKeySet = splitCommitBolbMap.keySet(); 849 | HashMap headCommitBolbMap = headCommit.getBlobMap(); 850 | Set headKeySet = headCommitBolbMap.keySet(); 851 | HashMap otherHeadCommitBolbMap = otherHeadCommit.getBlobMap(); 852 | Set otherKeySet = otherHeadCommitBolbMap.keySet(); 853 | 854 | processSplitCommit(splitCommit, headCommit, otherHeadCommit); 855 | /* 为解决被删除操作 */ 856 | for (var headTrackName : headKeySet) { 857 | if (!otherHeadCommitBolbMap.containsKey(headTrackName)) { 858 | if (!splitCommitBolbMap.containsKey(headTrackName)) { 859 | /* 情况4:如果在other和split中都没有这个文件 */ 860 | continue; 861 | } else { 862 | /* split:存在 other:被删除 */ 863 | if (!headCommitBolbMap.get(headTrackName) 864 | .equals(splitCommitBolbMap.get(headTrackName))) { 865 | /* HEAD:被修改 */ 866 | /* 存在 conflict */ 867 | processConflict(headCommit, otherHeadCommit, headTrackName); 868 | } 869 | /* 其他情况是情况6 已处理过 */ 870 | } 871 | } else if (otherHeadCommitBolbMap.containsKey(headTrackName) 872 | && !splitCommitBolbMap.containsKey(headTrackName)) { 873 | /* 情况3b other中存在文件, split中不存在文件,即这是不一致的修改*/ 874 | if (!otherHeadCommitBolbMap.get(headTrackName) 875 | .equals(headCommitBolbMap.get(headTrackName))) { 876 | /*如果是不一致的修改,进行conflict处理,如果是一致的就跳过*/ 877 | processConflict(headCommit, otherHeadCommit, headTrackName); 878 | } 879 | } 880 | } 881 | for (var otherTrackName : otherKeySet) { 882 | if (!headCommitBolbMap.containsKey(otherTrackName) 883 | && !splitCommitBolbMap.containsKey(otherTrackName)) { 884 | /* 情况5:如果在head和split中都没有这个文件 */ 885 | String[] checkOutArgs = {"checkout", 886 | otherHeadCommit.getHashName(), 887 | "--", 888 | otherTrackName}; 889 | checkOut(checkOutArgs); 890 | addStage(otherTrackName); 891 | } 892 | } 893 | if (splitCommit.getHashName().equals(headCommit.getHashName())) { 894 | message("Current branch fast-forwarded."); 895 | } 896 | /* 进行一次自动的commit */ 897 | String commitMsg = String.format("Merged %s into %s.", branchName, getHeadBranchName()); 898 | commitFileForMerge(commitMsg, branchName); 899 | } 900 | 901 | 902 | public static void processSplitCommit(Commit splitCommit, Commit headCommit, 903 | Commit otherHeadCommit) { 904 | HashMap splitCommitBolbMap = splitCommit.getBlobMap(); 905 | Set splitKeySet = splitCommitBolbMap.keySet(); 906 | HashMap headCommitBolbMap = headCommit.getBlobMap(); 907 | Set headKeySet = headCommitBolbMap.keySet(); 908 | HashMap otherHeadCommitBolbMap = otherHeadCommit.getBlobMap(); 909 | Set otherKeySet = otherHeadCommitBolbMap.keySet(); 910 | 911 | /* 从split中的文件开始 */ 912 | for (var splitTrackName : splitKeySet) { 913 | // 如果在HEAD中未被修改(包括未被删除) 914 | if (headCommitBolbMap.containsKey(splitTrackName) 915 | && headCommitBolbMap.get(splitTrackName) 916 | .equals(splitCommitBolbMap.get(splitTrackName))) { 917 | // 如果other中存在此文件 918 | if (otherHeadCommitBolbMap.containsKey(splitTrackName)) { 919 | /* 情况1 HEAD中未被修改,other中被修改*/ 920 | if (!otherHeadCommitBolbMap.get(splitTrackName) 921 | .equals(splitCommitBolbMap.get(splitTrackName))) { 922 | // 使用checkout将other的文件覆盖进工作区,同时将其add进暂存区 923 | String[] checkOutArgs = {"checkout", 924 | otherHeadCommit.getHashName(), 925 | "--", 926 | splitTrackName}; 927 | checkOut(checkOutArgs); 928 | addStage(splitTrackName); 929 | } 930 | } else { 931 | /* 情况6: 当HEAD未修改,other中被删除 */ 932 | removeStage(splitTrackName); 933 | } 934 | } else { 935 | // 在HEAD中被修改(包括被删除) 936 | if (otherHeadCommitBolbMap.containsKey(splitTrackName) 937 | && otherHeadCommitBolbMap.get(splitTrackName) 938 | .equals(splitCommitBolbMap.get(splitTrackName))) { 939 | /* 情况2 other中未被修改,HEAD中被修改,则不修改任何事情 940 | 情况7 other中未被修改,HEAD中被删除,则不修改任何事情 */ 941 | continue; 942 | } else { 943 | /* other中被修改 或者被删除 */ 944 | if (!otherHeadCommitBolbMap.containsKey(splitTrackName) 945 | && !headCommitBolbMap.containsKey(splitTrackName)) { 946 | /* 情况3a 一致的删除 */ 947 | continue; 948 | } else if (!otherHeadCommitBolbMap.containsKey(splitTrackName) 949 | || !headCommitBolbMap.containsKey(splitTrackName)) { 950 | /* 只存在一方被删除,跳过,从后面单独对HEAD和other指针进行操作 */ 951 | continue; 952 | } else { 953 | if (otherHeadCommitBolbMap.get(splitTrackName) 954 | .equals(headCommitBolbMap.get(splitTrackName))) { 955 | /* 情况3a 一致的修改 */ 956 | continue; 957 | } else { 958 | /* 情况3b 不一致的修改,不包括删除操作 */ 959 | processConflict(headCommit, otherHeadCommit, splitTrackName); 960 | } 961 | } 962 | } 963 | } 964 | } 965 | } 966 | 967 | 968 | /** 969 | * 检测merge指令的错误情况 970 | * 971 | * @param branchName 972 | */ 973 | public static void checkSafetyInMerge(String branchName) { 974 | 975 | List addStageFiles = plainFilenamesIn(ADD_STAGE_DIR); 976 | List rmStageFiles = plainFilenamesIn(REMOVE_STAGE_DIR); 977 | /* 如果存在暂存,直接退出 */ 978 | if (!addStageFiles.isEmpty() || !rmStageFiles.isEmpty()) { 979 | throw new GitletException("You have uncommitted changes."); 980 | } 981 | Commit headCommit = getHeadCommit(); 982 | // 如果不存在这个branch,则报错 983 | String errMsg = "A branch with that name does not exist."; 984 | Commit otherHeadCommit = getBranchHeadCommit(branchName, errMsg); 985 | 986 | if (getHeadBranchName().equals(branchName)) { 987 | throw new GitletException("Cannot merge a branch with itself."); 988 | } 989 | /* 查看是否存在未被跟踪的文件 */ 990 | if (untrackFileExists(headCommit)) { 991 | throw new GitletException("There is an untracked file in the way; " 992 | + "delete it, or add and commit it first."); 993 | } 994 | } 995 | } 996 | --------------------------------------------------------------------------------