├── .github └── workflows │ └── maven-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── doc ├── DiffTutorial.exe ├── OptionsAndParameters2.png ├── ZIT.pdf ├── diff2.pdf └── diff3-short.pdf ├── pom.xml └── src ├── main ├── java │ └── club │ │ └── qqtim │ │ ├── Main.java │ │ ├── command │ │ ├── Add.java │ │ ├── Branch.java │ │ ├── CatFile.java │ │ ├── Checkout.java │ │ ├── Commit.java │ │ ├── Diff.java │ │ ├── Fetch.java │ │ ├── HashObject.java │ │ ├── Init.java │ │ ├── Lg.java │ │ ├── Log.java │ │ ├── Merge.java │ │ ├── MergeBase.java │ │ ├── Push.java │ │ ├── ReadTree.java │ │ ├── Reset.java │ │ ├── Show.java │ │ ├── Status.java │ │ ├── Tag.java │ │ └── WriteTree.java │ │ ├── common │ │ ├── ConstantVal.java │ │ └── RegexConstantVal.java │ │ ├── context │ │ ├── Zit.java │ │ └── ZitContext.java │ │ ├── converter │ │ └── IdConverter.java │ │ ├── data │ │ ├── CommitObject.java │ │ ├── RefObjValue.java │ │ ├── RefObject.java │ │ ├── RefValue.java │ │ └── ZitObject.java │ │ ├── diff │ │ ├── DiffUtil.java │ │ ├── LineObject.java │ │ ├── MergePoint.java │ │ ├── SimplyChange.java │ │ └── algorithm │ │ │ ├── DiffAlgorithm.java │ │ │ ├── MyersDiff.java │ │ │ ├── Snake.java │ │ │ └── SnakePoint.java │ │ ├── parser │ │ ├── Parser.java │ │ └── support │ │ │ └── CommandLineParser.java │ │ └── util │ │ ├── ConvertUtil.java │ │ ├── FileUtil.java │ │ └── handler │ │ └── PosixHandler.java └── resources │ └── log4j.properties └── test └── java └── club └── qqtim └── data └── DataTest.java /.github/workflows/maven-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path 3 | 4 | name: Maven Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up JDK 11 21 | uses: actions/setup-java@v3 22 | with: 23 | java-version: '11' 24 | distribution: 'temurin' 25 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 26 | settings-path: ${{ github.workspace }} # location for the settings.xml file 27 | 28 | - name: Build with Maven 29 | run: mvn -B package --file pom.xml 30 | 31 | - name: Publish to GitHub Packages Apache Maven 32 | run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml 33 | env: 34 | GITHUB_TOKEN: ${{ github.token }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/java,maven,intellij 3 | # Edit at https://www.gitignore.io/?templates=java,maven,intellij 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/ 11 | 12 | # CMake 13 | cmake-build-*/ 14 | 15 | # Mongo Explorer plugin 16 | .idea/**/mongoSettings.xml 17 | 18 | # File-based project format 19 | *.iws 20 | 21 | # IntelliJ 22 | out/ 23 | 24 | # mpeltonen/sbt-idea plugin 25 | .idea_modules/ 26 | 27 | # JIRA plugin 28 | atlassian-ide-plugin.xml 29 | 30 | # Crashlytics plugin (for Android Studio and IntelliJ) 31 | com_crashlytics_export_strings.xml 32 | crashlytics.properties 33 | crashlytics-build.properties 34 | fabric.properties 35 | 36 | ### Intellij Patch ### 37 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 38 | 39 | *.iml 40 | modules.xml 41 | *.ipr 42 | 43 | ### Java ### 44 | # Compiled class file 45 | *.class 46 | 47 | # Log file 48 | *.log 49 | 50 | # BlueJ files 51 | *.ctxt 52 | 53 | # Mobile Tools for Java (J2ME) 54 | .mtj.tmp/ 55 | 56 | # Package Files # 57 | *.jar 58 | *.war 59 | *.nar 60 | *.ear 61 | *.zip 62 | *.tar.gz 63 | *.rar 64 | 65 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 66 | hs_err_pid* 67 | 68 | ### Maven ### 69 | target/ 70 | pom.xml.tag 71 | pom.xml.releaseBackup 72 | pom.xml.versionsBackup 73 | pom.xml.next 74 | release.properties 75 | dependency-reduced-pom.xml 76 | buildNumber.properties 77 | .mvn/timing.properties 78 | .mvn/wrapper/maven-wrapper.jar 79 | 80 | # End of https://www.gitignore.io/api/java,maven,intellij 81 | 82 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode 83 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode 84 | 85 | ### VisualStudioCode ### 86 | .vscode/* 87 | .settings/* 88 | !.vscode/settings.json 89 | !.vscode/tasks.json 90 | !.vscode/launch.json 91 | !.vscode/extensions.json 92 | 93 | ### VisualStudioCode Patch ### 94 | # Ignore all local history of files 95 | .history 96 | 97 | 98 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ReZero 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [GIT](https://github.com/ReZeroS/git) Development Log 2 | 3 | Thanks for the great tutorial: [https://www.leshenko.net/p/ugit](https://www.leshenko.net/p/ugit) 4 | 5 | Thanks again, Nikita! 6 | 7 | ## Resources 8 | 9 | I found some good reference materials and placed them in the `./doc` directory. 10 | 11 | Additionally, these links might be helpful: 12 | 13 | - [Nick Butler](http://simplygenius.net/Article/DiffTutorial1): A concise post that provides a high-level understanding of diff 14 | - [jcoglan](https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/): A detailed description of the diff algorithm 15 | - [Visualization](https://blog.robertelder.org/diff-algorithm/): A great resource for debugging or visualizing the diff algorithm 16 | - [PDF](https://github.com/ReZeroS/zit/blob/main/doc/ZIT.pptx): A short PDF about Git that I hope you'll find interesting 17 | 18 | If you're interested in this project, feel free to share your ideas in the discussion or contact me via email. 19 | 20 | ## Usage 21 | 22 | 0. `alias zit='java -jar ../zit-1.0-SNAPSHOT-shaded.jar'` - Alias the zit executable file 23 | 24 | - On Windows, you can set this in `C:\Program Files\Git\etc\profile.d\aliases.sh` 25 | 26 | 1. `zit init` - Initialize the `.zit` directory with an `objects` subdirectory, create an index file (stage area), and set the default `main` branch 27 | 28 | - The default HEAD file content is `ref: ref/heads/main` 29 | 30 | 2. `zit hash-object file` 31 | 32 | - Get the file path to store 33 | - Read the file 34 | - Hash the *content* of the file using SHA-1 35 | - Store the file under `.ugit/objects/{the SHA-1 hash}` 36 | 37 | 3. `zit cat-file hash [object|tree|commit|...type]` - Print the file content 38 | 39 | 4. `zit write-tree` - Generate a tree describing the entire repository 40 | 41 | - Executed after the `add` command, creating a tree from the index file 42 | 43 | 5. `zit read-tree hash` 44 | 45 | - Caution: This action will delete all existing content before reading 46 | - Use `cat-file` to find the `root` tree 47 | - Logs from `write-tree` can also help you find all trees 48 | 49 | 6. While `write-tree` can save versions, it lacks context information, so a `zit commit -m "message"` command is needed 50 | 51 | - Use `cat-file hash commit-id` to check commit content 52 | - `HEAD` will record the commit with its parent 53 | 54 | 7. Enjoy committing and use `log` to view commit history 55 | 56 | 8. Checkout: Select a commit ID from the `log` and verify the state 57 | 58 | - [Fixed with getBytes(Charsets.UTF-8)] Bug: Chinese file or directory names may appear garbled 59 | - Arguments can be head alias, hash, or ref (branch, tags, HEAD...) 60 | 61 | 9. `tag` will alias a commit ID, introducing a core concept 62 | 63 | - [git-ref](https://git-scm.com/book/en/v2/Git-Internals-Git-References) official post helps learn basic reference knowledge 64 | 65 | 10. TODO: `zit lg` graph feature with Graphviz 66 | 67 | 11. `zit branch name [id]` - Familiar branch creation 68 | 69 | - Every ref under `refs/heads` is treated as a branch 70 | - File content is simply the commit ID, defaulting to the head point 71 | 72 | 12. `zit show` will display detailed changes using diff, while `status` shows simple change information 73 | 74 | 13. `zit add` adds files or directories to the stage file: `.zit/index` 75 | 76 | 14. `zit commit` calls `write-tree` and updates the HEAD pointer to the commit ID 77 | 78 | - First-time usage creates the default `main` branch and rewrites the HEAD file content 79 | - Merge HEAD is deleted, and the message is added to the commit message 80 | 81 | 15. `zit status` shows the current situation 82 | 83 | - If not in a detached HEAD state, logs the current HEAD-pointed branch 84 | - Logs merge hash ID if in a merge state 85 | - Lists changes to be committed (diff between HEAD tree and index) 86 | - Lists changes not staged for the next commit (diff between index and working tree) 87 | 88 | 16. `zit diff` uses the Myers diff algorithm without linear space refinement optimization 89 | 90 | 17. `zit reset` changes HEAD to the current commit (difference from `checkout` is pending) 91 | 92 | 18. `zit merge` checks if the merge base equals the head, using fast-forward merge if possible 93 | 94 | - Fast-forward requires no commit 95 | - Otherwise, uses diff3 to merge the merge base, head tree, and other tree 96 | - Leaves `merge_head` in the zit root directory, requiring manual commit 97 | - `zit merge-base` helps find the first common parent commit for merging and debugging 98 | 99 | 19. `zit fetch` and `zit push` download or upload objects and update references 100 | 101 | ## Summary 102 | 103 | ### UPDATED 2021.02.21 104 | 105 | - Implemented diff (Myers diff without linear space optimization) and merge algorithms (simple diff3) instead of using Unix tools 106 | - `Ugit` uses Pythonic code, while zit aims to make the code easily understandable for developers of other languages 107 | 108 | ### TODO 109 | 110 | 1. `git hash-object` improvement: When real Git stores objects, it: 111 | - Writes the object size to the file 112 | - Compresses objects 113 | - Divides objects into 256 directories to avoid performance issues with large numbers of files 114 | -------------------------------------------------------------------------------- /doc/DiffTutorial.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroS/git/bbf78f7ce7f8a07008b5bbcdb70da44701b8d917/doc/DiffTutorial.exe -------------------------------------------------------------------------------- /doc/OptionsAndParameters2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroS/git/bbf78f7ce7f8a07008b5bbcdb70da44701b8d917/doc/OptionsAndParameters2.png -------------------------------------------------------------------------------- /doc/ZIT.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroS/git/bbf78f7ce7f8a07008b5bbcdb70da44701b8d917/doc/ZIT.pdf -------------------------------------------------------------------------------- /doc/diff2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroS/git/bbf78f7ce7f8a07008b5bbcdb70da44701b8d917/doc/diff2.pdf -------------------------------------------------------------------------------- /doc/diff3-short.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroS/git/bbf78f7ce7f8a07008b5bbcdb70da44701b8d917/doc/diff3-short.pdf -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | club.qqtim 8 | zit 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | 14 | 15 | org.projectlombok 16 | lombok 17 | 1.18.10 18 | provided 19 | 20 | 21 | 22 | 23 | ch.qos.logback 24 | logback-classic 25 | 1.3.12 26 | 27 | 28 | com.google.guava 29 | guava 30 | 32.0.0-jre 31 | 32 | 33 | info.picocli 34 | picocli 35 | 4.5.2 36 | 37 | 38 | 39 | 40 | com.google.code.gson 41 | gson 42 | 2.8.9 43 | 44 | 45 | 46 | 47 | junit 48 | junit 49 | 4.13.1 50 | test 51 | 52 | 53 | 54 | 55 | com.github.jnr 56 | jnr-posix 57 | 3.1.4 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-shade-plugin 67 | 68 | 69 | 70 | shade 71 | 72 | 73 | true 74 | 75 | 77 | club.qqtim.Main 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | org.apache.maven.plugins 86 | maven-compiler-plugin 87 | 88 | 8 89 | 8 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/Main.java: -------------------------------------------------------------------------------- 1 | package club.qqtim; 2 | 3 | import club.qqtim.parser.Parser; 4 | import club.qqtim.parser.support.CommandLineParser; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | 8 | @Slf4j 9 | public class Main { 10 | 11 | public static void main(String[] args) { 12 | log.info("hello, zit."); 13 | Parser parser = new CommandLineParser(); 14 | parser.execute(args); 15 | } 16 | 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Add.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.util.FileUtil; 5 | import com.google.gson.Gson; 6 | import com.google.gson.JsonObject; 7 | import lombok.extern.slf4j.Slf4j; 8 | import picocli.CommandLine; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.nio.file.Files; 13 | import java.nio.file.Paths; 14 | import java.util.List; 15 | 16 | /** 17 | * @title: Add 18 | * @Author ReZeroS 19 | * @Date: 2021/2/3 20 | * @Version 1.0.0 21 | */ 22 | 23 | @Slf4j 24 | @lombok.Data 25 | @CommandLine.Command(name = "add") 26 | public class Add implements Runnable{ 27 | 28 | 29 | @CommandLine.Parameters(paramLabel = "FILE", description = "one ore more files to archive", defaultValue = ".") 30 | List files; 31 | 32 | 33 | 34 | @Override 35 | public void run() { 36 | addFiles(files); 37 | } 38 | 39 | /** 40 | * update the index file content: if new file then add while old file will be updated 41 | * content is a big easy (key val directly) json like this: 42 | * { 43 | * ".\\doc\\OptionsAndParameters2.png": "f4a36c21ce890b0fa10067c10dab416e9b71a13e" 44 | * } 45 | */ 46 | private void addFiles(List files) { 47 | final String indexContent = FileUtil.getFileAsString(ConstantVal.INDEX, ConstantVal.NONE); 48 | final JsonObject asJsonObject = new Gson().fromJson(indexContent, JsonObject.class); 49 | 50 | files.forEach(file -> { 51 | final boolean isFile = FileUtil.isFile(file); 52 | if (isFile) { 53 | addFile(asJsonObject, file); 54 | } else { // is directory 55 | addDirectory(asJsonObject, file); 56 | } 57 | }); 58 | FileUtil.createFile(asJsonObject.toString(), ConstantVal.INDEX); 59 | } 60 | 61 | private void addFile(JsonObject asJsonObject, String file) { 62 | HashObject hashObject = new HashObject(); 63 | hashObject.setFile(new File(file)); 64 | hashObject.setType(ConstantVal.BLOB); 65 | final String objectId = hashObject.call(); 66 | asJsonObject.addProperty(file, objectId); 67 | } 68 | 69 | 70 | private void addDirectory(JsonObject asJsonObject, String file) { 71 | try { 72 | FileUtil.walk(Paths.get(file), Integer.MAX_VALUE) 73 | .filter(Files::isRegularFile) 74 | .forEach(regularFile -> addFile(asJsonObject, regularFile.toString())); 75 | } catch (IOException e) { 76 | log.error(e.toString()); 77 | } 78 | } 79 | 80 | 81 | 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Branch.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.converter.IdConverter; 6 | import club.qqtim.data.RefObject; 7 | import club.qqtim.data.RefValue; 8 | import lombok.extern.slf4j.Slf4j; 9 | import picocli.CommandLine; 10 | 11 | import java.nio.file.Paths; 12 | import java.util.List; 13 | import java.util.Objects; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * @title: Branch 18 | * @Author rezeros.github.io 19 | * @Date: 2020/12/9 20 | * @Version 1.0.0 21 | */ 22 | @lombok.Data 23 | @Slf4j 24 | @CommandLine.Command(name = "branch") 25 | public class Branch implements Runnable{ 26 | 27 | 28 | @CommandLine.Parameters(index = "0", defaultValue = ConstantVal.NONE) 29 | private String name; 30 | 31 | 32 | 33 | @CommandLine.Parameters(index = "1", defaultValue = ConstantVal.HEAD_ALIAS, converter = IdConverter.class) 34 | private String startPoint; 35 | 36 | 37 | /** 38 | * Pay attention, only you create a commit at branch, the branch will generate at real 39 | * so if you execute `zit init`, then `zit branch` immediately will get nothing, this is not a bug. 40 | */ 41 | @Override 42 | public void run() { 43 | // none name for readable command 44 | if(ConstantVal.NONE.equals(name)) { 45 | final String currentBranch = ZitContext.getBranchName(); 46 | final List branchNames = iteratorBranchNames(); 47 | branchNames.forEach(branchName -> 48 | log.info("{} {}", branchName.equals(currentBranch) ? ConstantVal.STAR : ConstantVal.EMPTY, branchName) 49 | ); 50 | } else { 51 | // got name for branch will be created base on the commit point 52 | createBranch(name, startPoint); 53 | log.info("created branch {} at {}", name, startPoint.substring(0, 11)); 54 | } 55 | } 56 | 57 | /** 58 | * list all branches in the repository 59 | */ 60 | private List iteratorBranchNames(){ 61 | final List refObjects = ZitContext.iteratorRefs(ConstantVal.HEADS_PATH); 62 | return refObjects.stream() 63 | .map(RefObject::getRefName) 64 | .map(path -> Paths.get(path).relativize(Paths.get(ConstantVal.HEADS_PATH)).toString()) 65 | .collect(Collectors.toList()); 66 | } 67 | 68 | 69 | /** 70 | * create a branch 71 | * this will create a file with commitId content called branch name under the refs/heads directory 72 | */ 73 | private static void createBranch(String name, String startPoint) { 74 | String branch = String.format(ConstantVal.BASE_REFS_HEADS_PATH, name); 75 | ZitContext.updateRef(branch, new RefValue(false, startPoint)); 76 | } 77 | 78 | 79 | /** 80 | * whether the branch exist 81 | */ 82 | public static boolean existBranch(String name){ 83 | String branch = String.format(ConstantVal.BASE_REFS_HEADS_PATH, name); 84 | final RefValue ref = ZitContext.getRef(branch); 85 | return Objects.nonNull(ref.getValue()); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/CatFile.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.converter.IdConverter; 6 | import lombok.Data; 7 | import lombok.extern.slf4j.Slf4j; 8 | import picocli.CommandLine; 9 | 10 | import java.util.concurrent.Callable; 11 | 12 | /** 13 | * @author rezeros.github.io 14 | */ 15 | @Data 16 | @Slf4j 17 | @CommandLine.Command(name = "cat-file") 18 | public class CatFile implements Callable { 19 | 20 | @CommandLine.Parameters(index = "0", converter = IdConverter.class) 21 | private String id; 22 | 23 | @CommandLine.Parameters(index = "1", defaultValue = "blob") 24 | private String type; 25 | 26 | @Override 27 | public String call() { 28 | final String fileContent = ZitContext.getObjectAsString(id, type); 29 | log.info(fileContent); 30 | return fileContent; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Checkout.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.data.CommitObject; 6 | import club.qqtim.data.RefValue; 7 | import lombok.extern.slf4j.Slf4j; 8 | import picocli.CommandLine; 9 | 10 | import java.nio.file.Path; 11 | 12 | /** 13 | * @title: Checkout 14 | * @Author rezeros.github.io 15 | * @Date: 2020/11/7 16 | * @Version 1.0.0 17 | */ 18 | @lombok.Data 19 | @Slf4j 20 | @CommandLine.Command(name = "checkout") 21 | public class Checkout implements Runnable { 22 | 23 | 24 | @CommandLine.Parameters(index = "0") 25 | private String ref; 26 | 27 | 28 | 29 | @Override 30 | public void run() { 31 | checkout(this.ref); 32 | } 33 | 34 | 35 | /** 36 | * checkout command generate module by the commit describe 37 | * @param name could be head alias, hash and ref(branch, tags, HEAD...) 38 | */ 39 | private void checkout(String name) { 40 | String id = ZitContext.getId(name); 41 | // get the name reference commit 42 | final CommitObject commit = Commit.getCommit(id); 43 | ReadTree.readTree(commit.getTree(), true); 44 | 45 | RefValue refValue; 46 | if (Branch.existBranch(name)) { 47 | refValue = new RefValue(true, String.format(ConstantVal.BASE_REFS_HEADS_PATH, name)); 48 | } else { 49 | refValue = new RefValue(false, id); 50 | } 51 | ZitContext.updateRef(ConstantVal.HEAD, refValue,false); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Commit.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.data.CommitObject; 6 | import club.qqtim.data.RefValue; 7 | import com.google.common.base.Charsets; 8 | import lombok.Data; 9 | import picocli.CommandLine; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | import java.util.concurrent.Callable; 15 | 16 | /** 17 | * @title: Commit 18 | * @Author rezeros.github.io 19 | * @Date: 2020/10/31 20 | * @Version 1.0.0 21 | */ 22 | @Data 23 | @CommandLine.Command(name = "commit") 24 | public class Commit implements Callable { 25 | 26 | @CommandLine.Option(names = {"-m", "--message"}, description = "do commit", required = true) 27 | private boolean commit; 28 | 29 | @CommandLine.Parameters(index = "0") 30 | private String message; 31 | 32 | 33 | @Override 34 | public String call() { 35 | return commit(); 36 | } 37 | 38 | private String commit() { 39 | // calc the commit message 40 | WriteTree writeTree = new WriteTree(); 41 | String commitMessage = String.format("%s %s\n", ConstantVal.TREE, writeTree.call()); 42 | 43 | // set last commit as parent commit 44 | String headId = ZitContext.getRef(ConstantVal.HEAD).getValue(); 45 | if (headId != null) { 46 | commitMessage += String.format("%s %s\n", ConstantVal.PARENT, headId); 47 | } 48 | 49 | String mergedHead = ZitContext.getRef(ConstantVal.MERGE_HEAD).getValue(); 50 | if (mergedHead != null) { 51 | commitMessage += String.format("%s %s\n", ConstantVal.PARENT, mergedHead); 52 | ZitContext.deleteRef(ConstantVal.MERGE_HEAD, false); 53 | } 54 | 55 | // write commit message 56 | commitMessage += String.format("\n%s\n", message); 57 | 58 | // write commit object and return commit id 59 | final String commitId = HashObject.hashObject(commitMessage.getBytes(Charsets.UTF_8), ConstantVal.COMMIT); 60 | ZitContext.updateRef(ConstantVal.HEAD, new RefValue(false, commitId)); 61 | return commitId; 62 | } 63 | 64 | 65 | public static CommitObject getCommit(String id) { 66 | final byte[] commit = ZitContext.getObject(id, ConstantVal.COMMIT); 67 | assert commit != null; 68 | final String commitContent = new String(commit, Charsets.UTF_8); 69 | final String[] lines = commitContent.split(ConstantVal.NEW_LINE); 70 | 71 | List parents = new ArrayList<>(); 72 | CommitObject commitObject = new CommitObject(); 73 | Arrays.stream(lines).forEach(line -> { 74 | final String[] fields = line.split(ConstantVal.SINGLE_SPACE); 75 | if (ConstantVal.TREE.equals(fields[0])) { 76 | commitObject.setTree(fields[1]); 77 | } 78 | if (ConstantVal.PARENT.equals(fields[0])) { 79 | parents.add(fields[1]); 80 | } 81 | }); 82 | commitObject.setParents(parents); 83 | commitObject.setMessage(commitContent); 84 | return commitObject; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Diff.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.diff.DiffUtil; 6 | import club.qqtim.util.FileUtil; 7 | import com.google.gson.Gson; 8 | import com.google.gson.reflect.TypeToken; 9 | import lombok.Data; 10 | import lombok.extern.slf4j.Slf4j; 11 | import picocli.CommandLine; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.nio.file.Files; 16 | import java.nio.file.Path; 17 | import java.nio.file.Paths; 18 | import java.util.Collections; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | import java.util.concurrent.Callable; 22 | import java.util.function.Function; 23 | import java.util.stream.Collectors; 24 | 25 | /** 26 | * show what changes in working directory since last commit 27 | * 28 | * @title: Diff 29 | * @Author rezeros.github.io 30 | * @Date: 2020/12/26 31 | * @Version 1.0.0 32 | */ 33 | @Data 34 | @Slf4j 35 | @CommandLine.Command(name = "diff") 36 | public class Diff implements Callable { 37 | 38 | @CommandLine.Option(names = {"--cached"}) 39 | private boolean cached = false; 40 | 41 | 42 | @CommandLine.Option(names = {"--commit"}) 43 | private String commit; 44 | 45 | 46 | @Override 47 | public String call() { 48 | String objectId; 49 | Map treeFrom = new HashMap<>(16), treeTo; 50 | if (this.commit != null) { 51 | objectId = ZitContext.getId(this.commit); 52 | treeFrom = ReadTree.getTree(Commit.getCommit(objectId).getTree()); 53 | } 54 | final String indexContent = FileUtil.getFileAsString(ConstantVal.INDEX, ConstantVal.NONE); 55 | 56 | if (cached) { 57 | treeTo = new Gson().fromJson(indexContent, new TypeToken>(){}.getType()); 58 | if (this.commit == null) { 59 | objectId = ZitContext.getId(ConstantVal.HEAD_ALIAS); 60 | treeFrom = ReadTree.getTree(Commit.getCommit(objectId).getTree()); 61 | } 62 | } else { 63 | treeTo = Diff.getWorkingTree(); 64 | if (this.commit == null) { 65 | treeFrom = new Gson().fromJson(indexContent, new TypeToken>(){}.getType()); 66 | } 67 | } 68 | 69 | final String diffChanges = DiffUtil.diffTrees(treeFrom, treeTo); 70 | log.info(diffChanges); 71 | return diffChanges; 72 | } 73 | 74 | public static Map getWorkingTree() { 75 | final Path basePath = Paths.get(ConstantVal.BASE_PATH); 76 | try { 77 | return FileUtil.walk(basePath, Integer.MAX_VALUE) 78 | .filter(Files::isRegularFile) 79 | .map(path -> basePath.relativize(path).toString()) 80 | .filter(ZitContext::isNotIgnored) 81 | .collect(Collectors.toMap(key -> basePath.resolve(key).toString(), path -> { 82 | final File file = new File(basePath.resolve(path).toString()); 83 | final byte[] fileContent = FileUtil.getFileAsBytes(file); 84 | return HashObject.hashObject(fileContent, ConstantVal.BLOB); 85 | })); 86 | } catch (IOException e) { 87 | log.error(e.toString()); 88 | } 89 | return Collections.emptyMap(); 90 | } 91 | 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Fetch.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.data.RefObjValue; 6 | import club.qqtim.data.RefObject; 7 | import club.qqtim.data.RefValue; 8 | import club.qqtim.util.FileUtil; 9 | import lombok.Data; 10 | import lombok.extern.slf4j.Slf4j; 11 | import picocli.CommandLine; 12 | 13 | import java.nio.file.Path; 14 | import java.nio.file.Paths; 15 | import java.util.List; 16 | import java.util.stream.Collectors; 17 | 18 | /** 19 | * Note that real Git supports multiple remotes, and each remote has a name. 20 | * For example, if there is a remote named "origin", its branches would go under refs/remote/origin/. 21 | * But in zit we assume for simplicity that there's only one remote and just put its branches under refs/remote/. 22 | * @title: Fetch 23 | * @Author rezeros.github.io 24 | * @Date: 2021/1/1 25 | * @Version 1.0.0 26 | */ 27 | @Data 28 | @Slf4j 29 | @CommandLine.Command(name = "fetch") 30 | public class Fetch implements Runnable { 31 | 32 | @CommandLine.Parameters(index = "0") 33 | private String remote; 34 | 35 | 36 | /** 37 | * if the remote repository has refs/heads/master we're going to save it locally as refs/remote/master 38 | */ 39 | private static final String REMOTE_REFS_BASE = "refs/heads"; 40 | private static final String LOCAL_REFS_BASE = "refs/remote"; 41 | 42 | @Override 43 | public void run() { 44 | fetch(remote); 45 | } 46 | 47 | public static void fetch(String remotePath) { 48 | 49 | { 50 | 51 | log.debug("Will fetch the following refs:"); 52 | 53 | final String currentDir = FileUtil.getCurrentDir(); 54 | FileUtil.setRootPathContext(remotePath); 55 | 56 | final List remoteRefs = getRemoteRefs(remotePath, REMOTE_REFS_BASE); 57 | 58 | final List objectIds = ZitContext.iteratorObjectsInCommits( 59 | remoteRefs.stream().map(RefObjValue::getValue).distinct().collect(Collectors.toList())); 60 | 61 | FileUtil.setRootPathContext(currentDir); 62 | 63 | objectIds.forEach(objectId -> ZitContext.fetchObjectIfMissing(objectId, remotePath)); 64 | 65 | remoteRefs.forEach(remoteRef -> { 66 | final String remoteName = remoteRef.getRefName(); 67 | final Path refName = Paths.get(REMOTE_REFS_BASE).relativize(Paths.get(remoteName)); 68 | 69 | ZitContext.updateRef(Paths.get(LOCAL_REFS_BASE).resolve(refName).toString(), 70 | new RefValue(false, remoteRef.getValue())); 71 | }); 72 | } 73 | 74 | } 75 | 76 | public static List getRemoteRefs(String remotePath) { 77 | final String currentDir = FileUtil.getCurrentDir(); 78 | FileUtil.setRootPathContext(remotePath); 79 | 80 | final List remoteRefs = getRemoteRefs(remotePath, ConstantVal.EMPTY); 81 | 82 | FileUtil.setRootPathContext(currentDir); 83 | 84 | return remoteRefs; 85 | } 86 | 87 | public static List getRemoteRefs(String remotePath, String prefix) { 88 | 89 | final List refObjects = ZitContext.iteratorRefs(prefix); 90 | 91 | return refObjects.stream().map(refObject -> { 92 | final RefObjValue refObjValue = new RefObjValue(); 93 | refObjValue.setRefName(refObject.getRefName()); 94 | refObjValue.setValue(refObject.getRefValue().getValue()); 95 | return refObjValue; 96 | }).collect(Collectors.toList()); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/HashObject.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.util.FileUtil; 5 | import com.google.common.base.Charsets; 6 | import com.google.common.io.Files; 7 | import com.google.common.primitives.Bytes; 8 | import com.google.common.primitives.Chars; 9 | import lombok.Data; 10 | import lombok.extern.slf4j.Slf4j; 11 | import picocli.CommandLine; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.math.BigInteger; 16 | import java.security.MessageDigest; 17 | import java.security.NoSuchAlgorithmException; 18 | import java.util.concurrent.Callable; 19 | 20 | 21 | /** 22 | * @author rezeros.github.io 23 | */ 24 | @Data 25 | @Slf4j 26 | @CommandLine.Command(name = "hash-object") 27 | public class HashObject implements Callable { 28 | 29 | @CommandLine.Parameters(index = "0") 30 | private File file; 31 | 32 | @CommandLine.Parameters(index = "1", defaultValue = "blob") 33 | private String type; 34 | 35 | @Override 36 | public String call() { 37 | return doHashObject(); 38 | } 39 | 40 | private String doHashObject() { 41 | byte[] fileContents = FileUtil.getFileAsBytes(file); 42 | return hashObject(fileContents, this.type); 43 | } 44 | 45 | public static String hashObject(byte[] fileContents) { 46 | return hashObject(fileContents, ConstantVal.BLOB); 47 | } 48 | 49 | /** 50 | * get hash of file content and create it with type describe. 51 | * type could be: blob, tree, commit 52 | */ 53 | public static String hashObject(byte[] fileContents, String type) { 54 | byte[] targetFileContents = Bytes.concat(type.getBytes(Charsets.UTF_8), Chars.toByteArray(ConstantVal.NULL_CHAR), fileContents); 55 | byte[] digest = new byte[0]; 56 | try { 57 | digest = MessageDigest.getInstance(ConstantVal.HASH_ALGORITHM).digest(targetFileContents); 58 | } catch (NoSuchAlgorithmException e) { 59 | log.error("no such algorithm "); 60 | } 61 | 62 | // convert digest bytes to hex string 63 | String objectId = new BigInteger(1, digest).toString(16); 64 | log.info("objectId with {} type is {}", type, objectId); 65 | // create file with file name as object id 66 | FileUtil.createFile(targetFileContents, ConstantVal.OBJECTS_DIR + "/" + objectId); 67 | return objectId; 68 | } 69 | 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Init.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.data.RefValue; 6 | import lombok.Data; 7 | import picocli.CommandLine; 8 | 9 | /** 10 | * @author rezeros.github.io 11 | * init .zit directory 12 | */ 13 | @Data 14 | @CommandLine.Command(name = "init") 15 | public class Init implements Runnable { 16 | @CommandLine.Option(names = "init", description = "zit init") 17 | private boolean init; 18 | 19 | @Override 20 | public void run() { 21 | ZitContext.init(); 22 | final RefValue refValue = new RefValue(true, String.format(ConstantVal.BASE_REFS_HEADS_PATH, ConstantVal.DEFAULT_BRANCH)); 23 | ZitContext.updateRef(ConstantVal.HEAD, refValue); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Lg.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.context.ZitContext; 4 | import club.qqtim.data.CommitObject; 5 | import club.qqtim.data.RefObject; 6 | import club.qqtim.data.RefValue; 7 | import lombok.extern.slf4j.Slf4j; 8 | import picocli.CommandLine; 9 | 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import java.util.Objects; 13 | import java.util.Set; 14 | import java.util.concurrent.Callable; 15 | 16 | /** 17 | * @title: Lg 18 | * @Author rezeros.github.io 19 | * @Date: 2020/12/7 20 | * @Version 1.0.0 21 | * https://www.leshenko.net/p/ugit/#k-render-graph 22 | */ 23 | @Slf4j 24 | @lombok.Data 25 | @CommandLine.Command(name = "lg") 26 | public class Lg implements Callable { 27 | 28 | 29 | @Override 30 | public String call() { 31 | StringBuilder dotGraph = new StringBuilder("digraph commits {\n"); 32 | 33 | 34 | Set idsSet = new HashSet<>(); 35 | final List refObjects = ZitContext.iteratorRefs(false); 36 | for (RefObject refObject : refObjects) { 37 | dotGraph.append(String.format("\"{%s}\" [shape=note]\n", refObject.getRefName())); 38 | final RefValue refValue = refObject.getRefValue(); 39 | dotGraph.append(String.format("\"{%s}\" -> \"{%s}\"\n", refObject.getRefName(), refValue.getValue())); 40 | if (!refValue.getSymbolic()) { 41 | idsSet.add(refObject.getRefValue().getValue()); 42 | } 43 | } 44 | final List refIds = ZitContext.iteratorCommitsAndParents(idsSet); 45 | refIds.forEach(refId -> { 46 | final CommitObject commit = Commit.getCommit(refId); 47 | final String shapeBox = String.format("\"{%s}\" [shape=box style=filled label=\"{%s}\"]\n", refId, refId.substring(0, 11)); 48 | dotGraph.append(shapeBox); 49 | if (Objects.nonNull(commit.getParents()) && !commit.getParents().isEmpty()) { 50 | commit.getParents().forEach(parent -> { 51 | dotGraph.append(String.format("\"{%s}\" -> \"{%s}\"\n", refId, parent)); 52 | }); 53 | } 54 | }); 55 | dotGraph.append("}"); 56 | log.info(dotGraph.toString()); 57 | 58 | //todo: output rendered image to the screen 59 | return null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Log.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.converter.IdConverter; 5 | import club.qqtim.data.CommitObject; 6 | import club.qqtim.context.ZitContext; 7 | import club.qqtim.data.RefObject; 8 | import lombok.extern.slf4j.Slf4j; 9 | import picocli.CommandLine; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.stream.Collectors; 17 | 18 | /** 19 | * @title: Log 20 | * @Author rezeros.github.io 21 | * @Date: 2020/10/31 22 | * @Version 1.0.0 23 | */ 24 | 25 | @lombok.Data 26 | @Slf4j 27 | @CommandLine.Command(name = "log") 28 | public class Log implements Runnable{ 29 | 30 | @CommandLine.Parameters(index = "0", defaultValue = ConstantVal.HEAD_ALIAS, converter = IdConverter.class) 31 | private String id; 32 | 33 | @Override 34 | public void run() { 35 | Map> refValueName = new HashMap<>(8); 36 | 37 | final List refObjects = ZitContext.iteratorRefs(); 38 | refObjects.forEach(refObject -> { 39 | final String refName = refObject.getRefName(); 40 | final String value = refObject.getRefValue().getValue(); 41 | refValueName.putIfAbsent(value, new ArrayList<>()); 42 | final List existRefNames = refValueName.get(value); 43 | existRefNames.add(refName); 44 | }); 45 | 46 | 47 | // if no args, set HEAD 48 | // else use tag or hash as object id 49 | final List idList = ZitContext.iteratorCommitsAndParents(Collections.singletonList(id)); 50 | idList.forEach(objectId -> { 51 | CommitObject commit = Commit.getCommit(objectId); 52 | final List refNames = refValueName.getOrDefault(objectId, Collections.emptyList()); 53 | printCommit(objectId, commit, refNames); 54 | }); 55 | } 56 | 57 | public static void printCommit(String objectId, CommitObject commit) { 58 | printCommit(objectId, commit, Collections.emptyList()); 59 | } 60 | 61 | public static void printCommit(String objectId, CommitObject commit, List refNames) { 62 | String refsStr = String.join(",", refNames); 63 | log.info(String.format("%s %s (%s)\n", ConstantVal.COMMIT, objectId, refsStr)); 64 | log.info(String.format("%s\n", commit.getMessage())); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Merge.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.converter.IdConverter; 6 | import club.qqtim.data.CommitObject; 7 | import club.qqtim.data.RefValue; 8 | import club.qqtim.diff.DiffUtil; 9 | import club.qqtim.util.FileUtil; 10 | import com.google.gson.Gson; 11 | import lombok.extern.slf4j.Slf4j; 12 | import picocli.CommandLine; 13 | 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.Map; 16 | 17 | /** 18 | * @title: Merge 19 | * @Author rezeros.github.io 20 | * @Date: 2020/12/26 21 | * @Version 1.0.0 22 | */ 23 | @lombok.Data 24 | @Slf4j 25 | @CommandLine.Command(name = "merge") 26 | public class Merge implements Runnable { 27 | 28 | @CommandLine.Parameters(index = "0", converter = IdConverter.class) 29 | private String other; 30 | 31 | @Override 32 | public void run() { 33 | final String headRefCommitId = ZitContext.getRef(ConstantVal.HEAD).getValue(); 34 | 35 | final String mergeBase = MergeBase.getMergeBase(this.other, headRefCommitId); 36 | final CommitObject otherCommit = Commit.getCommit(this.other); 37 | 38 | if (headRefCommitId.equals(mergeBase)) { 39 | ReadTree.readTree(otherCommit.getTree(), true); 40 | ZitContext.updateRef(ConstantVal.HEAD, new RefValue(false, this.other)); 41 | log.info("Fast-forward merge, no need to commit"); 42 | return; 43 | } 44 | 45 | ZitContext.updateRef(ConstantVal.MERGE_HEAD, new RefValue(false, this.other)); 46 | 47 | final CommitObject mergeBaseCommit = Commit.getCommit(mergeBase); 48 | final CommitObject headCommit = Commit.getCommit(headRefCommitId); 49 | readTreeMerged(mergeBaseCommit.getTree(), headCommit.getTree(), otherCommit.getTree(), true); 50 | log.info("merged in working tree\nPlease commit"); 51 | } 52 | 53 | private void readTreeMerged(String baseTree, String headTree, String otherTree) { 54 | readTreeMerged(baseTree, headTree, otherTree, false); 55 | } 56 | 57 | 58 | private void readTreeMerged(String baseTree, String headTree, String otherTree, boolean updateWorking) { 59 | 60 | Map pathBlobs = DiffUtil.mergeTrees( 61 | ReadTree.getTree(baseTree), ReadTree.getTree(headTree), ReadTree.getTree(otherTree)); 62 | String fileContent = new Gson().toJson(pathBlobs); 63 | if (updateWorking) { 64 | ReadTree.checkoutIndex(pathBlobs); 65 | } 66 | 67 | FileUtil.createFile(fileContent, ConstantVal.INDEX); 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/MergeBase.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.context.ZitContext; 4 | import club.qqtim.converter.IdConverter; 5 | import lombok.extern.slf4j.Slf4j; 6 | import picocli.CommandLine; 7 | 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | /** 13 | * @title: MergeBase 14 | * @Author rezeros.github.io 15 | * @Date: 2020/12/31 16 | * @Version 1.0.0 17 | */ 18 | @lombok.Data 19 | @Slf4j 20 | @CommandLine.Command(name = "merge-base") 21 | public class MergeBase implements Runnable { 22 | 23 | 24 | @CommandLine.Parameters(index = "0", converter = IdConverter.class) 25 | private String commitFirst; 26 | 27 | @CommandLine.Parameters(index = "1", converter = IdConverter.class) 28 | private String commitSecond; 29 | 30 | 31 | @Override 32 | public void run() { 33 | 34 | final String mergeBase = getMergeBase(commitFirst, commitSecond); 35 | log.debug("Compute common ancestor of a commit: {}", mergeBase); 36 | 37 | } 38 | 39 | /** 40 | * @param commitFirst first commit 41 | * @param commitSecond second commit 42 | * @return Compute common ancestor of a commit 43 | */ 44 | public static String getMergeBase(String commitFirst, String commitSecond) { 45 | final List parentIdsOfFirstCommit = ZitContext 46 | .iteratorCommitsAndParents(Collections.singleton(commitFirst)) 47 | .stream().distinct().collect(Collectors.toList()); 48 | 49 | final List parentIdsOfSecondCommit = ZitContext.iteratorCommitsAndParents(Collections.singleton(commitSecond)); 50 | 51 | for (String secondId : parentIdsOfSecondCommit) { 52 | if (parentIdsOfFirstCommit.contains(secondId)) { 53 | return secondId; 54 | } 55 | } 56 | return null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Push.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.data.RefObjValue; 6 | import club.qqtim.data.RefValue; 7 | import club.qqtim.util.FileUtil; 8 | import com.sun.org.apache.xml.internal.utils.PrefixResolver; 9 | import lombok.extern.slf4j.Slf4j; 10 | import picocli.CommandLine; 11 | 12 | import java.util.Collections; 13 | import java.util.HashSet; 14 | import java.util.List; 15 | import java.util.Objects; 16 | import java.util.Set; 17 | import java.util.stream.Collectors; 18 | 19 | /** 20 | * @title: Push 21 | * @Author ReZeroS 22 | * @Date: 2021/2/1 23 | * @Version 1.0.0 24 | * 25 | * Instead of pushing all objects in remote.push(), let's add a simple check to determine which objects the remote has: 26 | * 27 | * Take all remote refs that exist on the remote. Since the remote might have refs that point to branches that we didn't pull yet, filter out all refs that point to unknown OIDs. 28 | * Collect into a set all objects that are reachable from the known remote OIDs. 29 | * Collect into a set all objects that are reachable from the local branch that is being pushed. 30 | * Take the difference between the two sets. This effectively gives us the objects that are needed to fully describe the pushed branch but are missing from the remote. 31 | * Push those missing objects. 32 | */ 33 | @Slf4j 34 | @lombok.Data 35 | @CommandLine.Command(name = "push") 36 | public class Push implements Runnable{ 37 | 38 | @CommandLine.Parameters(index = "0") 39 | private String remote; 40 | 41 | @CommandLine.Parameters(index = "1") 42 | private String branch; 43 | 44 | 45 | 46 | 47 | @Override 48 | public void run() { 49 | push(remote, String.format(ConstantVal.BASE_REFS_HEADS_PATH, branch)); 50 | } 51 | 52 | private void push(String remotePath, String refName) { 53 | final List remoteRefs = Fetch.getRemoteRefs(remotePath); 54 | final RefObjValue remoteRef = remoteRefs.stream().filter(e -> e.getRefName().equals(refName)).findAny().orElse(null); 55 | 56 | final String localRef = ZitContext.getRef(refName).getValue(); 57 | 58 | // new branch or have common parent commit 59 | if(!(Objects.isNull(remoteRef) || isAncestorOf(localRef, remoteRef.getValue()))){ 60 | log.error("force pushed fail"); 61 | throw new IllegalArgumentException("can be forced push"); 62 | 63 | } 64 | 65 | final String currentDir = FileUtil.getCurrentDir(); 66 | FileUtil.setRootPathContext(remotePath); 67 | 68 | final List knownRemoteRefs = remoteRefs.stream().map(RefObjValue::getValue).filter(ZitContext::objectExists).collect(Collectors.toList()); 69 | final Set remoteObjects = new HashSet<>(ZitContext.iteratorObjectsInCommits(knownRemoteRefs)); 70 | 71 | FileUtil.setRootPathContext(currentDir); 72 | 73 | 74 | final Set localObjects = new HashSet<>(ZitContext.iteratorObjectsInCommits(Collections.singletonList(localRef))); 75 | final List objectsToPush = localObjects.stream().filter(object -> !remoteObjects.contains(object)).collect(Collectors.toList()); 76 | 77 | // only push missing objects 78 | for (String objectId : objectsToPush) { 79 | ZitContext.pushObject(objectId, remotePath); 80 | } 81 | 82 | FileUtil.setRootPathContext(remotePath); 83 | { 84 | ZitContext.updateRef(refName, new RefValue(false, localRef)); 85 | } 86 | FileUtil.setRootPathContext(currentDir); 87 | 88 | } 89 | 90 | 91 | 92 | /** 93 | * To prevent it from happening we're going to allow pushing only in two cases: 94 | * 95 | * The ref that we're pushing doesn't exist yet on the remote. It means that it's a new branch and there is no risk of overwriting other's work. 96 | * 97 | * If the remote ref does exist, it must point to a commit that is an ancestor of the pushed ref. 98 | * This ancestry means that the local commit is based on the remote commit, 99 | * which means that the remote commit not getting overwritten, since it's part of the history of the newly pushed commit. 100 | */ 101 | private boolean isAncestorOf(String localRef, String maybeAncestor) { 102 | return ZitContext.iteratorObjectsInCommits(Collections.singletonList(localRef)).contains(maybeAncestor); 103 | } 104 | 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/ReadTree.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | 4 | import club.qqtim.common.ConstantVal; 5 | import club.qqtim.context.ZitContext; 6 | import club.qqtim.data.ZitObject; 7 | import club.qqtim.util.FileUtil; 8 | import com.google.gson.Gson; 9 | import lombok.Data; 10 | import lombok.extern.slf4j.Slf4j; 11 | import picocli.CommandLine; 12 | 13 | import java.util.Arrays; 14 | import java.util.Collections; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Objects; 19 | import java.util.stream.Collectors; 20 | 21 | import static club.qqtim.util.FileUtil.emptyCurrentDir; 22 | 23 | /** 24 | * @author rezeros.github.io 25 | * extract the tree, opposite to write-tree 26 | */ 27 | @Data 28 | @Slf4j 29 | @CommandLine.Command(name = "read-tree") 30 | public class ReadTree implements Runnable { 31 | 32 | /** 33 | * tree id 34 | */ 35 | @CommandLine.Parameters(index = "0") 36 | private String hash; 37 | 38 | @Override 39 | public void run() { 40 | readTree(hash); 41 | } 42 | 43 | 44 | public static void readTree(String id){ 45 | readTree(id, false); 46 | } 47 | 48 | 49 | /** 50 | * updateWorking means whether reset the working area to the index description 51 | */ 52 | public static void readTree(String id, boolean updateWorking){ 53 | 54 | Map tree = getTree(id); 55 | 56 | String fileContent = new Gson().toJson(tree); 57 | FileUtil.createFile(fileContent, ConstantVal.INDEX); 58 | 59 | if (updateWorking) { 60 | checkoutIndex(tree); 61 | } 62 | 63 | } 64 | 65 | 66 | 67 | public static List iteratorTreeEntries(String treeId) { 68 | final String tree = ZitContext.getObjectAsString(treeId, "tree"); 69 | assert tree != null; 70 | return Arrays.stream(tree.split(ConstantVal.NEW_LINE)) 71 | .map(object -> object.split(ConstantVal.SINGLE_SPACE)) 72 | .map(objectFields -> new ZitObject(objectFields[0], objectFields[1], objectFields[2])) 73 | .collect(Collectors.toList()); 74 | } 75 | 76 | /** 77 | * only one layer 78 | * [ 79 | * {key: path, val: object id}, 80 | * {...} 81 | * ] 82 | */ 83 | public static Map getTree(String treeId) { 84 | return getTree(treeId, ConstantVal.EMPTY); 85 | } 86 | 87 | private static Map getTree(String treeId, String basePath) { 88 | if (Objects.isNull(treeId)) { 89 | return Collections.emptyMap(); 90 | } 91 | final List zitObjects = iteratorTreeEntries(treeId); 92 | Map map = new HashMap<>(16); 93 | zitObjects.forEach(zitObject -> { 94 | String path = basePath + zitObject.getName(); 95 | if (ConstantVal.BLOB.equals(zitObject.getType())) { 96 | map.put(path, zitObject.getObjectId()); 97 | } else if (ConstantVal.TREE.equals(zitObject.getType())) { 98 | final Map tree = getTree(zitObject.getObjectId(), String.format("%s/", path)); 99 | map.putAll(tree); 100 | } 101 | }); 102 | return map; 103 | } 104 | 105 | 106 | 107 | 108 | /** 109 | * index must only have one layer 110 | */ 111 | public static void checkoutIndex(Map index) { 112 | // clean current dir 113 | emptyCurrentDir(); 114 | 115 | for (Map.Entry pathObjectId : index.entrySet()) { 116 | String path = pathObjectId.getKey(); 117 | String objectId = pathObjectId.getValue(); 118 | FileUtil.createParentDirs(path); 119 | final byte[] objectBytes = ZitContext.getObject(objectId); 120 | FileUtil.createFile(objectBytes, path); 121 | } 122 | 123 | } 124 | 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Reset.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.converter.IdConverter; 6 | import club.qqtim.data.RefValue; 7 | import lombok.extern.slf4j.Slf4j; 8 | import picocli.CommandLine; 9 | 10 | /** 11 | * @title: Reset 12 | * @Author rezeros.github.io 13 | * @Date: 2020/12/13 14 | * @Version 1.0.0 15 | */ 16 | @lombok.Data 17 | @Slf4j 18 | @CommandLine.Command(name = "reset") 19 | public class Reset implements Runnable { 20 | 21 | @CommandLine.Parameters(index = "0", converter = IdConverter.class) 22 | private String commit; 23 | 24 | @Override 25 | public void run() { 26 | ZitContext.updateRef(ConstantVal.HEAD, new RefValue(false, commit)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Show.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.converter.IdConverter; 5 | import club.qqtim.data.CommitObject; 6 | import club.qqtim.diff.DiffUtil; 7 | import lombok.extern.slf4j.Slf4j; 8 | import picocli.CommandLine; 9 | 10 | import java.util.Objects; 11 | 12 | /** 13 | * @title: Show 14 | * @Author rezeros.github.io 15 | * @Date: 2020/12/13 16 | * @Version 1.0.0 17 | */ 18 | @lombok.Data 19 | @Slf4j 20 | @CommandLine.Command(name = "show") 21 | public class Show implements Runnable { 22 | 23 | @CommandLine.Parameters(index = "0", defaultValue = ConstantVal.HEAD_ALIAS, converter = IdConverter.class) 24 | private String id; 25 | 26 | @Override 27 | public void run() { 28 | final CommitObject commit = Commit.getCommit(id); 29 | 30 | String parentTree = null; 31 | if (Objects.nonNull(commit.getParents()) && !commit.getParents().isEmpty()) { 32 | parentTree = Commit.getCommit(commit.getParents().get(0)).getTree(); 33 | } 34 | 35 | Log.printCommit(id, commit); 36 | 37 | final String result = DiffUtil.diffTrees( 38 | ReadTree.getTree(parentTree), ReadTree.getTree(commit.getTree()) 39 | ); 40 | 41 | log.info(result); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Status.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.diff.DiffUtil; 6 | import club.qqtim.diff.SimplyChange; 7 | import club.qqtim.util.FileUtil; 8 | import com.google.gson.Gson; 9 | import com.google.gson.reflect.TypeToken; 10 | import lombok.extern.slf4j.Slf4j; 11 | import picocli.CommandLine; 12 | 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Objects; 16 | 17 | /** 18 | * @title: Status 19 | * @Author rezeros.github.io 20 | * @Date: 2020/12/12 21 | * @Version 1.0.0 22 | */ 23 | @lombok.Data 24 | @Slf4j 25 | @CommandLine.Command(name = "status") 26 | public class Status implements Runnable { 27 | 28 | @Override 29 | public void run() { 30 | final String headId = ZitContext.getId(ConstantVal.HEAD_ALIAS); 31 | final String branchName = ZitContext.getBranchName(); 32 | if (Objects.nonNull(branchName)) { 33 | log.info("On branch * {}", branchName); 34 | } else { 35 | if (headId != null) { 36 | log.info("HEAD detached at {}", headId.substring(0, 11)); 37 | } 38 | } 39 | 40 | final String mergeHeadId = ZitContext.getRef(ConstantVal.MERGE_HEAD).getValue(); 41 | if (Objects.nonNull(mergeHeadId)) { 42 | log.info("Merging with {}", mergeHeadId.substring(0, 11)); 43 | } 44 | 45 | 46 | 47 | final String indexContent = FileUtil.getFileAsString(ConstantVal.INDEX, ConstantVal.NONE); 48 | 49 | Map indexItems = new Gson().fromJson(indexContent, new TypeToken>(){}.getType()); 50 | 51 | if (headId != null) { 52 | log.info("\nChanges to be committed:\n"); 53 | final String headTree = Commit.getCommit(headId).getTree(); 54 | final List toBeCommitted = DiffUtil.iteratorChangedFiles(ReadTree.getTree(headTree), indexItems); 55 | toBeCommitted.forEach(simplyChange -> log.info(simplyChange.toString())); 56 | } 57 | 58 | 59 | log.info("\nChanges not staged for commit:\n"); 60 | final List notStaged = DiffUtil.iteratorChangedFiles(indexItems, Diff.getWorkingTree()); 61 | notStaged.forEach(simplyChange -> log.info(simplyChange.toString())); 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/Tag.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.converter.IdConverter; 6 | import club.qqtim.data.RefValue; 7 | import lombok.extern.slf4j.Slf4j; 8 | import picocli.CommandLine; 9 | 10 | import static club.qqtim.common.ConstantVal.BASE_REFS_TAGS_PATH; 11 | 12 | /** 13 | * @title: Tag 14 | * @Author rezeros.github.io 15 | * @Date: 2020/11/7 16 | * @Version 1.0.0 17 | */ 18 | @lombok.Data 19 | @Slf4j 20 | @CommandLine.Command(name = "tag") 21 | public class Tag implements Runnable { 22 | 23 | @CommandLine.Parameters(index = "0", description = "name") 24 | private String name; 25 | 26 | @CommandLine.Parameters(index = "1", defaultValue = ConstantVal.HEAD_ALIAS, converter = IdConverter.class, description = "commit id") 27 | private String id; 28 | 29 | 30 | 31 | @Override 32 | public void run() { 33 | if (id != null) { 34 | String tag = String.format(BASE_REFS_TAGS_PATH, name); 35 | ZitContext.updateRef(tag, new RefValue(false, id)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/command/WriteTree.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.command; 2 | 3 | 4 | import club.qqtim.common.ConstantVal; 5 | import club.qqtim.context.ZitContext; 6 | import club.qqtim.data.ZitObject; 7 | import club.qqtim.util.FileUtil; 8 | import com.google.common.base.Charsets; 9 | import com.google.common.base.Joiner; 10 | import com.google.gson.Gson; 11 | import com.google.gson.reflect.TypeToken; 12 | import lombok.Data; 13 | import lombok.extern.slf4j.Slf4j; 14 | import picocli.CommandLine; 15 | 16 | import java.io.File; 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.HashMap; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.concurrent.Callable; 23 | import java.util.stream.Collectors; 24 | 25 | /** 26 | * @author rezeros.github.io 27 | * If hash-object was for storing an individual file, then write-tree is for storing a whole directory. 28 | */ 29 | @Data 30 | @Slf4j 31 | @CommandLine.Command(name = "write-tree") 32 | public class WriteTree implements Callable { 33 | 34 | @Override 35 | public String call() { 36 | return writeTree(); 37 | } 38 | 39 | /** 40 | * init the indexAsTree to visual tree 41 | * @return tree Id 42 | */ 43 | private String writeTree() { 44 | // read the index dict 45 | final String indexContent = FileUtil.getFileAsString(ConstantVal.INDEX, ConstantVal.NONE); 46 | Map indexItems = new Gson().fromJson(indexContent, new TypeToken>(){}.getType()); 47 | 48 | 49 | /* construct the tree map called indexAsTree like the below 50 | { 51 | java: { => val is map => tree 52 | com: { => val is map => tree 53 | file1: objectId => val is string => object 54 | club: { => val is map => tree 55 | file2: objectId => val is string => object 56 | } 57 | } 58 | } 59 | 60 | */ 61 | Map indexAsTree = new HashMap<>(16); 62 | for (Map.Entry pathObjectId : indexItems.entrySet()) { 63 | String path = pathObjectId.getKey(); 64 | String objectId = pathObjectId.getValue(); 65 | List pathAndFile = Arrays.asList(path.split("/")); 66 | List dirPaths = pathAndFile.subList(0, pathAndFile.size() - 1); 67 | String fileName = pathAndFile.get(pathAndFile.size() - 1); 68 | 69 | Map current = indexAsTree; 70 | for (String dirPath : dirPaths) { 71 | current = (Map) current.computeIfAbsent(dirPath, e -> new HashMap<>(2)); 72 | } 73 | current.put(fileName, objectId); 74 | } 75 | 76 | // write objects into zit repository with the reference of above tree 77 | return writeTreeRecursive(indexAsTree); 78 | } 79 | 80 | /** 81 | * this is an easy dfs for writing objects and trees 82 | */ 83 | private String writeTreeRecursive(Map treeDict) { 84 | List zitObjects = new ArrayList<>(); 85 | for (Map.Entry keyVal : treeDict.entrySet()) { 86 | String type, objectId; 87 | String name = keyVal.getKey(); 88 | Object value = keyVal.getValue(); 89 | if (value instanceof Map) { 90 | type = ConstantVal.TREE; 91 | // get tree id 92 | objectId = writeTreeRecursive((Map) value); 93 | } else { 94 | type = ConstantVal.BLOB; 95 | objectId = (String) value; 96 | } 97 | zitObjects.add(new ZitObject(type, objectId, name)); 98 | } 99 | String fileContent = zitObjects.stream().sorted() 100 | .map(e -> String.format("%s %s %s\n", e.getType(), e.getObjectId(), e.getName())) 101 | .collect(Collectors.joining()); 102 | return HashObject.hashObject(fileContent.getBytes(Charsets.UTF_8), ConstantVal.TREE); 103 | } 104 | 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/common/ConstantVal.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.common; 2 | 3 | import club.qqtim.diff.LineObject; 4 | 5 | import java.util.Arrays; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * @author rezeros.github.io 12 | */ 13 | public final class ConstantVal { 14 | public static final String HASH_ALGORITHM = "SHA-1"; 15 | 16 | // action type 17 | public static final String SYNC = "SYNC"; 18 | public static final String PLUS = "PLUS"; 19 | public static final String PLUS_SYMBOL = "+"; 20 | public static final String MINUS = "MINUS"; 21 | public static final String MINUS_SYMBOL = "-"; 22 | public static final String CONFLICT = "CONFLICT"; 23 | 24 | 25 | // ref type 26 | 27 | public static final String HEAD = "HEAD"; 28 | public static final String HEAD_ALIAS = "@"; 29 | 30 | public static final String MERGE_HEAD = "MERGE_HEAD"; 31 | 32 | // param type 33 | 34 | public static final String NONE = "None"; 35 | 36 | 37 | // object type 38 | 39 | public static final String PARENT = "parent"; 40 | 41 | public static final String TREE = "tree"; 42 | 43 | public static final String BLOB = "blob"; 44 | 45 | public static final String COMMIT = "commit"; 46 | 47 | 48 | // symbol type 49 | 50 | public static final char NULL_CHAR = 0; 51 | 52 | public static final String STAR = "*"; 53 | 54 | public static final String EMPTY = ""; 55 | 56 | public static final String NEW_LINE = "\n"; 57 | 58 | public static final String SINGLE_SPACE = " "; 59 | 60 | public static final String UNIX_PATH_SEPARATOR = "/"; 61 | 62 | public static final String BASE_PATH = "./"; 63 | 64 | 65 | public static final String BASE_FORMAT = "%s"; 66 | public static final String BASE_REFS_PATH = "refs/%s"; 67 | public static final String BASE_REFS_TAGS_PATH = "refs/tags/%s"; 68 | 69 | public static final String HEADS_PATH = "refs/heads"; 70 | public static final String BASE_REFS_HEADS_PATH = HEADS_PATH + "/%s"; 71 | 72 | public static final List REF_REGISTRY_DIRECTORIES = 73 | Arrays.asList(BASE_FORMAT, BASE_REFS_PATH, BASE_REFS_TAGS_PATH, BASE_REFS_HEADS_PATH); 74 | 75 | 76 | // default value 77 | 78 | public static final String DEFAULT_BRANCH = "main"; 79 | 80 | public static final String ZIT_DIR = ".zit"; 81 | public static final String OBJECTS_DIR = ZIT_DIR + "/objects"; 82 | public static final String REFS_DIR = "refs"; 83 | public static final String REFS_DIR_REAL = ZIT_DIR + "/" + REFS_DIR; 84 | 85 | public static final String INDEX = ZIT_DIR + "/index"; 86 | 87 | // merge conflict 88 | 89 | public static final String HEAD_CONFLICT = "<<<<<<<<<<"; 90 | public static final String OTHER_CONFLICT = ">>>>>>>>>>"; 91 | public static final String ORIGIN_CONFLICT = "=========="; 92 | 93 | public static Map MERGE_CONFLICT = new HashMap<>(4); 94 | 95 | static { 96 | final LineObject headLine = new LineObject(); 97 | headLine.setAction(CONFLICT); 98 | headLine.setLineContent(HEAD_CONFLICT); 99 | MERGE_CONFLICT.put(HEAD_CONFLICT, headLine); 100 | 101 | final LineObject originLine = new LineObject(); 102 | originLine.setAction(CONFLICT); 103 | originLine.setLineContent(ORIGIN_CONFLICT); 104 | MERGE_CONFLICT.put(ORIGIN_CONFLICT, originLine); 105 | 106 | final LineObject otherLine = new LineObject(); 107 | otherLine.setAction(CONFLICT); 108 | otherLine.setLineContent(OTHER_CONFLICT); 109 | MERGE_CONFLICT.put(OTHER_CONFLICT, otherLine); 110 | 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/common/RegexConstantVal.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.common; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | /** 6 | * @title: RegexContantVal 7 | * @Author rezeros.github.io 8 | * @Date: 2020/12/7 9 | * @Version 1.0.0 10 | */ 11 | public final class RegexConstantVal { 12 | 13 | public static Pattern ALL_HEX = Pattern.compile("[0-9a-z]{40}"); 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/context/Zit.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.context; 2 | 3 | 4 | import club.qqtim.command.Add; 5 | import club.qqtim.command.Branch; 6 | import club.qqtim.command.CatFile; 7 | import club.qqtim.command.Checkout; 8 | import club.qqtim.command.Commit; 9 | import club.qqtim.command.Diff; 10 | import club.qqtim.command.Fetch; 11 | import club.qqtim.command.HashObject; 12 | import club.qqtim.command.Init; 13 | import club.qqtim.command.Lg; 14 | import club.qqtim.command.Log; 15 | import club.qqtim.command.Merge; 16 | import club.qqtim.command.MergeBase; 17 | import club.qqtim.command.Push; 18 | import club.qqtim.command.ReadTree; 19 | import club.qqtim.command.Reset; 20 | import club.qqtim.command.Show; 21 | import club.qqtim.command.Status; 22 | import club.qqtim.command.Tag; 23 | import club.qqtim.command.WriteTree; 24 | import picocli.CommandLine; 25 | 26 | /** 27 | * @author rezeros.github.io 28 | */ 29 | @CommandLine.Command(name = "zit", subcommands = { 30 | Init.class, 31 | HashObject.class, 32 | CatFile.class, 33 | WriteTree.class, 34 | ReadTree.class, 35 | Commit.class, 36 | Log.class, 37 | Checkout.class, 38 | Tag.class, 39 | Lg.class, 40 | Branch.class, 41 | Status.class, 42 | Reset.class, 43 | Show.class, 44 | Diff.class, 45 | Merge.class, 46 | MergeBase.class, 47 | Fetch.class, 48 | Push.class, 49 | Add.class 50 | }) 51 | public class Zit { 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/context/ZitContext.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.context; 2 | 3 | import club.qqtim.command.Commit; 4 | import club.qqtim.command.ReadTree; 5 | import club.qqtim.common.ConstantVal; 6 | import club.qqtim.common.RegexConstantVal; 7 | import club.qqtim.data.CommitObject; 8 | import club.qqtim.data.RefObject; 9 | import club.qqtim.data.RefValue; 10 | import club.qqtim.data.ZitObject; 11 | import club.qqtim.util.FileUtil; 12 | import com.google.common.base.Charsets; 13 | import lombok.extern.slf4j.Slf4j; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.nio.charset.StandardCharsets; 18 | import java.nio.file.Files; 19 | import java.nio.file.Path; 20 | import java.nio.file.Paths; 21 | import java.util.ArrayList; 22 | import java.util.Collection; 23 | import java.util.Collections; 24 | import java.util.Deque; 25 | import java.util.HashSet; 26 | import java.util.LinkedList; 27 | import java.util.List; 28 | import java.util.Objects; 29 | import java.util.Set; 30 | import java.util.stream.Collectors; 31 | 32 | 33 | @Slf4j 34 | public class ZitContext { 35 | 36 | 37 | /** 38 | * behind commit id is the first parent while other parents will at the end before each commit found their first parent 39 | */ 40 | public static List iteratorCommitsAndParents(Collection ids) { 41 | Deque idsDeque = new LinkedList<>(ids); 42 | Set visitedIds = new HashSet<>(); 43 | Set resultSet = new HashSet<>(ids); 44 | 45 | while (!idsDeque.isEmpty()) { 46 | // pop an element 47 | final String id = idsDeque.pollFirst(); 48 | // check if visited this element 49 | if (visitedIds.contains(id)) { 50 | continue; 51 | } 52 | // if not visited, then add it to result and check it's parent 53 | visitedIds.add(id); 54 | resultSet.add(id); 55 | final CommitObject commit = Commit.getCommit(id); 56 | if (Objects.nonNull(commit.getParents()) && !commit.getParents().isEmpty()) { 57 | // todo java ugly slice 58 | idsDeque.offerFirst(commit.getParents().get(0)); 59 | if (commit.getParents().size() > 1) { 60 | final List lastCommits = commit.getParents().subList(1, commit.getParents().size()); 61 | lastCommits.forEach(idsDeque::offerLast); 62 | } 63 | } 64 | } 65 | return new ArrayList<>(resultSet); 66 | } 67 | 68 | 69 | //https://www.leshenko.net/p/ugit/#fetch-remote-refs-objects 70 | 71 | public static void iteratorObjectsInTree(String objectId, Set visited, Collection collections){ 72 | visited.add(objectId); 73 | collections.add(objectId); 74 | final List zitObjects = ReadTree.iteratorTreeEntries(objectId); 75 | for (ZitObject zitObject : zitObjects) { 76 | if (!visited.contains(zitObject.getObjectId())) { 77 | if (ConstantVal.TREE.equals(zitObject.getType())) { 78 | iteratorObjectsInTree(zitObject.getObjectId(), visited, collections); 79 | } else { 80 | visited.add(zitObject.getObjectId()); 81 | collections.add(zitObject.getObjectId()); 82 | } 83 | } 84 | } 85 | } 86 | 87 | //https://www.leshenko.net/p/ugit/#fetch-remote-refs-objects 88 | public static List iteratorObjectsInCommits(List objectIds) { 89 | List result = new ArrayList<>(); 90 | final Set visited = new HashSet<>(); 91 | 92 | final List commitIds = iteratorCommitsAndParents(objectIds); 93 | for (String commitId : commitIds) { 94 | result.add(commitId); 95 | final CommitObject commit = Commit.getCommit(commitId); 96 | if (!visited.contains(commit.getTree())) { 97 | List traverseTree = new ArrayList<>(); 98 | iteratorObjectsInTree(commit.getTree(), visited, traverseTree); 99 | result.addAll(traverseTree); 100 | } 101 | } 102 | return result; 103 | } 104 | 105 | 106 | 107 | 108 | /** 109 | * @param objectId object id 110 | * @return true if the object exist 111 | */ 112 | public static boolean objectExists(String objectId) { 113 | return FileUtil.isFile(Paths.get(ConstantVal.OBJECTS_DIR).resolve(objectId).toString()); 114 | } 115 | 116 | public static void fetchObjectIfMissing(String objectId, String remoteDir) { 117 | if (objectExists(objectId)) { 118 | return; 119 | } 120 | final Path remoteFile = Paths.get(remoteDir).resolve(ConstantVal.OBJECTS_DIR).resolve(objectId); 121 | final Path localFile = Paths.get(ConstantVal.OBJECTS_DIR).resolve(objectId); 122 | FileUtil.copy(remoteFile, localFile); 123 | } 124 | 125 | 126 | public static List iteratorRefs() { 127 | return iteratorRefs(ConstantVal.EMPTY, true); 128 | } 129 | 130 | public static List iteratorRefs(String prefix) { 131 | return iteratorRefs(prefix, true); 132 | } 133 | 134 | public static List iteratorRefs(boolean deference) { 135 | return iteratorRefs(ConstantVal.EMPTY, deference); 136 | } 137 | 138 | 139 | /** 140 | * return all ref objects in the context 141 | * 142 | * @return HEAD and all ref objects in refs directory 143 | */ 144 | public static List iteratorRefs(String prefix, boolean dereference) { 145 | List refs = new ArrayList<>(1); 146 | refs.add(ConstantVal.HEAD); 147 | refs.add(ConstantVal.MERGE_HEAD); 148 | 149 | //get all file relative path from refs/ , like refs/* format 150 | final Path refsPath = Paths.get(ConstantVal.REFS_DIR_REAL); 151 | final Path refsDir = Paths.get(ConstantVal.REFS_DIR); 152 | final List pathList; 153 | try { 154 | pathList = FileUtil.walk(refsPath, Integer.MAX_VALUE) 155 | .filter(Files::isRegularFile) 156 | .map(path -> refsDir.resolve(refsPath.relativize(path)).toString()) 157 | .map(FileUtil::convertUnixPath).collect(Collectors.toList()); 158 | } catch (IOException e) { 159 | return Collections.emptyList(); 160 | } 161 | 162 | refs.addAll(pathList); 163 | 164 | return refs.stream() 165 | .filter(refName -> refName.startsWith(prefix)) 166 | .map(refName -> { 167 | final RefValue ref = getRef(refName, dereference); 168 | if (Objects.isNull(ref.getValue())) { 169 | return null; 170 | } 171 | final RefObject refObject = new RefObject(); 172 | refObject.setRefName(refName); 173 | refObject.setRefValue(ref); 174 | return refObject; 175 | }).filter(Objects::nonNull).collect(Collectors.toList()); 176 | } 177 | 178 | 179 | public static RefValue getRef(String ref) { 180 | return getRef(ref, true); 181 | } 182 | 183 | /** 184 | * dereference it recursively for content ref: 185 | * 186 | * @param ref ref 187 | * @return real ref 188 | */ 189 | public static RefValue getRef(String ref, boolean dereference) { 190 | return getRefInternal(ref, dereference).getRefValue(); 191 | } 192 | 193 | private static RefObject getRefInternal(String ref) { 194 | return getRefInternal(ref, true); 195 | } 196 | 197 | /** 198 | * 1. if ref not exist, return refObject: {refName:ref, refValue: {falseSymbolic, null}} 199 | * it means if the ref not exist, refName will just return the param ref 200 | * 2. dereference is an action symbol to determine 201 | * whether deference the chain to return the final hash id or just return the ref directly 202 | **/ 203 | private static RefObject getRefInternal(String ref, boolean dereference) { 204 | String value = null; 205 | File file = new File(String.format("%s/%s", ConstantVal.ZIT_DIR, ref)); 206 | // read the file first line 207 | if (file.exists()) { 208 | value = FileUtil.readFileFirstLine(file); 209 | if (value == null) { 210 | return null; 211 | } 212 | } 213 | // determine whether it is an direct hash or reference symbolic 214 | boolean symbolic = (value != null && value.startsWith("ref:")); 215 | // if a reference 216 | if (symbolic){ 217 | // get the reference key like ··ref: ref/heads/main·· 218 | // pick up the `ref/heads/main` 219 | value = value.split(":", 2)[1].trim(); 220 | if (dereference) { 221 | return getRefInternal(value); 222 | } 223 | } 224 | // if a direct hash 225 | return new RefObject(ref, new RefValue(symbolic, value)); 226 | } 227 | 228 | public static void updateRef(String ref, RefValue refValue) { 229 | updateRef(ref, refValue, true); 230 | } 231 | 232 | public static void updateRef(String ref, RefValue refValue, boolean dereference) { 233 | // pick up the origin ref value 234 | final String refName = Objects.requireNonNull(getRefInternal(ref, dereference)).getRefName(); 235 | String value; 236 | if (refValue.getSymbolic()) { 237 | value = String.format("ref: %s", refValue.getValue()); 238 | } else { 239 | value = refValue.getValue(); 240 | } 241 | // update the origin ref with the new refValue 242 | FileUtil.createFile(value.getBytes(Charsets.UTF_8), String.format("%s/%s", ConstantVal.ZIT_DIR, refName)); 243 | } 244 | 245 | public static void deleteRef(String ref) { 246 | deleteRef(ref, true); 247 | } 248 | 249 | public static void deleteRef(String ref, boolean dereference) { 250 | final RefObject refInternal = getRefInternal(ref, dereference); 251 | if (Objects.nonNull(refInternal)) { 252 | final String refName = refInternal.getRefName(); 253 | FileUtil.deleteDir(String.format("%s/%s", ConstantVal.ZIT_DIR, refName)); 254 | } 255 | } 256 | 257 | /** 258 | * determine whether ref not exist use deference false 259 | * but get id finally will return the direct hash 260 | */ 261 | public static String getId(String refOrId) { 262 | if (ConstantVal.HEAD_ALIAS.equals(refOrId)) { 263 | refOrId = ConstantVal.HEAD; 264 | } 265 | for (String path : ConstantVal.REF_REGISTRY_DIRECTORIES) { 266 | final String refValue = String.format(path, refOrId); 267 | 268 | // pay attention, check use dereference false 269 | final RefValue ref = ZitContext.getRef(refValue, false); 270 | if (Objects.nonNull(ref.getValue())) { 271 | // while pick up dereference as true 272 | return ZitContext.getRef(refValue, true).getValue(); 273 | } 274 | } 275 | if (RegexConstantVal.ALL_HEX.matcher(refOrId).find()) { 276 | return refOrId; 277 | } 278 | return null; 279 | } 280 | 281 | /** 282 | * get current branch name by read the head pointer ref 283 | */ 284 | public static String getBranchName(){ 285 | final RefValue head = getRef(ConstantVal.HEAD, false); 286 | if (!head.getSymbolic()) { 287 | return null; 288 | } 289 | final String headPath = head.getValue(); 290 | if (headPath.startsWith(ConstantVal.HEADS_PATH)) { 291 | return Paths.get(ConstantVal.HEADS_PATH).relativize(Paths.get(headPath)).toString(); 292 | } 293 | return null; 294 | } 295 | 296 | public static void init() { 297 | initRoot(); 298 | initIndex(); 299 | initObjects(); 300 | } 301 | 302 | private static void initIndex() { 303 | FileUtil.createFile("{}".getBytes(StandardCharsets.UTF_8), ConstantVal.INDEX); 304 | } 305 | 306 | private static void initRoot() { 307 | FileUtil.mkdir(ConstantVal.ZIT_DIR); 308 | } 309 | 310 | private static void initObjects() { 311 | FileUtil.mkdir(ConstantVal.OBJECTS_DIR); 312 | } 313 | 314 | public static byte[] getObject(String hash) { 315 | return getObject(hash, ConstantVal.BLOB); 316 | } 317 | 318 | public static byte[] getObject(String hash, String type) { 319 | String path = ConstantVal.OBJECTS_DIR + "/" + hash; 320 | log.debug("get the content of {} file", path); 321 | try { 322 | return FileUtil.getFileByteSource(path, type).read(); 323 | } catch (IOException e) { 324 | log.error(e.toString()); 325 | } 326 | return null; 327 | } 328 | 329 | public static String getObjectAsString(String hash, String type) { 330 | String path = ConstantVal.OBJECTS_DIR + "/" + hash; 331 | log.debug("get the content of {} file", path); 332 | return FileUtil.getFileAsString(path, type); 333 | } 334 | 335 | /** 336 | * @param path file path 337 | * @return whether it's zit meta file 338 | */ 339 | public static boolean isNotIgnored(String path) { 340 | return !isIgnored(path); 341 | } 342 | 343 | /** 344 | * @param path file path 345 | * @return whether it's zit meta file 346 | */ 347 | public static boolean isIgnored(String path) { 348 | return path != null && 349 | ( 350 | path.startsWith(ConstantVal.ZIT_DIR) 351 | || path.startsWith(".zit") 352 | || path.startsWith("doc") 353 | || path.startsWith("target") 354 | 355 | ); 356 | } 357 | 358 | public static void pushObject(String objectId, String remotePath) { 359 | remotePath += "/.zit"; 360 | FileUtil.copy(String.format(ConstantVal.OBJECTS_DIR + "/%s", objectId), String.format("%s/objects/%s", remotePath, objectId)); 361 | } 362 | 363 | } 364 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/converter/IdConverter.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.converter; 2 | 3 | import club.qqtim.context.ZitContext; 4 | import picocli.CommandLine; 5 | 6 | /** 7 | * @title: IdConverter 8 | * @Author rezeros.github.io 9 | * @Date: 2020/12/12 10 | * @Version 1.0.0 11 | */ 12 | public class IdConverter implements CommandLine.ITypeConverter { 13 | 14 | @Override 15 | public String convert(String id) { 16 | return ZitContext.getId(id); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/data/CommitObject.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.data; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * @title: Commit 9 | * @Author rezeros.github.io 10 | * @Date: 2020/10/31 11 | * @Version 1.0.0 12 | */ 13 | @Data 14 | public class CommitObject { 15 | 16 | /** 17 | * write tree id 18 | */ 19 | private String tree; 20 | 21 | /** 22 | * parents commit id 23 | */ 24 | private List parents; 25 | 26 | /** 27 | * commit message 28 | */ 29 | private String message; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/data/RefObjValue.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.data; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * @title: RefObjValue 9 | * @Author ReZeroS 10 | * @Date: 2021/1/10 11 | * @Version 1.0.0 12 | */ 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class RefObjValue { 17 | 18 | private String refName; 19 | 20 | private String value; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/data/RefObject.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.data; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.ToString; 7 | 8 | /** 9 | * @title: RefObject 10 | * @Author rezeros.github.io 11 | * @Date: 2020/12/8 12 | * @Version 1.0.0 13 | */ 14 | @Data 15 | @ToString 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class RefObject { 19 | 20 | private String refName; 21 | 22 | private RefValue refValue; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/data/RefValue.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.data; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.ToString; 7 | 8 | /** 9 | * @title: RefValue 10 | * @Author rezeros.github.io 11 | * @Date: 2020/12/10 12 | * @Version 1.0.0 13 | */ 14 | @Data 15 | @ToString 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class RefValue { 19 | 20 | /** 21 | * whether it's a reference symbolic or a direct ref. 22 | */ 23 | private Boolean symbolic; 24 | 25 | private String value; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/data/ZitObject.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.data; 2 | 3 | import com.google.common.collect.Comparators; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.ToString; 7 | 8 | /** 9 | * @title: ZitObject 10 | * @Author rezeros.github.io 11 | * @Date: 2020/10/31 12 | * @Version 1.0.0 13 | */ 14 | @Data 15 | @ToString 16 | @AllArgsConstructor 17 | public class ZitObject implements Comparable { 18 | 19 | private String type; 20 | 21 | private String objectId; 22 | 23 | private String name; 24 | 25 | 26 | @Override 27 | public int compareTo(ZitObject o) { 28 | return Integer.compare(this.hashCode(), o.hashCode()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/diff/DiffUtil.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.diff; 2 | 3 | import club.qqtim.command.HashObject; 4 | import club.qqtim.common.ConstantVal; 5 | import club.qqtim.context.ZitContext; 6 | import club.qqtim.diff.algorithm.MyersDiff; 7 | import com.google.common.base.Charsets; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import java.nio.charset.StandardCharsets; 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.Objects; 18 | import java.util.stream.Collectors; 19 | import java.util.stream.IntStream; 20 | 21 | /** 22 | * @title: Diff 23 | * @Author rezeros.github.io 24 | * @Date: 2020/12/13 25 | * @Version 1.0.0 26 | */ 27 | @Slf4j 28 | public class DiffUtil { 29 | 30 | 31 | public static List iteratorChangedFiles(Map fromTree, Map toTree) { 32 | final Map> pathObjectIds = compareTrees(fromTree, toTree); 33 | return pathObjectIds.entrySet().stream().map(entry -> { 34 | final String path = entry.getKey(); 35 | final List objectIds = entry.getValue(); 36 | final String fromObject = objectIds.get(0); 37 | final String toObject = objectIds.get(1); 38 | if (!Objects.equals(fromObject, toObject)) { 39 | final SimplyChange simplyChange = new SimplyChange(); 40 | simplyChange.setPath(path); 41 | simplyChange.setAction(fromObject == null ? "new file" : toObject == null ? "delete file" : "modify file"); 42 | return simplyChange; 43 | } 44 | return null; 45 | }).filter(Objects::nonNull).collect(Collectors.toList()); 46 | } 47 | 48 | public static String diffTrees(Map treeFrom, Map treeTo) { 49 | StringBuilder output = new StringBuilder(); 50 | final Map> pathObjectIds = compareTrees(treeFrom, treeTo); 51 | pathObjectIds.forEach((path, objectIds) -> { 52 | final String from = objectIds.get(0); 53 | final String to = objectIds.get(1); 54 | if (!Objects.equals(from, to)) { 55 | if (from == null) { 56 | output.append(String.format("\ncreated file: %s\n", path)); 57 | final List lineObjects = convertObjectContentToLines(to); 58 | lineObjects.forEach(lineObject -> lineObject.setAction(ConstantVal.PLUS)); 59 | final String diffResult = lineObjects.stream().map(LineObject::toString).collect(Collectors.joining(System.lineSeparator())); 60 | output.append(diffResult); 61 | } else if (to == null) { 62 | output.append(String.format("\ndeleted file: %s\n", path)); 63 | final List lineObjects = convertObjectContentToLines(from); 64 | lineObjects.forEach(lineObject -> lineObject.setAction(ConstantVal.MINUS)); 65 | final String diffResult = lineObjects.stream().map(LineObject::toString).collect(Collectors.joining(System.lineSeparator())); 66 | output.append(diffResult); 67 | } else { 68 | output.append(String.format("\nchange file: %s\n", path)); 69 | final List lineObjects = diffBlobs(from, to); 70 | final String diffResult = lineObjects.stream().map(LineObject::toString).collect(Collectors.joining(System.lineSeparator())); 71 | output.append(diffResult); 72 | } 73 | } 74 | }); 75 | return output.toString(); 76 | } 77 | 78 | /** 79 | * https://qqtim.club/2020/06/14/git-myers-diff/ 80 | * https://blog.robertelder.org/diff-algorithm/ 81 | * 82 | * @param from from blob id 83 | * @param to to blob id 84 | */ 85 | private static List diffBlobs(String from, String to) { 86 | final List fromLineObjects = convertObjectContentToLines(from); 87 | final List toLineObjects = convertObjectContentToLines(to); 88 | return diffBlobs(fromLineObjects, toLineObjects); 89 | } 90 | 91 | private static List diffBlobs(List fromLineObjects, List toLineObjects) { 92 | try { 93 | // strategy pattern, default is myers diff 94 | return new MyersDiff().diff(fromLineObjects, toLineObjects); 95 | } catch (Exception e) { 96 | e.printStackTrace(); 97 | log.error(e.toString()); 98 | } 99 | return Collections.emptyList(); 100 | } 101 | 102 | private static List convertObjectContentToLines(String objectId) { 103 | if (Objects.isNull(objectId)) { 104 | return Collections.emptyList(); 105 | } 106 | final byte[] objectContentBytes = ZitContext.getObject(objectId); 107 | final String objectContent = new String(objectContentBytes, Charsets.UTF_8); 108 | final List objectLines = Arrays.stream(objectContent.split(System.lineSeparator())).collect(Collectors.toList()); 109 | return IntStream.range(0, objectLines.size()) 110 | .mapToObj(i -> new LineObject(i + 1, objectLines.get(i))).collect(Collectors.toList()); 111 | } 112 | 113 | /** 114 | * given trees, return path, object ids 115 | * 116 | * @param trees compare trees 117 | * @return key path, val objectIds 118 | */ 119 | @SafeVarargs 120 | public static Map> compareTrees(Map... trees) { 121 | Map> entries = new HashMap<>(1); 122 | for (int i = 0; i < trees.length; i++) { 123 | for (Map.Entry entry : trees[i].entrySet()) { 124 | String path = entry.getKey(); 125 | String objectId = entry.getValue(); 126 | entries.putIfAbsent(path, new ArrayList<>(Collections.nCopies(trees.length, null))); 127 | entries.get(path).set(i, objectId); 128 | } 129 | } 130 | return entries; 131 | } 132 | 133 | /** 134 | * key: path val: blob content 135 | * Three-way merge 136 | * @param headTree source tree 137 | * @param otherTree target tree 138 | * @return merged tree 139 | */ 140 | public static Map mergeTrees(Map baseTree, Map headTree, Map otherTree) { 141 | final Map> comparedTrees = compareTrees(baseTree, headTree, otherTree); 142 | Map mergedTrees = new HashMap<>(); 143 | comparedTrees.forEach((path, trees) -> { 144 | String mergeBlobs = mergeBlobs(trees.get(0), trees.get(1), trees.get(2)); 145 | final String objectId = HashObject.hashObject(mergeBlobs.getBytes(StandardCharsets.UTF_8)); 146 | mergedTrees.put(path, objectId); 147 | }); 148 | return mergedTrees; 149 | } 150 | 151 | /** 152 | * diff3 153 | * @param fromBlob from blob id 154 | * @param toBlob to blob id 155 | * @return merged blob content 156 | * https://blog.jcoglan.com/2017/05/08/merging-with-diff3/ 157 | */ 158 | public static String mergeBlobs(String baseBlob, String fromBlob, String toBlob) { 159 | 160 | final List originalLines = convertObjectContentToLines(baseBlob); 161 | final List headLines = convertObjectContentToLines(fromBlob); 162 | final List otherLines = convertObjectContentToLines(toBlob); 163 | return mergeBlobs(originalLines, headLines, otherLines); 164 | } 165 | 166 | public static String mergeBlobs(List originalLines, List headLines, List otherLines) { 167 | 168 | List result = new ArrayList<>(); 169 | 170 | // generate two match sets 171 | Map matchBaseHead = diffBlobs(new ArrayList<>(originalLines), new ArrayList<>(headLines)) 172 | .stream().filter(e -> ConstantVal.SYNC.equals(e.getAction())) 173 | .collect(Collectors.toMap(LineObject::getIndex, LineObject::getAnotherIndex)); 174 | Map matchBaseOther = diffBlobs(new ArrayList<>(originalLines), new ArrayList<>(otherLines)) 175 | .stream().filter(e -> ConstantVal.SYNC.equals(e.getAction())) 176 | .collect(Collectors.toMap(LineObject::getIndex, LineObject::getAnotherIndex)); 177 | 178 | // all start from zero: base pointer 179 | int originStart = 0, headStart = 0, otherStart = 0; 180 | 181 | for (;;) { 182 | // offset 183 | int i = nextMisMatch(originStart, headStart, 184 | otherStart, headLines, otherLines, matchBaseHead, matchBaseOther); 185 | // end index 186 | int originEnd = 0, headEnd = 0, otherEnd = 0; 187 | // we’re already in a non-matching chunk and we need to find the start of the next matching one 188 | if (i == 1) { 189 | final MergePoint mergePoint = nextMatch(originStart, originalLines, matchBaseHead, matchBaseOther); 190 | originEnd = mergePoint.getOrigin(); 191 | headEnd = mergePoint.getHead(); 192 | otherEnd = mergePoint.getOther(); 193 | } else if (i > 1) { 194 | // we’ve found the start of the next non-match 195 | // and we can emit a chunk up to i steps from our current line offsets 196 | originEnd = originStart + i; 197 | headEnd = headStart + i; 198 | otherEnd = otherStart + i; 199 | } 200 | 201 | if (originEnd == 0 || headEnd == 0 || otherEnd == 0) { 202 | break; 203 | } 204 | 205 | // chunk 206 | buildChunk(originStart, headStart, otherStart, 207 | originEnd - 1, headEnd - 1, otherEnd - 1, 208 | originalLines, headLines, otherLines, result); 209 | 210 | originStart = originEnd - 1; 211 | headStart = headEnd - 1; 212 | otherStart = otherEnd - 1; 213 | } 214 | 215 | // build final chunk 216 | buildChunk(originStart, headStart, otherStart, 217 | originalLines.size(), headLines.size(), otherLines.size(), 218 | originalLines, headLines, otherLines, result); 219 | 220 | return result.stream().map(LineObject::getLineContent).collect(Collectors.joining(System.lineSeparator())); 221 | } 222 | 223 | private static void buildChunk(int originLineNumber, int headLineNumber, 224 | int otherLineNumber, int origin, int head, int other, 225 | List originalLines, List headLines, 226 | List otherLines, List result) { 227 | final List originChunk = originalLines.subList(originLineNumber, origin); 228 | final List headChunk = headLines.subList(headLineNumber, head); 229 | final List otherChunk = otherLines.subList(otherLineNumber, other); 230 | 231 | if (headChunk.equals(otherChunk)) { 232 | result.addAll(originChunk); 233 | } else if (originChunk.equals(headChunk)) { 234 | result.addAll(otherChunk); 235 | } else if (originChunk.equals(otherChunk)) { 236 | result.addAll(headChunk); 237 | } else { 238 | result.add(ConstantVal.MERGE_CONFLICT.get(ConstantVal.HEAD_CONFLICT)); 239 | 240 | result.addAll(headChunk); 241 | 242 | result.add(ConstantVal.MERGE_CONFLICT.get(ConstantVal.ORIGIN_CONFLICT)); 243 | 244 | result.addAll(otherChunk); 245 | 246 | result.add(ConstantVal.MERGE_CONFLICT.get(ConstantVal.OTHER_CONFLICT)); 247 | } 248 | } 249 | 250 | 251 | private static int nextMisMatch(int originStart, int headStart, int otherStart, 252 | List headLines, List otherLines, 253 | Map matchBaseHead, Map matchBaseOther){ 254 | for (int i = 1; i <= headLines.size() && i <= otherLines.size(); i++) { 255 | // got mapped index in head and other 256 | final Integer head = matchBaseHead.get(originStart + i); 257 | final Integer other = matchBaseOther.get(originStart + i); 258 | 259 | if (Objects.isNull(head) || !head.equals(headStart + i) 260 | || Objects.isNull(other) || !other.equals(otherStart + i)) { 261 | return i; 262 | } 263 | 264 | } 265 | return 0; 266 | } 267 | 268 | private static MergePoint nextMatch(int originStart, List originLines, 269 | Map matchBaseHead, Map matchBaseOther) { 270 | for (int i = originStart + 1; i <= originLines.size(); i++) { 271 | final Integer head = matchBaseHead.get(i); 272 | final Integer other = matchBaseOther.get(i); 273 | 274 | if (Objects.nonNull(head) && Objects.nonNull(other)) { 275 | return new MergePoint(i, head, other); 276 | } 277 | } 278 | return new MergePoint(0, 0, 0); 279 | 280 | } 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | private static int findNextSyncLine(int index, List fromDiffBase) { 291 | for (int i = index; i < fromDiffBase.size(); i++) { 292 | if (ConstantVal.SYNC.equals(fromDiffBase.get(i).getAction())) { 293 | return i; 294 | } 295 | } 296 | // not found 297 | return -1; 298 | } 299 | 300 | /** 301 | * test method 302 | */ 303 | public static void main(String[] args) { 304 | String origin = "celery-garlic-onions-salmon-tomatoes-wine"; 305 | String head = "celery-salmon-tomatoes-garlic-onions-wine"; 306 | String other = "celery-salmon-garlic-onions-tomatoes-wine"; 307 | // String origin = "1-2-3-4-5-6"; 308 | // String head = "1-4-5-2-3-6"; 309 | // String other = "1-2-4-5-3-6"; 310 | 311 | List originLineObjects = new ArrayList<>(); 312 | List headLineObjects = new ArrayList<>(); 313 | List otherLineObjects = new ArrayList<>(); 314 | final String[] originArr = origin.split("-"); 315 | final String[] headArr = head.split("-"); 316 | final String[] otherArr = other.split("-"); 317 | for (int i = 0; i < originArr.length; i++) { 318 | int index = i + 1; 319 | originLineObjects.add(new LineObject(index, originArr[i])); 320 | headLineObjects.add(new LineObject(index, headArr[i])); 321 | otherLineObjects.add(new LineObject(index, otherArr[i])); 322 | } 323 | final String mergeDetails = mergeBlobs(originLineObjects, headLineObjects, otherLineObjects); 324 | log.info(mergeDetails); 325 | } 326 | 327 | 328 | 329 | 330 | } 331 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/diff/LineObject.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.diff; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.Objects; 9 | 10 | /** 11 | * @title: LineObject 12 | * @Author rezeros.github.io 13 | * @Date: 2020/12/16 14 | * @Version 1.0.0 15 | */ 16 | @Data 17 | @NoArgsConstructor 18 | @EqualsAndHashCode(of = {"lineContent"}) 19 | public class LineObject { 20 | 21 | /** 22 | * sync, plus, minus 23 | */ 24 | private String action; 25 | 26 | /** 27 | * row number, default from zero 28 | */ 29 | private Integer index; 30 | 31 | /** 32 | * if action is sync, the above index is from original, and this index is from head 33 | */ 34 | private Integer anotherIndex; 35 | 36 | /** 37 | * can not be null 38 | * line content which contains the whole content of the current line 39 | */ 40 | private String lineContent; 41 | 42 | public LineObject(Integer index, String lineContent) { 43 | this.index = index; 44 | this.lineContent = lineContent; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | String actionTxt = ConstantVal.EMPTY; 50 | if (ConstantVal.SYNC.equals(this.action)) { 51 | actionTxt = ConstantVal.SINGLE_SPACE; 52 | } 53 | if (ConstantVal.PLUS.equals(this.action)) { 54 | actionTxt = ConstantVal.PLUS_SYMBOL; 55 | } 56 | if (ConstantVal.MINUS.equals(this.action)) { 57 | actionTxt = ConstantVal.MINUS_SYMBOL; 58 | } 59 | // action index 60 | return actionTxt + ConstantVal.SINGLE_SPACE 61 | + (Objects.isNull(index)? ConstantVal.EMPTY: index) 62 | + (ConstantVal.SYNC.equals(this.action) ? String.format("[%d]", anotherIndex): ConstantVal.EMPTY) 63 | + ConstantVal.SINGLE_SPACE + lineContent; 64 | } 65 | 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/diff/MergePoint.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.diff; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * @title: MergePoint 9 | * @Author lijie78 10 | * @Date: 2021/2/20 11 | * @Version 1.0.0 12 | */ 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class MergePoint { 17 | 18 | private Integer origin; 19 | 20 | private Integer head; 21 | 22 | private Integer other; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/diff/SimplyChange.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.diff; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @title: Simply 7 | * @Author rezeros.github.io 8 | * @Date: 2020/12/26 9 | * @Version 1.0.0 10 | */ 11 | @Data 12 | public class SimplyChange { 13 | 14 | private String action; 15 | 16 | private String path; 17 | 18 | @Override 19 | public String toString() { 20 | return action + " : " + path; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/diff/algorithm/DiffAlgorithm.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.diff.algorithm; 2 | 3 | import club.qqtim.diff.LineObject; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * @author rezeros.github.io 9 | */ 10 | public interface DiffAlgorithm { 11 | 12 | /** 13 | * convert from content to target content 14 | * @param fromLineObjects from content 15 | * @param targetLineObjects target content 16 | * @return convert short edit script 17 | */ 18 | List diff(List fromLineObjects, List targetLineObjects); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/diff/algorithm/MyersDiff.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.diff.algorithm; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.diff.DiffUtil; 5 | import club.qqtim.diff.LineObject; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.Stack; 13 | 14 | /** 15 | * @title: MyersDiff 16 | * @Author rezeros.github.io 17 | * @Date: 2021/2/19 18 | * @Version 1.0.0 19 | */ 20 | @Slf4j 21 | public class MyersDiff implements DiffAlgorithm { 22 | 23 | /** 24 | * complexity of both time and space : O ((N + M)D) 25 | * k = x - y 26 | * 0 - 1 - 2 - 3 // x 27 | * | 28 | * 1 29 | * | 30 | * 2 // y 31 | */ 32 | @Override 33 | public List diff(List fromLineObjects, List targetLineObjects) { 34 | 35 | // init step 36 | int finalStep = 0; 37 | // we set from as x anxious 38 | final int fromLineCount = fromLineObjects.size(); 39 | 40 | // we set target as y anxious 41 | final int targetLineCount = targetLineObjects.size(); 42 | 43 | // sum of from and target lines count 44 | final int totalLineCount = targetLineCount + fromLineCount; 45 | 46 | int vSize = Math.max(fromLineCount, targetLineCount) * 2 + 1; 47 | 48 | // do snapshot for v while iterate step 49 | int [][] vList = new int[totalLineCount + 1][vSize]; 50 | 51 | // k can be zero, so plus one 52 | //todo optimize for minimize v.length 53 | final int[] v = new int[vSize]; 54 | 55 | // set the previous start point 56 | v[v.length / 2 + 1] = 0; 57 | 58 | boolean foundShortest = false; 59 | for (int step = 0; step <= totalLineCount; step++) { 60 | 61 | // little trick, java can not use negative number as array index 62 | int negativeStep = v.length / 2 - step; 63 | int positiveStep = v.length / 2 + step; 64 | for (int k = negativeStep; k >= 0 && k <= positiveStep; k += 2) { 65 | int kAimD = k - v.length / 2; 66 | boolean down = (kAimD == -step || (kAimD != step && v[k - 1] < v[k + 1])); 67 | 68 | int xStart = down? v[k + 1]: v[k - 1]; 69 | 70 | int xEnd = down? xStart: xStart + 1; 71 | int yEnd = xEnd - kAimD; 72 | // diagonal 73 | while ((0 <= xEnd && xEnd < fromLineCount) && (0 <= yEnd && yEnd < targetLineCount) 74 | && (fromLineObjects.get(xEnd).getLineContent().equals(targetLineObjects.get(yEnd).getLineContent()))){ 75 | xEnd++; yEnd++; 76 | } 77 | v[k] = xEnd; 78 | if (xEnd >= fromLineCount && yEnd >= targetLineCount) { 79 | foundShortest = true; 80 | break; 81 | } 82 | } 83 | // do snapshot for v 84 | vList[step] = Arrays.copyOf(v, v.length); 85 | if (foundShortest) { 86 | finalStep = step; 87 | break; 88 | } 89 | } 90 | List result = new ArrayList<>(); 91 | 92 | if (foundShortest) { 93 | Stack snakeStack = generateSnakes(fromLineCount, targetLineCount, vList, finalStep); 94 | 95 | // the final step, let's rock 96 | SnakePoint realStartPoint = new SnakePoint(0, 0); 97 | while(!snakeStack.empty()) { 98 | final Snake snake = snakeStack.pop(); 99 | final SnakePoint start = snake.getStart(); 100 | final SnakePoint middle = snake.getMiddle(); 101 | final SnakePoint end = snake.getEnd(); 102 | 103 | result.addAll(compareSnakePoint(realStartPoint, start, fromLineObjects, targetLineObjects)); 104 | result.addAll(compareSnakePoint(start, middle, fromLineObjects, targetLineObjects)); 105 | result.addAll(compareSnakePoint(middle, end, fromLineObjects, targetLineObjects)); 106 | 107 | realStartPoint = end; 108 | } 109 | 110 | return result; 111 | } 112 | 113 | fromLineObjects.forEach(line -> line.setAction(ConstantVal.MINUS)); 114 | targetLineObjects.forEach(line -> line.setAction(ConstantVal.PLUS)); 115 | result.addAll(fromLineObjects); 116 | result.addAll(targetLineObjects); 117 | return result; 118 | } 119 | 120 | 121 | /** 122 | * let's do backtrack to generate the shortest path 123 | * now vList has total record we need: every step the (k, x) val 124 | * 125 | * start(K-1) -- mid(K) 126 | * \ 127 | * \ 128 | * end 129 | * 130 | * 131 | */ 132 | private static Stack generateSnakes(int fromLineCount, int targetLineCount, int[][] vList, int finalStep) { 133 | 134 | Stack snakeStack = new Stack<>(); 135 | int fromEndX = fromLineCount; 136 | int targetEndY = targetLineCount; 137 | // step >= 0 or (fromEndX > 0 && targetEndY> 0) 138 | for (int step = finalStep; step >= 0; step--) { 139 | final int[] v = vList[step]; 140 | 141 | int negativeStep = v.length / 2 - step; 142 | int positiveStep = v.length / 2 + step; 143 | 144 | int k = fromEndX - targetEndY; 145 | int kIndex = v.length / 2 + k; 146 | 147 | // set current k as end point 148 | int xEnd = v[kIndex]; 149 | int yEnd = xEnd - k; 150 | 151 | boolean down = (kIndex == negativeStep || (kIndex != positiveStep && v[kIndex - 1] < v[kIndex + 1])); 152 | 153 | int xStart = v[down? kIndex + 1: kIndex - 1]; 154 | int yStart = xStart - (down? k + 1: k -1); 155 | 156 | int xMid = down? xStart: xStart + 1; 157 | int yMid = xMid - k; 158 | 159 | fromEndX = xStart; 160 | targetEndY = yStart; 161 | if (fromEndX < 0 || targetEndY < 0) { 162 | break; 163 | } 164 | final Snake snake = new Snake(); 165 | snake.setStart(new SnakePoint(xStart, yStart)); 166 | snake.setMiddle(new SnakePoint(xMid, yMid)); 167 | snake.setEnd(new SnakePoint(xEnd, yEnd)); 168 | snakeStack.push(snake); 169 | 170 | } 171 | return snakeStack; 172 | } 173 | 174 | /** 175 | * compare start, mid and end point to generate result of diff lines 176 | * receive two points: from and end 177 | */ 178 | public static List compareSnakePoint 179 | (SnakePoint from, SnakePoint end, List fromLineObjects, List targetLineObjects) { 180 | // mid equals end 181 | if (from.equals(end)) { 182 | return Collections.emptyList(); 183 | } 184 | 185 | List result = new ArrayList<>(); 186 | // mid to end 187 | if (!from.getX().equals(end.getX()) && !from.getY().equals(end.getY())) { 188 | for (int fromX = from.getX(), fromY = from.getY(); fromX < end.getX() && fromY < end.getY(); fromX++, fromY++) { 189 | final LineObject lineObject = fromLineObjects.get(fromX); 190 | final LineObject anotherLine = targetLineObjects.get(fromY); 191 | lineObject.setAction(ConstantVal.SYNC); 192 | lineObject.setAnotherIndex(anotherLine.getIndex()); 193 | result.add(lineObject); 194 | } 195 | } else { 196 | // start to mid 197 | if (!from.getX().equals(end.getX())) { 198 | final LineObject lineObject = fromLineObjects.get(from.getX()); 199 | lineObject.setAction(ConstantVal.MINUS); 200 | result.add(lineObject); 201 | } else if (!from.getY().equals(end.getY())) { 202 | final LineObject lineObject = targetLineObjects.get(from.getY()); 203 | lineObject.setAction(ConstantVal.PLUS); 204 | result.add(lineObject); 205 | } 206 | } 207 | return result; 208 | } 209 | 210 | 211 | /** 212 | * test method 213 | */ 214 | public static void main(String[] args) { 215 | String a = "celery-garlic-onions-salmon-tomatoes-wine"; 216 | String b = "celery-salmon-tomatoes-garlic-onions-wine"; 217 | 218 | final String[] aArray = a.split("-"); 219 | final String[] bArray = b.split("-"); 220 | List fromLineObjects = new ArrayList<>(); 221 | List targetLineObjects = new ArrayList<>(); 222 | for (int i = 0; i < aArray.length; i++) { 223 | int index = i + 1; 224 | final LineObject lineObject = new LineObject(); 225 | lineObject.setIndex(index); 226 | lineObject.setLineContent(aArray[i]); 227 | fromLineObjects.add(lineObject); 228 | } 229 | for (int i = 0; i < bArray.length; i++) { 230 | int index = i + 1; 231 | final LineObject lineObject = new LineObject(); 232 | lineObject.setIndex(index); 233 | lineObject.setLineContent(bArray[i]); 234 | targetLineObjects.add(lineObject); 235 | } 236 | // final List diff = new MyersDiff().diff(fromLineObjects, targetLineObjects); 237 | // DiffUtil.mergeBlobs(fromLineObjects, targetLineObjects, null); 238 | // diff.forEach(line -> log.debug(line.toString())); 239 | } 240 | 241 | } 242 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/diff/algorithm/Snake.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.diff.algorithm; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * @title: TinySnake 9 | * @Author lijie78 10 | * @Date: 2021/2/19 11 | * @Version 1.0.0 12 | */ 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class Snake { 17 | 18 | private SnakePoint start; 19 | 20 | private SnakePoint middle; 21 | 22 | private SnakePoint end; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/diff/algorithm/SnakePoint.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.diff.algorithm; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * @title: SnakePoint 10 | * @Author lijie78 11 | * @Date: 2021/2/19 12 | * @Version 1.0.0 13 | */ 14 | @Data 15 | @EqualsAndHashCode 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class SnakePoint { 19 | 20 | private Integer x; 21 | 22 | private Integer y; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/parser/Parser.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.parser; 2 | 3 | 4 | public interface Parser { 5 | 6 | void execute(String[] args); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/parser/support/CommandLineParser.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.parser.support; 2 | 3 | import club.qqtim.context.Zit; 4 | import club.qqtim.parser.Parser; 5 | import lombok.extern.slf4j.Slf4j; 6 | import picocli.CommandLine; 7 | 8 | import java.util.Arrays; 9 | 10 | @Slf4j 11 | public class CommandLineParser implements Parser { 12 | 13 | @Override 14 | public void execute(String[] args) { 15 | log.debug(Arrays.toString(args)); 16 | new CommandLine(new Zit()).execute(args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/util/ConvertUtil.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.util; 2 | 3 | /** 4 | * @author rezeros.github.io 5 | */ 6 | public final class ConvertUtil { 7 | 8 | private ConvertUtil(){} 9 | 10 | 11 | public static byte[] toPrimitives(Byte[] oBytes) { 12 | byte[] bytes = new byte[oBytes.length]; 13 | for(int i = 0; i < oBytes.length; i++){ 14 | bytes[i] = oBytes[i]; 15 | } 16 | return bytes; 17 | } 18 | 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/util/FileUtil.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.util; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import club.qqtim.context.ZitContext; 5 | import club.qqtim.util.handler.PosixHandler; 6 | import com.google.common.base.Charsets; 7 | import com.google.common.io.ByteSource; 8 | import com.google.common.io.CharSource; 9 | import com.google.common.io.Files; 10 | import com.google.common.primitives.Chars; 11 | import jnr.posix.POSIX; 12 | import jnr.posix.POSIXFactory; 13 | import lombok.extern.slf4j.Slf4j; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.nio.ByteBuffer; 18 | import java.nio.charset.StandardCharsets; 19 | import java.nio.file.FileVisitOption; 20 | import java.nio.file.Path; 21 | import java.nio.file.Paths; 22 | import java.util.Arrays; 23 | import java.util.Objects; 24 | import java.util.function.Predicate; 25 | import java.util.stream.Stream; 26 | 27 | /** 28 | * @author rezeros.github.io 29 | */ 30 | @Slf4j 31 | public final class FileUtil { 32 | 33 | private FileUtil() { 34 | } 35 | 36 | 37 | public static void setRootPathContext(String target) { 38 | System.setProperty("user.dir", target); 39 | POSIX posix = POSIXFactory.getPOSIX(new PosixHandler(), true); 40 | posix.chdir(target); 41 | } 42 | 43 | 44 | 45 | public static void mkdir(String dirName) { 46 | boolean mkdir = new File(dirName).mkdir(); 47 | if (mkdir) { 48 | log.info("Init {} directory in .zit repository", Paths.get(Objects.requireNonNull(FileUtil.getCurrentDir()), dirName)); 49 | } else { 50 | log.info("Create directory failed, please check your access right."); 51 | } 52 | } 53 | 54 | public static String getCurrentDir() { 55 | File file = new File("."); 56 | try { 57 | return file.getCanonicalPath(); 58 | } catch (IOException e) { 59 | log.error("can not get the current dir, please check your access right"); 60 | } 61 | return null; 62 | } 63 | 64 | public static void deleteDir(String path){ 65 | deleteDir(new File(path), null); 66 | } 67 | 68 | public static void deleteDir(String path, Predicate ignorePredicate){ 69 | deleteDir(new File(path), ignorePredicate); 70 | } 71 | 72 | public static void deleteDir(File file, Predicate ignorePredicate){ 73 | if (ignorePredicate != null) { 74 | final boolean ignorePath = ignorePredicate.test(file.getPath()); 75 | if (ignorePath) { 76 | return; 77 | } 78 | } 79 | if (file.isDirectory()) { 80 | Arrays.stream(Objects.requireNonNull(file.listFiles())) 81 | .forEach(currentFile -> deleteDir(currentFile, ignorePredicate)); 82 | } 83 | final boolean delete = file.delete(); 84 | if (!delete) { 85 | log.error("delete file failed, please check your access right."); 86 | } 87 | } 88 | 89 | public static void createFile(String fileContents, String fileName) { 90 | createFile(fileContents.getBytes(StandardCharsets.UTF_8), fileName); 91 | } 92 | 93 | public static void createFile(byte[] fileContents, String fileName) { 94 | File hashObject = new File(fileName); 95 | try { 96 | // first create the parent directory 97 | Files.createParentDirs(hashObject); 98 | // then create the file 99 | Files.write(fileContents, hashObject); 100 | } catch (IOException e) { 101 | log.error(e.toString()); 102 | } 103 | } 104 | 105 | public static void createParentDirs(String path) { 106 | try { 107 | Files.createParentDirs(new File(path)); 108 | } catch (IOException e) { 109 | log.error(e.toString()); 110 | } 111 | } 112 | 113 | 114 | public static String getFileAsString(String path, String type) { 115 | try { 116 | return getFileByteSource(path, type).asCharSource(Charsets.UTF_8).read(); 117 | } catch (IOException e) { 118 | log.error(e.toString()); 119 | } 120 | return null; 121 | } 122 | 123 | public static ByteSource getFileByteSource(String path, String type) throws IOException { 124 | char nullChar = 0; 125 | ByteSource byteSource = Files.asByteSource(new File(path)); 126 | if (ConstantVal.NONE.equals(type)) { 127 | return byteSource; 128 | } 129 | // todo: refactor the below code to extract a method like obj.partition in python 130 | byte[] fileWithHeader = byteSource.read(); 131 | byte[] header = new byte[type.getBytes(Charsets.UTF_8).length]; 132 | byte[] nullBytes = new byte[Chars.toByteArray(nullChar).length]; 133 | byte[] fileContent = new byte[fileWithHeader.length - header.length - nullBytes.length]; 134 | ByteBuffer fileWithHeaderBuffer = ByteBuffer.wrap(fileWithHeader); 135 | fileWithHeaderBuffer.get(header, 0, header.length); 136 | log.debug("current object type is {}:", new String(header, Charsets.UTF_8)); 137 | fileWithHeaderBuffer.get(nullBytes, 0, nullBytes.length); 138 | fileWithHeaderBuffer.get(fileContent, 0, fileContent.length); 139 | return ByteSource.wrap(fileContent); 140 | } 141 | 142 | 143 | 144 | public static void emptyCurrentDir() { 145 | File file = new File(ConstantVal.BASE_PATH); 146 | final String[] paths = file.list(); 147 | if (paths != null) { 148 | Arrays.stream(paths).forEach(path -> { 149 | if (ZitContext.isNotIgnored(path)) { 150 | FileUtil.deleteDir(path); 151 | } 152 | }); 153 | } 154 | } 155 | 156 | 157 | public static boolean isFile(String path) { 158 | return new File(path).isFile(); 159 | } 160 | 161 | 162 | 163 | public static void copy(String from, String to) { 164 | copy(Paths.get(from), Paths.get(to)); 165 | } 166 | 167 | public static void copy(Path from, Path to) { 168 | try { 169 | final File fromFile = from.toFile(); 170 | final File toFile = to.toFile(); 171 | Files.createParentDirs(toFile); 172 | Files.copy(fromFile, toFile); 173 | } catch (IOException e) { 174 | log.error(e.toString()); 175 | } 176 | } 177 | 178 | 179 | public static String readFileFirstLine(File file) { 180 | String value; 181 | try { 182 | final CharSource charSource = Files.asCharSource(file, Charsets.UTF_8); 183 | value = Objects.requireNonNull(charSource.readFirstLine()).trim(); 184 | } catch (IOException e) { 185 | log.error(e.toString()); 186 | return null; 187 | } 188 | return value; 189 | } 190 | 191 | 192 | /** 193 | * convert file content to byte[] 194 | */ 195 | public static byte[] getFileAsBytes(File file){ 196 | try { 197 | return Files.toByteArray(file); 198 | } catch (IOException e) { 199 | log.error(e.toString()); 200 | } 201 | return null; 202 | } 203 | 204 | 205 | public static Stream walk(Path start, int maxDepth, FileVisitOption... options) throws IOException { 206 | return java.nio.file.Files.walk(start, maxDepth, options); 207 | } 208 | 209 | 210 | public static String convertUnixPath(String path) { 211 | if (Objects.isNull(path)) { 212 | return null; 213 | } 214 | return path.replace(File.separatorChar, '/'); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/main/java/club/qqtim/util/handler/PosixHandler.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.util.handler; 2 | 3 | import jnr.constants.platform.Errno; 4 | import jnr.posix.POSIXHandler; 5 | 6 | import java.io.File; 7 | import java.io.InputStream; 8 | import java.io.PrintStream; 9 | import java.util.Map; 10 | 11 | import static java.util.stream.Collectors.toList; 12 | 13 | public class PosixHandler implements POSIXHandler { 14 | 15 | @Override 16 | public void error(Errno error, String extraData) { 17 | System.err.printf("%s %s\n", error, extraData); 18 | } 19 | 20 | @Override 21 | public void error(Errno error, String methodName, String extraData) { 22 | System.err.printf("%s %s %s\n", error, methodName, extraData); 23 | } 24 | 25 | @Override 26 | public void unimplementedError(String methodName) { 27 | System.err.printf("%s\n", methodName); 28 | } 29 | 30 | @Override 31 | public void warn(WARNING_ID id, String message, Object... data) { 32 | System.err.printf("%s %s %s\n", id, message, data); 33 | } 34 | 35 | @Override 36 | public boolean isVerbose() { 37 | return false; 38 | } 39 | 40 | @Override 41 | public File getCurrentWorkingDirectory() { 42 | return new File(System.getProperty("user.dir")); 43 | } 44 | 45 | @Override 46 | public String[] getEnv() { 47 | return System.getenv() 48 | .entrySet() 49 | .stream() 50 | .map(Map.Entry::toString) 51 | .collect(toList()) 52 | .toArray(new String[0]); 53 | } 54 | 55 | @Override 56 | public InputStream getInputStream() { 57 | return System.in; 58 | } 59 | 60 | @Override 61 | public PrintStream getOutputStream() { 62 | return System.out; 63 | } 64 | 65 | @Override 66 | public int getPID() { 67 | return 0; 68 | } 69 | 70 | @Override 71 | public PrintStream getErrorStream() { 72 | return System.err; 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=DEBUG, stdout 3 | 4 | # Direct log messages to stdout 5 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 6 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 7 | log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n -------------------------------------------------------------------------------- /src/test/java/club/qqtim/data/DataTest.java: -------------------------------------------------------------------------------- 1 | package club.qqtim.data; 2 | 3 | import club.qqtim.common.ConstantVal; 4 | import org.junit.Test; 5 | 6 | import java.io.IOException; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | /** 14 | * @title: DataTest 15 | * @Author rezeros.github.io 16 | * @Date: 2020/12/8 17 | * @Version 1.0.0 18 | */ 19 | 20 | public class DataTest { 21 | 22 | 23 | 24 | @Test 25 | public void testIteratorRefs() throws IOException { 26 | final Path basePath = Paths.get("."); 27 | final Path objectPath = Paths.get(".zit/objects"); 28 | final Path refsDir = Paths.get("refs"); 29 | final List pathList = Files.walk(basePath, Integer.MAX_VALUE) 30 | .filter(Files::isRegularFile).map(Path::toString).collect(Collectors.toList()); 31 | pathList.forEach(System.out::println); 32 | } 33 | } 34 | --------------------------------------------------------------------------------