├── gradle.properties
├── images
├── logo.png
├── kit-tag.png
├── kit-help.png
├── kit-init.png
├── kit-add-status.png
├── kit-config-commit.png
├── kit-branch-checkout.png
└── kit-convert-to-git.png
├── settings.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── kotlinc.xml
├── vcs.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
├── gradle.xml
└── workspace.xml
├── src
├── main
│ └── kotlin
│ │ └── kit
│ │ ├── Main.kt
│ │ ├── cli
│ │ ├── LogCommand.kt
│ │ ├── StatusCommand.kt
│ │ ├── GitCommand.kt
│ │ ├── CommitCommand.kt
│ │ ├── CheckoutCommand.kt
│ │ ├── Cli.kt
│ │ ├── AddCommand.kt
│ │ ├── ConfigCommand.kt
│ │ ├── UnStageCommand.kt
│ │ ├── InitCommand.kt
│ │ ├── BranchCommand.kt
│ │ ├── TagCommand.kt
│ │ └── Kit.kt
│ │ ├── plumbing
│ │ ├── Zlib.kt
│ │ ├── GitIndex.kt
│ │ └── plumbing.kt
│ │ ├── utils
│ │ └── Utils.kt
│ │ └── porcelain
│ │ ├── Config.kt
│ │ └── Porcaline.kt
└── test
│ └── kotlin
│ └── kit
│ ├── utils
│ └── UtilsKtTest.kt
│ ├── porcelain
│ └── PorcelainKtTest.kt
│ └── plumbing
│ └── PlumbingKtTest.kt
├── .gitignore
├── LICENSE
├── gradlew.bat
├── README.md
└── gradlew
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Badr-1/Kit/HEAD/images/logo.png
--------------------------------------------------------------------------------
/images/kit-tag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Badr-1/Kit/HEAD/images/kit-tag.png
--------------------------------------------------------------------------------
/images/kit-help.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Badr-1/Kit/HEAD/images/kit-help.png
--------------------------------------------------------------------------------
/images/kit-init.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Badr-1/Kit/HEAD/images/kit-init.png
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 |
2 | rootProject.name = "Git-A-Home-Made-Recipe-With-Kotlin"
3 |
4 |
--------------------------------------------------------------------------------
/images/kit-add-status.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Badr-1/Kit/HEAD/images/kit-add-status.png
--------------------------------------------------------------------------------
/images/kit-config-commit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Badr-1/Kit/HEAD/images/kit-config-commit.png
--------------------------------------------------------------------------------
/images/kit-branch-checkout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Badr-1/Kit/HEAD/images/kit-branch-checkout.png
--------------------------------------------------------------------------------
/images/kit-convert-to-git.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Badr-1/Kit/HEAD/images/kit-convert-to-git.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Badr-1/Kit/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/main/kotlin/kit/Main.kt:
--------------------------------------------------------------------------------
1 | package kit
2 |
3 | import kit.cli.*
4 |
5 | object Main {
6 | @JvmStatic
7 | fun main(args: Array) {
8 | Cli.kit.main(args)
9 | }
10 | }
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/LogCommand.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import kit.porcelain.log
5 |
6 | class LogCommand : CliktCommand(name = "log", help = "Show commit logs") {
7 | override fun run() {
8 | log()
9 | }
10 | }
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/StatusCommand.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import kit.porcelain.status
5 |
6 | class StatusCommand : CliktCommand(name = "status", help = "Show the working tree status") {
7 | override fun run() {
8 | status()
9 | }
10 | }
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/GitCommand.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import java.io.File
5 |
6 | class GitCommand : CliktCommand(name = "git", help = "convert kit repository to git repository") {
7 | override fun run() {
8 | // change .kit to .git
9 | val kitDir = File("${System.getProperty("user.dir")}/.kit")
10 | val gitDir = File("${System.getProperty("user.dir")}/.git")
11 | kitDir.renameTo(gitDir)
12 | }
13 | }
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/CommitCommand.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import com.github.ajalt.clikt.parameters.options.option
5 | import com.github.ajalt.clikt.parameters.options.required
6 | import kit.porcelain.commit
7 |
8 | class CommitCommand : CliktCommand(name = "commit", help = "Record changes to the repository") {
9 | private val message by option("-m", "--message", help = "Commit message").required()
10 |
11 | override fun run() {
12 | commit(message)
13 | }
14 | }
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/CheckoutCommand.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import com.github.ajalt.clikt.parameters.arguments.argument
5 | import kit.porcelain.checkout
6 |
7 | class CheckoutCommand : CliktCommand(name = "checkout", help = "Switch branches or restore working tree files") {
8 | private val branchOrCommit by argument(
9 | help = "The branch or commit to checkout",
10 | name = "branchOrCommit"
11 | )
12 |
13 | override fun run() {
14 | checkout(branchOrCommit)
15 | }
16 | }
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/Cli.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.completion.CompletionCommand
4 | import com.github.ajalt.clikt.core.subcommands
5 |
6 | object Cli{
7 | val kit = Kit().subcommands(
8 | InitCommand(),
9 | ConfigCommand(),
10 | GitCommand(),
11 | AddCommand(),
12 | UnStageCommand(),
13 | StatusCommand(),
14 | CommitCommand(),
15 | LogCommand(),
16 | CheckoutCommand(),
17 | BranchCommand(),
18 | TagCommand(),
19 | CompletionCommand()
20 | )
21 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/AddCommand.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import com.github.ajalt.clikt.parameters.arguments.argument
5 | import kit.porcelain.add
6 |
7 | class AddCommand : CliktCommand(name = "add", help = "Add file contents to the index") {
8 | private val path by argument(
9 | help = "The path of the file to add",
10 | name = "path"
11 | )
12 |
13 | override fun run() {
14 | if (path.startsWith(System.getProperty("user.dir")))
15 | add(path)
16 | else
17 | add(System.getProperty("user.dir") + "/" + path)
18 | }
19 | }
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/ConfigCommand.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import com.github.ajalt.clikt.parameters.arguments.argument
5 | import kit.porcelain.Config
6 |
7 |
8 | class ConfigCommand : CliktCommand(name = "config", help = "Set kit configuration values") {
9 | private val name by argument(
10 | help = "The name of the configuration value",
11 | name = "name"
12 | )
13 | private val value by argument(
14 | help = "The value of the configuration value",
15 | name = "value"
16 | )
17 |
18 | override fun run() {
19 | Config.set(name, value)
20 | }
21 | }
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/UnStageCommand.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import com.github.ajalt.clikt.parameters.arguments.argument
5 | import kit.porcelain.unstage
6 |
7 | class UnStageCommand : CliktCommand(name = "unstage", help = "Remove file contents from the index") {
8 | private val path by argument(
9 | help = "The path of the file to unstage",
10 | name = "path"
11 | )
12 |
13 | override fun run() {
14 | if (path.startsWith(System.getProperty("user.dir")))
15 | unstage(path)
16 | else
17 | unstage(System.getProperty("user.dir") + "/" + path)
18 | }
19 | }
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/InitCommand.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import com.github.ajalt.clikt.parameters.arguments.argument
5 | import com.github.ajalt.clikt.parameters.arguments.optional
6 | import kit.porcelain.init
7 | import java.nio.file.Path
8 |
9 | class InitCommand : CliktCommand(name = "init", help = "Initialize a new, empty repository") {
10 | private val directory by argument(
11 | help = "Directory to initialize the repository in",
12 | name = "directory"
13 | ).optional()
14 |
15 | override fun run() {
16 | val path = Path.of(directory ?: "").toAbsolutePath()
17 | init(path)
18 | }
19 | }
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/BranchCommand.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import com.github.ajalt.clikt.parameters.arguments.argument
5 | import com.github.ajalt.clikt.parameters.arguments.optional
6 | import kit.porcelain.branch
7 |
8 | class BranchCommand : CliktCommand(name = "branch", help = "create branch") {
9 | private val branchName by argument(
10 | help = "The name of the branch",
11 | name = "branchName"
12 | )
13 | private val ref by argument().optional()
14 |
15 | override fun run() {
16 | if (ref == null)
17 | branch(branchName)
18 | else
19 | branch(branchName, ref!!)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | build/
3 | !gradle/wrapper/gradle-wrapper.jar
4 | !**/src/main/**/build/
5 | !**/src/test/**/build/
6 | working/
7 |
8 | ### IntelliJ IDEA ###
9 | .idea/modules.xml
10 | .idea/jarRepositories.xml
11 | .idea/compiler.xml
12 | .idea/libraries/
13 | *.iws
14 | *.iml
15 | *.ipr
16 | out/
17 | !**/src/main/**/out/
18 | !**/src/test/**/out/
19 |
20 | ### Eclipse ###
21 | .apt_generated
22 | .classpath
23 | .factorypath
24 | .project
25 | .settings
26 | .springBeans
27 | .sts4-cache
28 | bin/
29 | !**/src/main/**/bin/
30 | !**/src/test/**/bin/
31 |
32 | ### NetBeans ###
33 | /nbproject/private/
34 | /nbbuild/
35 | /dist/
36 | /nbdist/
37 | /.nb-gradle/
38 |
39 | ### VS Code ###
40 | .vscode/
41 |
42 | ### Mac OS ###
43 | .DS_Store
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/TagCommand.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import com.github.ajalt.clikt.parameters.arguments.argument
5 | import com.github.ajalt.clikt.parameters.arguments.optional
6 | import com.github.ajalt.clikt.parameters.options.option
7 | import com.github.ajalt.clikt.parameters.options.required
8 | import kit.porcelain.tag
9 |
10 | class TagCommand : CliktCommand(name = "tag", help = "create tag") {
11 | private val tagName by argument(
12 | help = "The name of the tag",
13 | name = "tagName"
14 | )
15 | private val ref by argument().optional()
16 | private val message by option("-m", "--message", help = "Tag message").required()
17 | override fun run() {
18 | if (ref == null)
19 | tag(tagName, message)
20 | else
21 | tag(tagName, message, ref!!)
22 | }
23 | }
--------------------------------------------------------------------------------
/src/main/kotlin/kit/cli/Kit.kt:
--------------------------------------------------------------------------------
1 | package kit.cli
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import kit.plumbing.GitIndex
5 | import kit.porcelain.Config
6 | import java.io.File
7 | import kotlin.system.exitProcess
8 |
9 | class Kit : CliktCommand(name = "kit", help = "The kit version control system") {
10 | override fun run() {
11 | val context = currentContext
12 | val subcommand = context.invokedSubcommand
13 | if (File("${System.getProperty("user.dir")}/.kit").exists()) {
14 | // load config file
15 | Config.read()
16 | // load index file
17 | GitIndex
18 | } else {
19 | // only the init command is allowed to run without a .kit directory
20 | if (subcommand?.commandName != "init") {
21 | echo("Not a kit repository (or any of the parent directories): .kit")
22 | exitProcess(1)
23 | }
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 badr
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.
--------------------------------------------------------------------------------
/src/test/kotlin/kit/utils/UtilsKtTest.kt:
--------------------------------------------------------------------------------
1 | package kit.utils
2 |
3 | import org.junit.jupiter.api.Test
4 |
5 | import org.junit.jupiter.api.Assertions.*
6 |
7 | class UtilsKtTest {
8 |
9 | /**
10 | * Testing whether the runCommand function returns the correct output
11 | */
12 | @Test
13 | fun runCommand() {
14 | val command = "echo Hello World"
15 | val output = command.runCommand()
16 | assertEquals(
17 | /* expected = */ "Hello World",
18 | /* actual = */ output,
19 | /* message = */ "The output should be equal"
20 | )
21 | }
22 | /**
23 | * Testing whether it throws an exception when the command fails
24 | */
25 | @Test
26 | fun runCommandFail() {
27 | val command = "ls -l /non/existing/path"
28 | try {
29 | command.runCommand()
30 | fail("The command should fail")
31 | } catch (e: RuntimeException) {
32 | assertEquals(
33 | /* expected = */ "Command '$command' failed",
34 | /* actual = */ e.message,
35 | /* message = */ "The exception message should be equal"
36 | )
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/main/kotlin/kit/plumbing/Zlib.kt:
--------------------------------------------------------------------------------
1 | package kit.plumbing
2 |
3 | import java.io.ByteArrayOutputStream
4 | import java.util.zip.*
5 |
6 | /**
7 | * Zlib compression and decompression.
8 | */
9 | object Zlib {
10 |
11 | /**
12 | * Compresses the given byte array using the Zlib algorithm.
13 | * @param content The byte array to compress.
14 | * @return The compressed byte array.
15 | */
16 | @JvmStatic
17 | fun deflate(content: ByteArray): ByteArray {
18 | val deflater = Deflater()
19 | deflater.setInput(content)
20 | deflater.finish()
21 | val buffer = ByteArray(1024)
22 | val outputStream = ByteArrayOutputStream()
23 | while (!deflater.finished()) {
24 | val count = deflater.deflate(buffer)
25 | outputStream.write(buffer, 0, count)
26 | }
27 | outputStream.close()
28 | return outputStream.toByteArray()
29 | }
30 |
31 | /**
32 | * Decompresses the given byte array using the Zlib algorithm.
33 | * @param content The byte array to decompress.
34 | * @return The decompressed byte array.
35 | */
36 | @JvmStatic
37 | fun inflate(content: ByteArray): ByteArray {
38 | val inflater = Inflater()
39 | inflater.setInput(content)
40 | val buffer = ByteArray(1024)
41 | val outputStream = ByteArrayOutputStream()
42 | while (!inflater.finished()) {
43 | val count = inflater.inflate(buffer)
44 | outputStream.write(buffer, 0, count)
45 | }
46 | outputStream.close()
47 | return outputStream.toByteArray()
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/kotlin/kit/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package kit.utils
2 | import java.io.File
3 | import java.nio.file.Files
4 | import java.util.concurrent.TimeUnit
5 | import kotlin.io.path.Path
6 |
7 | /**
8 | * Runs the given command and returns the output
9 | */
10 | fun String.runCommand(): String {
11 |
12 | val process = ProcessBuilder(*split(" ").toTypedArray())
13 | .redirectOutput(ProcessBuilder.Redirect.PIPE)
14 | .redirectError(ProcessBuilder.Redirect.PIPE)
15 | .start().apply { waitFor(60, TimeUnit.MINUTES) }
16 |
17 | val output = process.inputStream.bufferedReader().readText()
18 |
19 | if (process.exitValue() == 0) {
20 | return output.trim()
21 | } else {
22 | throw RuntimeException("Command '$this' failed")
23 | }
24 | }
25 |
26 | /**
27 | * convert a hex string to a byte array
28 | */
29 | fun String.hexStringToByteArray(): ByteArray {
30 | val len = length
31 | require(len % 2 == 0) { "Hex string must have even number of characters" }
32 | val byteArray = ByteArray(len / 2)
33 | var i = 0
34 | while (i < len) {
35 | byteArray[i / 2] = ((Character.digit(get(i), 16) shl 4)
36 | + Character.digit(get(i + 1), 16)).toByte()
37 | i += 2
38 | }
39 | return byteArray
40 | }
41 |
42 | /**
43 | * returns the relative path of this file to the given path
44 | * @param path the path to which the relative path is calculated
45 | */
46 | fun File.relativePath(path: String = System.getProperty("user.dir")): String = this.relativeTo(File(path)).path
47 |
48 | /**
49 | * helper function that returns the mode of a file
50 | * @param file the file
51 | * @return the mode based on git's documentation
52 | */
53 | fun getMode(file: File): String {
54 | val mode = when {
55 | // check if the file is executable
56 | file.canExecute() -> "100755"
57 | // check if the file is a symlink
58 | Files.isSymbolicLink(Path(file.path)) -> "120000"
59 | // then it's a normal file
60 | else -> "100644"
61 | }
62 | return mode
63 | }
64 |
65 | /**
66 | * colorize the output in blue
67 | */
68 | fun String.blue() = "\u001B[34m$this\u001B[0m"
69 |
70 | /**
71 | * colorize the output in red
72 | */
73 | fun String.red() = "\u001B[31m$this\u001B[0m"
74 |
75 | /**
76 | * colorize the output in green
77 | */
78 | fun String.green() = "\u001B[32m$this\u001B[0m"
79 |
80 | /**
81 | * colorize the output in yellow
82 | */
83 | fun String.yellow() = "\u001B[33m$this\u001B[0m"
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/src/main/kotlin/kit/porcelain/Config.kt:
--------------------------------------------------------------------------------
1 | package kit.porcelain
2 |
3 | import java.io.File
4 |
5 | /**
6 | * a singleton object that represents the config file
7 | */
8 | object Config {
9 | /**
10 | * consists of the following:
11 | * - sections, eg [ core ]
12 | * - keys, eg repositoryformatversion
13 | * - values, eg 0
14 | * */
15 | private fun getConfigFile() = File("${System.getProperty("user.dir")}/.kit/config")
16 | private val sections = mutableSetOf("core") // to be updated when more sections are added
17 | private val values = mutableMapOf(
18 | "core" to mutableMapOf(
19 | "repositoryformatversion" to "0", "filemode" to "true", "bare" to "false", "logallrefupdates" to "true"
20 | )
21 | )
22 |
23 | /**
24 | * unset the config file to its default values
25 | */
26 | fun unset() {
27 | sections.removeIf { it != "core" }
28 | }
29 |
30 | /**
31 | * write the config file
32 | */
33 | fun write() {
34 | getConfigFile().createNewFile()
35 | getConfigFile().writeText("")
36 | for (section in sections) {
37 | getConfigFile().appendText("[$section]\n")
38 | for ((key, value) in values[section]!!) {
39 | getConfigFile().appendText("\t$key = $value\n")
40 | }
41 | }
42 | }
43 |
44 | /**
45 | * read the config file attributes
46 | */
47 | fun read() {
48 | val lines = getConfigFile().readLines()
49 | for (line in lines) {
50 | if (line.startsWith("[")) {
51 | val section = line.substring(1, line.length - 1)
52 | sections.add(section)
53 | values[section] = mutableMapOf()
54 | } else if (line.startsWith("\t")) {
55 | val key = line.substring(line.indexOf("\t") + 1, line.indexOf(" = "))
56 | val value = line.substring(line.indexOf(" = ") + 3)
57 | values[sections.last()]!![key] = value
58 | }
59 | }
60 | }
61 |
62 | /**
63 | * set a value in the config file and write it
64 | * @param sectionWithKey the section and key separated by a dot, e.g. core.repositoryformatversion
65 | * @param value the value to be set
66 | */
67 | fun set(sectionWithKey: String, value: String) {
68 | val section = sectionWithKey.split(".")[0]
69 | val key = sectionWithKey.split(".")[1]
70 | if (!sections.contains(section)) {
71 | sections.add(section)
72 | values[section] = mutableMapOf()
73 | }
74 | values[section]!![key] = value
75 | write()
76 | }
77 |
78 | /**
79 | * get a value from the config file
80 | * @param sectionWithKey the section and key separated by a dot, e.g. core.repositoryformatversion
81 | * @return the value
82 | */
83 | fun get(sectionWithKey: String): String {
84 | val section = sectionWithKey.split(".")[0]
85 | val key = sectionWithKey.split(".")[1]
86 | if (section == "user" && (key == "name" || key == "email")) {
87 | if (!sections.contains(section)) {
88 | sections.add(section)
89 | values[section] = mutableMapOf()
90 | }
91 | if (values[section]!![key] == null) values[section]!![key] = "Kit $key"
92 | }
93 | return values[section]!![key]!!
94 | }
95 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Git A Home-Made Recipe With Kotlin
2 |
3 |
4 |
5 |
6 | ## About
7 |
8 | This Project is My Attempt To Reimplement Git With Kotlin.
9 | Why? I See You Wondering... Why Not?\
10 | I've Been Reading About Git For Quite Some Time And have Explained It To Many Of My Peers.\
11 | And While I Was Learning Kotlin With JetBrains Academy, One Of The Projects Was About Version Control System, especially
12 | Git, But The Implementation Level Wasn't That Interesting, After That I came Across [CodeCrafters](https://codecrafters.io/),
13 | They have modules for building some projects with theme `Build Your Own`, and one of them was `Build Your Own Git`,
14 | but it didn't also catch my attention, and they didn't have a kotlin version, so I decided to implement it myself, and make it compatible with git and I named it `kit (kotlin implementation of git).`\
15 | my intention for this project was to learn more about git, and build a decent project with kotlin considering that it's my favorite language.\
16 | also I'm not implementing the whole git, I'm just implementing the core features of git, and I'm not implementing it in the best way, I'm just trying to implement it in a way that I can understand it, and hopefully others can understand it too.
17 | so it's **an educational project**, also if you want to contribute, you're welcome.
18 |
19 | ## Features
20 |
21 | git have two types of commands:
22 | - **Low Level Commands ie plumbing commands**: these commands are the core commands of git, and they are the commands that git uses to implement the high level commands, you rarely use these commands directly, but you can use them if you want to.
23 | - **High Level Commands ie porcelain commands**: these commands are the commands that the user uses to interact with git.
24 |
25 | git model is based on four objects:
26 | - **Blob**: a blob is a file, it's the smallest unit of git, it's the content of a file.
27 | - **Tree**: a tree is a directory, it's a collection of blobs and trees.
28 | - **Commit**: a commit is a snapshot of the repository, it's a collection of trees and blobs.
29 | - **Tag**: a tag is a label for a commit, it's a pointer to a commit.
30 |
31 | I'v Implemented the following commands:
32 | ## Plumbing Commands
33 | - [x] hash-object
34 | - [x] cat-file
35 | - [x] update-index
36 | - [x] write-tree
37 | - [x] commit-tree
38 | - [x] ls-files
39 |
40 | ## Porcelain Commands
41 | - [x] init
42 | - [x] add
43 | - [x] unStage
44 | - [x] commit
45 | - [x] tag
46 | - [x] log
47 | - [x] status
48 | - [x] checkout
49 | - [x] branch
50 | - [x] config
51 |
52 | ## Installation
53 |
54 | You don't need to install anything, just download the jar file from the [releases](https://github.com/Badr-1/Git-A-Home-Made-Recipe-With-Kotlin/releases) , and run it with
55 | ```bash
56 | java -jar kit.jar
57 | ```
58 | and to use it like you use git, you can create a bash script with the following content and add it to your path
59 | ```bash
60 | #!/bin/bash
61 | java -jar /path/to/kit.jar "$@"
62 | ```
63 | just make sure that you replace `/path/to/kit.jar` with the actual path to the jar file.
64 |
65 |
66 |
67 | ## Screenshots
68 |
69 | ### Init a repo creating a file and checking status
70 |
71 |
72 |
73 |
74 |
75 | ### Adding file to index and configuring user then committing and log to see the commit
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | ### Create a branch and checkout to it
86 |
87 |
88 |
89 |
90 |
91 | ### Create a tag
92 |
93 |
94 |
95 |
96 |
97 | ### Convert repo to git repo
98 |
99 |
100 |
101 |
102 |
103 | ### Help
104 |
105 |
106 |
107 |
108 |
109 | ## How to contribute
110 | 1. Fork the project
111 | 2. Clone the project
112 | 3. Create a new branch with the name of the feature you're going to implement
113 | 4. Implement the feature
114 | 5. write tests for the feature
115 | 6. commit and push your changes
116 | 7. create a pull request
117 |
118 | And I'll gladly review and merge your changes 🎉
119 |
120 | ## License
121 | [MIT](https://choosealicense.com/licenses/mit/)
122 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/src/main/kotlin/kit/plumbing/GitIndex.kt:
--------------------------------------------------------------------------------
1 | package kit.plumbing
2 |
3 | import kit.utils.*
4 | import java.io.File
5 | import java.nio.ByteBuffer
6 | import java.nio.file.Files
7 | import java.time.Instant
8 | import kotlin.experimental.and
9 |
10 | /**
11 | * Singleton class for managing the Git index file
12 | */
13 | object GitIndex {
14 | private val entries = mutableListOf()
15 | private lateinit var signature: String
16 | private var version: Int = 2
17 | private var entryCount: Int = 0
18 | private val indexFile = File("${System.getProperty("user.dir")}/.kit/index")
19 |
20 | /**
21 | * gets count of entries in the index file
22 | */
23 | fun getEntryCount(): Int {
24 | return entryCount
25 | }
26 |
27 | /**
28 | * data class for storing index file entries
29 | */
30 | data class GitIndexEntry(
31 | var ctimeSeconds: Int,
32 | var ctimeNanoSeconds: Int,
33 | var mtimeSeconds: Int,
34 | var mtimeNanoSeconds: Int,
35 | val dev: Int,
36 | val ino: Int,
37 | val mode: Int,
38 | val uid: Int,
39 | val gid: Int,
40 | var fileSize: Int,
41 | var sha1: String,
42 | val flags: Int,
43 | val path: String,
44 | val padding: Int
45 | ) {
46 | /**
47 | * writes the entry to the index file
48 | */
49 | fun write() {
50 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(ctimeSeconds).array())
51 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(ctimeNanoSeconds).array())
52 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(mtimeSeconds).array())
53 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(mtimeNanoSeconds).array())
54 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(dev).array())
55 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(ino).array())
56 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(mode).array())
57 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(uid).array())
58 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(gid).array())
59 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(fileSize).array())
60 | indexFile.appendBytes(sha1.hexStringToByteArray())
61 | indexFile.appendBytes(ByteBuffer.allocate(2).putShort(flags.toShort()).array())
62 | indexFile.appendBytes(path.toByteArray())
63 | indexFile.appendBytes(ByteArray(padding))
64 | }
65 | }
66 |
67 | init {
68 | readIndex()
69 | }
70 |
71 | /**
72 | * clear variables after index file is deleted
73 | */
74 | fun clearIndex() {
75 | entries.clear()
76 | entryCount = 0
77 | }
78 |
79 | /**
80 | * reads the index file and stores the entries in the entries list
81 | */
82 | fun readIndex() {
83 | if (indexFile.exists()) {
84 | val indexBytes: ByteArray = indexFile.readBytes()
85 | var offset = 0
86 | // The first 12 bytes of the index file are a header
87 | signature = String(indexBytes.copyOfRange(0, 4))
88 | version = (ByteBuffer.wrap(indexBytes.copyOfRange(4, 8)).int)
89 | entryCount = (ByteBuffer.wrap(indexBytes.copyOfRange(8, 12)).int)
90 | offset += 12
91 |
92 | // The next section of the index file consists of entry metadata
93 | for (i in 0 until entryCount) {
94 | val ctimeSeconds = (ByteBuffer.wrap(indexBytes.copyOfRange(offset, offset + 4)).int)
95 | val ctimeNanoSeconds = (ByteBuffer.wrap(indexBytes.copyOfRange(offset + 4, offset + 8)).int)
96 | val mtimeSeconds = (ByteBuffer.wrap(indexBytes.copyOfRange(offset + 8, offset + 12)).int)
97 | val mtimeNanoSeconds = (ByteBuffer.wrap(indexBytes.copyOfRange(offset + 12, offset + 16)).int)
98 | val dev = (ByteBuffer.wrap(indexBytes.copyOfRange(offset + 16, offset + 20)).int)
99 | val ino = (ByteBuffer.wrap(indexBytes.copyOfRange(offset + 20, offset + 24)).int)
100 | val mode = ByteBuffer.wrap(indexBytes.copyOfRange(offset + 24, offset + 28)).int
101 | val uid = (ByteBuffer.wrap(indexBytes.copyOfRange(offset + 28, offset + 32)).int)
102 | val gid = (ByteBuffer.wrap(indexBytes.copyOfRange(offset + 32, offset + 36)).int)
103 | val fileSize = (ByteBuffer.wrap(indexBytes.copyOfRange(offset + 36, offset + 40)).getInt())
104 | // read next 20 bytes for sha1 and decode it to hex string and put it in a variable
105 | val sha1 = indexBytes.copyOfRange(offset + 40, offset + 60).joinToString("") { "%02x".format(it) }
106 | val flags = (ByteBuffer.wrap(indexBytes.copyOfRange(offset + 60, offset + 62)).short)
107 | // 1 bit for assume valid
108 | // val assumeValid = flags and 0x8000.toShort() != 0.toShort()
109 | // 1 bit for extended
110 | // val extended = flags and 0x4000.toShort() != 0.toShort()
111 | // 1 bit for stageOne
112 | // val stageOne = flags and 0x2000.toShort() != 0.toShort()
113 | // 1 bit for stageTwo
114 | // val stageTwo = flags and 0x1000.toShort() != 0.toShort()
115 | // val stage = stageOne.toString() + stageTwo.toString()
116 | // 12 bits for name length
117 | val nameLength = flags and 0x0FFF.toShort()
118 | val path = String(indexBytes.copyOfRange(offset + 62, offset + 62 + nameLength))
119 | offset += 62 + nameLength
120 |
121 | // convert this ((8 - ((62 + nameLength) % 8)) or 8) to int
122 | val padding = ((8 - ((62 + nameLength) % 8)).coerceAtMost(8))
123 | // treat padding as a binary number and convert it to int
124 |
125 | // read next padding bytes
126 | // val paddingBytes = indexBytes.copyOfRange(offset, offset + padding)
127 | offset += padding
128 |
129 | entries.add(
130 | GitIndexEntry(
131 | ctimeSeconds,
132 | ctimeNanoSeconds,
133 | mtimeSeconds,
134 | mtimeNanoSeconds,
135 | dev,
136 | ino,
137 | mode,
138 | uid,
139 | gid,
140 | fileSize,
141 | sha1,
142 | flags.toInt(),
143 | path,
144 | padding
145 | )
146 | )
147 | }
148 | // The final section of the index file consists of the SHA1 of the index file
149 | indexBytes.copyOfRange(offset, offset + 20).joinToString("") { "%02x".format(it) }
150 | } else {
151 | indexFile.createNewFile()
152 | signature = "DIRC"
153 | version = 2
154 | entryCount = 0
155 | indexFile.writeBytes(signature.toByteArray())
156 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(version).array())
157 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(entryCount).array())
158 | val sha1 = sha1(indexFile.readBytes())
159 | indexFile.appendBytes(sha1.hexStringToByteArray())
160 | entries.clear()
161 | }
162 | }
163 |
164 | /**
165 | * list all files in the index
166 | */
167 | fun list(): String {
168 | return entries.joinToString("\n") { it.path }
169 | }
170 |
171 | /**
172 | * add a file to the index entries
173 | * @param file the file to add
174 | * @param sha1 the sha1 of the file
175 | * @param cacheInfo the cache info of the file
176 | */
177 | fun add(file: File, sha1: String, cacheInfo: String) {
178 | // check if the file is already in the index
179 | if (entries.any { it.path == file.relativePath() }) {
180 | // check if the file is modified
181 | val entry = entries.first { it.path == file.relativePath() }
182 | if (entry.sha1 == sha1 && entry.mode == cacheInfo.toInt(8)) {
183 | return
184 | }
185 | remove(file)
186 | }
187 | // write header
188 | indexFile.writeBytes("".toByteArray())
189 | indexFile.writeBytes(signature.toByteArray())
190 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(version).array())
191 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(entryCount + 1).array())
192 |
193 | // add file to entries
194 | entries.add(makeEntry(file, sha1, cacheInfo))
195 |
196 | // write entries
197 | entries.forEach { entry ->
198 | entry.write()
199 | }
200 |
201 | // write sha1
202 | indexFile.appendBytes(sha1(indexFile.readBytes()).hexStringToByteArray())
203 | entryCount++
204 | }
205 |
206 | /**
207 | * remove a file from the index entries
208 | * @param file the file to remove
209 | */
210 | fun remove(file: File) {
211 | // check if the file is in the index
212 | if (entries.any { it.path == file.relativePath() }) {
213 | // remove the file from the index
214 | entries.removeIf { it.path == file.relativePath() }
215 | // write header
216 | indexFile.writeBytes("".toByteArray())
217 | indexFile.writeBytes(signature.toByteArray())
218 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(version).array())
219 | indexFile.appendBytes(ByteBuffer.allocate(4).putInt(entryCount - 1).array())
220 |
221 | // write entries
222 | entries.forEach { entry ->
223 | entry.write()
224 | }
225 |
226 | // write sha1
227 | indexFile.appendBytes(sha1(indexFile.readBytes()).hexStringToByteArray())
228 | entryCount--
229 | }
230 | }
231 |
232 | /**
233 | * get index entry by path
234 | * @param path path of the file
235 | * @return index entry
236 | */
237 | fun get(path: String): GitIndexEntry? {
238 | return entries.firstOrNull { it.path == path }
239 | }
240 |
241 | /**
242 | * get all index entries
243 | * @return list of index entries
244 | */
245 | fun entries(): List {
246 | return mutableListOf(*entries.toTypedArray())
247 | }
248 |
249 | /**
250 | * create a new index entry
251 | * @param file file to be added
252 | * @param sha1 sha1 of the file
253 | * @param cacheInfo cache info of the file
254 | * @return index entry
255 | */
256 | private fun makeEntry(file: File, sha1: String, cacheInfo: String): GitIndexEntry {
257 | val attr = Files.readAttributes(file.toPath(), "unix:*")
258 | val creationTime = Instant.parse("${attr["creationTime"]!!}").epochSecond
259 | val creationNanoTime = Instant.parse("${attr["creationTime"]!!}").nano
260 | val lastModifiedTime = Instant.parse("${attr["lastModifiedTime"]!!}").epochSecond
261 | val lastModifiedNanoTime = Instant.parse("${attr["lastModifiedTime"]!!}").nano
262 | val dev = attr["dev"]!!.toString().toInt()
263 | val ino = attr["ino"]!!.toString().toInt()
264 | val mode = cacheInfo.toInt(8)
265 | val uid = attr["uid"]!!.toString().toInt()
266 | val gid = attr["gid"]!!.toString().toInt()
267 | val fileSize = file.readBytes().size
268 | val name = file.relativePath()
269 | val flags = 0x0000 + name.length
270 | val entrySize = 62 + name.length
271 | val padding = ((8 - ((entrySize) % 8)).coerceAtMost(8))
272 | return GitIndexEntry(
273 | creationTime.toInt(),
274 | creationNanoTime,
275 | lastModifiedTime.toInt(),
276 | lastModifiedNanoTime,
277 | dev,
278 | ino,
279 | mode,
280 | uid,
281 | gid,
282 | fileSize,
283 | sha1,
284 | flags,
285 | name,
286 | padding
287 | )
288 | }
289 | }
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | {
68 | "keyToString": {
69 | "RunOnceActivity.OpenProjectViewOnStart": "true",
70 | "RunOnceActivity.ShowReadmeOnStart": "true",
71 | "SHARE_PROJECT_CONFIGURATION_FILES": "true",
72 | "com.intellij.testIntegration.createTest.CreateTestDialog.defaultLibrary": "JUnit5",
73 | "com.intellij.testIntegration.createTest.CreateTestDialog.defaultLibrarySuperClass.JUnit5": "",
74 | "git-widget-placeholder": "main",
75 | "last_opened_file_path": "/home/badr/IdeaProjects/Git-A-Home-Made-Recipe-With-Kotlin",
76 | "project.structure.last.edited": "Modules",
77 | "project.structure.proportion": "0.0",
78 | "project.structure.side.proportion": "0.0",
79 | "settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings"
80 | }
81 | }
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | false
120 | true
121 | false
122 | true
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | false
144 | true
145 | false
146 | true
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 | false
168 | true
169 | false
170 | true
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 | false
192 | true
193 | false
194 | true
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 | false
214 | true
215 | false
216 | true
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 | 1680300470808
234 |
235 |
236 | 1680300470808
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
--------------------------------------------------------------------------------
/src/main/kotlin/kit/plumbing/plumbing.kt:
--------------------------------------------------------------------------------
1 | package kit.plumbing
2 |
3 | import kit.porcelain.Config
4 | import kit.utils.*
5 | import java.io.*
6 | import java.security.MessageDigest
7 | import java.text.SimpleDateFormat
8 | import java.time.ZonedDateTime
9 | import java.time.format.DateTimeFormatter
10 | import java.util.*
11 |
12 |
13 | /**
14 | * @param path the path of the file to be added to the tree
15 | * @param option the option to use (either -a or -d)
16 | * @return success message
17 | */
18 | fun updateIndex(path: String, option: String, sha1: String = "", cacheInfo: String = "") {
19 | when {
20 | option == "-d" -> {
21 | GitIndex.remove(File(path))
22 | println("Removed ${File(path).relativePath().red()} from index")
23 | }
24 |
25 | option == "-a" && cacheInfo.isNotEmpty() -> {
26 | GitIndex.add(File(path), sha1, cacheInfo)
27 | println("Added ${File(path).relativePath().green()} to index")
28 | }
29 |
30 | else -> {
31 | throw IllegalArgumentException("usage: update-index (-a|-d) ")
32 | }
33 | }
34 | }
35 |
36 | /**
37 | * @param treeHash the hash of the tree to be committed
38 | * @param parentCommit the hash of the parent commit
39 | * @param commitMessage the message of the commit
40 | * @return the sha1 hash of the commit
41 | */
42 | fun commitTree(treeHash: String, commitMessage: String, parentCommit: String = ""): String {
43 | // validate the tree
44 | if (!treeHash.objectExists()) {
45 | throw Exception("The tree doesn't exist")
46 | }
47 | // validate the parent commit
48 | if (parentCommit.isNotEmpty()) {
49 | if (!parentCommit.objectExists()) {
50 | throw Exception("parent commit doesn't exist")
51 | }
52 | }
53 | if (File("${System.getProperty("user.dir")}/.kit/config").exists()) {
54 | Config.read()
55 | }
56 | val now = ZonedDateTime.now()
57 | val timestamp = "${now.toEpochSecond()} ${now.format(DateTimeFormatter.ofPattern(" Z "))}"
58 | val tree = "tree $treeHash\n"
59 | val parent = if (parentCommit.isEmpty()) parentCommit else "parent $parentCommit\n"
60 | val author =
61 | "author ${Config.get("user.name")} <${Config.get("user.email")}> $timestamp\n"
62 | val committer =
63 | "committer ${Config.get("user.name")} <${Config.get("user.email")}> $timestamp\n\n"
64 | val message = commitMessage.ifEmpty { throw Exception("commit message is empty") } + "\n"
65 | val content = tree + parent + author + committer + message
66 | return hashObject(content, type = "commit", write = true)
67 | }
68 |
69 | /**
70 | * display the content of an object
71 | * @param hashObject the hash of the object
72 | * @param option the option to use (either -t, -s or -p)
73 | * @return the content of the object
74 | */
75 | fun catFile(hashObject: String, option: String): String {
76 | val workingDirectory = System.getProperty("user.dir")
77 | val path = "${workingDirectory}/.kit/objects/${hashObject.substring(0, 2)}/${hashObject.substring(2)}"
78 | val content = Zlib.inflate(File(path).readBytes())
79 | val contentStr = content.toString(Charsets.UTF_8)
80 | val type = contentStr.substringBefore(" ")
81 | val size = contentStr.substringAfter(" ").substringBefore("\u0000").toInt()
82 | val contentWithoutHeader = contentStr.substringAfter("\u0000")
83 |
84 | /**
85 | * content without the header depends on the type of the object
86 | * - object: just use UTF-8 encoding
87 | * - commit: just use UTF-8 encoding
88 | * - tree: use the tree parser
89 | * */
90 |
91 | return when (option) {
92 | "-t" -> type.apply { println(this) }
93 | "-s" -> size.toString().apply { println(this) }
94 | "-p" -> {
95 | if (type == "tree") {
96 | val list = content.toMutableList()
97 | // remove till the first NUL
98 | list.subList(0, list.indexOf(0.toByte()) + 1).clear()
99 | parseTreeContent(list)
100 | } else
101 | contentWithoutHeader.apply { println(this) }
102 | }
103 |
104 | else -> throw IllegalArgumentException("usage: cat-file [-t | -s | -p] ")
105 | }
106 | }
107 |
108 |
109 | /**
110 | * @param directory the directory to write the tree for
111 | * @param write whether to write the tree to the object database or not
112 | * @return the sha1 hash of the tree
113 | */
114 | fun writeTree(directory: String, write: Boolean = false): String {
115 | val chosenDirectory = File(directory)
116 | if (!chosenDirectory.exists()) {
117 | throw FileNotFoundException("$directory (No such file or directory)")
118 | }
119 | if (chosenDirectory.listFiles()!!.toMutableList().none { it.name != ".kit" }) {
120 | // why not throw an exception?
121 | // because we want to run this recursively
122 | // and there's no problem if a directory is empty just avoid it
123 | return ""
124 | }
125 | val files = chosenDirectory.listFiles()!!.toMutableList().filter { it.name != ".kit" }
126 | val entries = mutableListOf()
127 | for (file in files) {
128 | if (file.isDirectory) {
129 | val workingDirectory = System.getProperty("user.dir")
130 | System.setProperty("user.dir", file.parentFile.path)
131 | val treeHash = writeTree(file.path, write)
132 | if (treeHash.isNotEmpty())
133 | entries.add(TreeEntry("40000", file.name, treeHash))
134 | System.setProperty("user.dir", workingDirectory)
135 | } else {
136 | /**
137 | * there are three cases for files being added to the tree
138 | *
139 | * 1- the file is not in the index => ignore it, still untracked
140 | *
141 | * 2- the file is in the index with the same hash => add it to the tree, the file is either:
142 | * 1- not modified
143 | * 2- modified and staged
144 | *
145 | * 3- the file is in the index with a different hash => add what's in the index to the tree
146 | * this means that the file is modified but not staged yet.
147 | */
148 | /**
149 | * there are three cases for files being added to the tree
150 | *
151 | * 1- the file is not in the index => ignore it, still untracked
152 | *
153 | * 2- the file is in the index with the same hash => add it to the tree, the file is either:
154 | * 1- not modified
155 | * 2- modified and staged
156 | *
157 | * 3- the file is in the index with a different hash => add what's in the index to the tree
158 | * this means that the file is modified but not staged yet.
159 | */
160 | val indexEntry = GitIndex.get(file.relativePath())
161 | ?: // case 1
162 | continue
163 |
164 | /**
165 | * mode per git documentation:
166 | * @see git Objects - Tree Objects
167 | */
168 | val mode = getMode(file)
169 | // case 2 & 3
170 | entries.add(TreeEntry(mode, file.name, indexEntry.sha1))
171 | }
172 | }
173 | if (entries.isEmpty())
174 | return ""
175 |
176 | return mkTree(
177 | entries,
178 | write
179 | ).apply {
180 | if (write) {
181 | println("writing object of type ${"tree".blue()} into the object database ${this.substring(0, 7).red()}")
182 | catFile(this, "-p")
183 | }
184 |
185 | }
186 | }
187 |
188 | /**
189 | * List the files in the index
190 | */
191 | fun lsFiles(): String {
192 | return GitIndex.list().apply { println(this) }
193 | }
194 |
195 | /**
196 | * Hashes the given file content using sha1
197 | * @param path the file path
198 | * @param write whether to write the hash to the object directory
199 | * @return the sha1 hash of the file
200 | */
201 | fun hashObject(path: String, type: String = "blob", write: Boolean = false): String {
202 | val workingDirectory = System.getProperty("user.dir")
203 | // read the file content
204 | val content = when (type) {
205 | "blob" -> {
206 | // check if the file exists
207 | if (!File(path).exists()) {
208 | throw Exception("File does not exist")
209 | }
210 | File(path).readText()
211 | }
212 |
213 | else -> path
214 | }
215 | // encode the content
216 | val encoding = "UTF-8"
217 | val bytes = content.toByteArray(charset(encoding))
218 | // create the prefix as described in the git documentation
219 | /**
220 | * The prefix is a header that is prepended to the content before hashing.
221 | * It consists of the object type, a space, and the size of the content in bytes.
222 | * The header is followed by a NUL byte (0x00) and then the content.
223 | */
224 | val prefix = "$type ${bytes.size}\u0000"
225 | val sha1 = sha1(prefix.toByteArray(charset(encoding)) + bytes)
226 | if (write) {
227 | val objectPath = sha1.objectPath()
228 | // make the directory if it doesn't exist
229 | val objectDatabase = File("${workingDirectory}/.kit/objects")
230 | if (!objectDatabase.exists()) {
231 | // print files in the current directory
232 | throw Exception("The repository doesn't exist")
233 | } else {
234 | File(objectPath).parentFile.mkdirs()
235 | }
236 | // compress the file content using zlib
237 | val compressed = Zlib.deflate(prefix.toByteArray() + content.toByteArray())
238 | // write the compressed content to the file
239 | File(objectPath).writeBytes(compressed)
240 | }
241 |
242 | return sha1.apply {
243 | when {
244 | write && type == "blob" -> println(
245 | "writing " + File(path).relativePath()
246 | .yellow() + " object of type " + "blob".blue() + " into the object database " + this.substring(0, 7)
247 | .red()
248 | )
249 |
250 | write && type == "commit" -> println(
251 | "writing object of type " + "commit".blue() + " into the object database " + this.substring(
252 | 0,
253 | 7
254 | ).red()
255 | )
256 |
257 | write && type == "tag" -> println(
258 | "writing object of type " + "tag".blue() + " into the object database " + this.substring(
259 | 0,
260 | 7
261 | ).red()
262 | )
263 | }
264 | }
265 | }
266 |
267 |
268 | /********** helper functions **********/
269 |
270 | /**
271 | * Hashes the given bytes using sha1
272 | * @param bytes the bytes to be hashed
273 | * @return the sha1 hash of the bytes
274 | */
275 | fun sha1(bytes: ByteArray): String {
276 | val digest = MessageDigest
277 | .getInstance("SHA-1")
278 | digest.reset()
279 | digest.update(bytes)
280 | return digest.digest().joinToString("") { "%02x".format(it) }
281 | }
282 |
283 | /**
284 | * @param entries the list of entries to be added to the tree
285 | * @param write whether to write the tree to the object database or not
286 | * @return the sha1 hash of the tree
287 | */
288 | fun mkTree(entries: List, write: Boolean): String {
289 | var entriesContent: ByteArray = byteArrayOf()
290 | entries.forEach { entry ->
291 | val entryContent =
292 | entry.mode.toByteArray() + " ".toByteArray() + entry.path.toByteArray() + "\u0000".toByteArray() + entry.hash.hexStringToByteArray()
293 | entriesContent += entryContent
294 | }
295 | val prefix = "tree ${entriesContent.size}\u0000"
296 | val content = prefix.toByteArray(Charsets.UTF_8) + entriesContent
297 | val sha1 = sha1(content)
298 | if (write) {
299 | val workingDirectory = System.getProperty("user.dir")
300 | val path = sha1.objectPath()
301 | val objectDatabase = File("${workingDirectory}/.kit/objects")
302 | if (!objectDatabase.exists()) {
303 | // print files in the current directory
304 | throw Exception("The repository doesn't exist")
305 | } else {
306 | File(path).parentFile.mkdirs()
307 | }
308 | // compress the file content using zlib
309 | val compressed =
310 | Zlib.deflate(prefix.toByteArray() + entriesContent)
311 | // write the compressed content to the file
312 | File(path).writeBytes(compressed)
313 | }
314 | return sha1
315 | }
316 |
317 |
318 | /**
319 | * data class to represent a tree entry
320 | * @param mode the mode of the entry
321 | * @param path the path of the entry
322 | * @param hash the hash of the entry
323 | */
324 | data class TreeEntry(val mode: String, val path: String, val hash: String)
325 |
326 |
327 | /**
328 | * helper function for catFile to parse the content of a tree
329 | * @param contentWithoutHeader the content of the tree without the header
330 | * @return the parsed content of the tree
331 | */
332 | fun parseTreeContent(contentWithoutHeader: MutableList): String {
333 | /**
334 | * format of entries in the tree:
335 | * mode SP path NUL sha1
336 | * mode: 6 bytes
337 | * SP: 1 byte
338 | * path: variable length
339 | * NUL: 1 byte
340 | * sha1: 20 bytes
341 | * parse till the end of the content
342 | * */
343 | val entries = mutableListOf()
344 | while (contentWithoutHeader.isNotEmpty()) {
345 | val mode = contentWithoutHeader.subList(0, contentWithoutHeader.indexOf(32.toByte())).toByteArray()
346 | .toString(Charsets.UTF_8)
347 | contentWithoutHeader.subList(0, contentWithoutHeader.indexOf(32.toByte())).clear()
348 | contentWithoutHeader.removeAt(0) // remove the space
349 | val path = contentWithoutHeader.subList(0, contentWithoutHeader.indexOf(0.toByte())).toByteArray()
350 | .toString(Charsets.UTF_8)
351 | contentWithoutHeader.subList(0, contentWithoutHeader.indexOf(0.toByte()) + 1).clear()
352 | val sha1 = contentWithoutHeader.subList(0, 20).joinToString("") { "%02x".format(it) }
353 | contentWithoutHeader.subList(0, 20).clear()
354 | entries.add(TreeEntry(mode, path, sha1))
355 | }
356 | return entries.joinToString("\n") {
357 | "${it.mode} ${getType(it.hash)} ${it.hash}\t${it.path}"
358 | }.apply {
359 | entries.joinToString("\n") {
360 | "${it.mode.green()} ${getType(it.hash).blue()} ${it.hash.red()}\t${it.path.yellow()}"
361 | }.apply {
362 | println(this)
363 | }
364 | }
365 | }
366 |
367 |
368 | /**
369 | * gets the path of the object in the object database
370 | * @receiver the hash of the object
371 | * @return the path of the object in the object database
372 | */
373 | fun String.objectPath(): String {
374 | return "${System.getProperty("user.dir")}/.kit/objects/${this.substring(0, 2)}/${this.substring(2)}"
375 | }
376 |
377 | /**
378 | * check if an object exists in the object database
379 | * @receiver the hash of the object
380 | * @return true if the object exists, false otherwise
381 | */
382 | fun String.objectExists(): Boolean {
383 | return File(this.objectPath()).exists()
384 | }
385 |
386 | fun getType(hashObject: String): String {
387 | val workingDirectory = System.getProperty("user.dir")
388 | val path = "${workingDirectory}/.kit/objects/${hashObject.substring(0, 2)}/${hashObject.substring(2)}"
389 | val content = Zlib.inflate(File(path).readBytes())
390 | val contentStr = content.toString(Charsets.UTF_8)
391 | return contentStr.substringBefore(" ")
392 | }
393 |
394 | fun getContent(hashObject: String): String {
395 | val workingDirectory = System.getProperty("user.dir")
396 | val path = "${workingDirectory}/.kit/objects/${hashObject.substring(0, 2)}/${hashObject.substring(2)}"
397 | val content = Zlib.inflate(File(path).readBytes())
398 | val contentStr = content.toString(Charsets.UTF_8)
399 | val type = contentStr.substringBefore(" ")
400 | val contentWithoutHeader = contentStr.substringAfter("\u0000")
401 |
402 |
403 | return if (type == "tree") {
404 | val list = content.toMutableList()
405 | // remove till the first NUL
406 | list.subList(0, list.indexOf(0.toByte()) + 1).clear()
407 | getTreeContent(list)
408 | } else
409 | contentWithoutHeader
410 | }
411 |
412 | fun getTreeContent(contentWithoutHeader: MutableList): String {
413 | /**
414 | * format of entries in the tree:
415 | * mode SP path NUL sha1
416 | * mode: 6 bytes
417 | * SP: 1 byte
418 | * path: variable length
419 | * NUL: 1 byte
420 | * sha1: 20 bytes
421 | * parse till the end of the content
422 | * */
423 | val entries = mutableListOf()
424 | while (contentWithoutHeader.isNotEmpty()) {
425 | val mode = contentWithoutHeader.subList(0, contentWithoutHeader.indexOf(32.toByte())).toByteArray()
426 | .toString(Charsets.UTF_8)
427 | contentWithoutHeader.subList(0, contentWithoutHeader.indexOf(32.toByte())).clear()
428 | contentWithoutHeader.removeAt(0) // remove the space
429 | val path = contentWithoutHeader.subList(0, contentWithoutHeader.indexOf(0.toByte())).toByteArray()
430 | .toString(Charsets.UTF_8)
431 | contentWithoutHeader.subList(0, contentWithoutHeader.indexOf(0.toByte()) + 1).clear()
432 | val sha1 = contentWithoutHeader.subList(0, 20).joinToString("") { "%02x".format(it) }
433 | contentWithoutHeader.subList(0, 20).clear()
434 | entries.add(TreeEntry(mode, path, sha1))
435 | }
436 | return entries.joinToString("\n") {
437 | "${it.mode} ${getType(it.hash)} ${it.hash}\t${it.path}"
438 | }
439 | }
440 |
441 | /********** helper functions **********/
--------------------------------------------------------------------------------
/src/main/kotlin/kit/porcelain/Porcaline.kt:
--------------------------------------------------------------------------------
1 | package kit.porcelain
2 |
3 | import kit.plumbing.*
4 | import kit.porcelain.ChangeType.*
5 | import kit.utils.*
6 | import java.io.File
7 | import java.nio.file.Path
8 | import java.text.SimpleDateFormat
9 | import java.time.*
10 | import java.util.*
11 | import kotlin.io.path.absolutePathString
12 | import kotlin.io.path.exists
13 |
14 |
15 | /**
16 | * Initialize a new repository or reinitialize an existing one
17 | * @param repositoryPath the name of the repository, if empty the current directory will be used
18 | * @return a message indicating the result of the operation
19 | */
20 | fun init(repositoryPath: Path = Path.of(System.getProperty("user.dir")).toAbsolutePath()): String {
21 | repositoryPath.toFile().mkdir()
22 | val kitPath = repositoryPath.resolve(".kit")
23 | val database = kitPath.resolve("objects")
24 | val refs = kitPath.resolve("refs")
25 | val heads = refs.resolve("heads")
26 | val head = kitPath.resolve("HEAD")
27 | val config = kitPath.resolve("config")
28 |
29 | if (kitPath.exists())
30 | return "Reinitialized existing Kit repository in ${kitPath.absolutePathString()}".apply { println(this) }
31 | // repository
32 | println("Creating ${kitPath.toFile().relativePath()} directory".green())
33 | kitPath.toFile().mkdir()
34 | // objects database
35 | println("Creating ${database.toFile().relativePath()} object database directory".green())
36 | database.toFile().mkdir()
37 | // refs
38 | println("Creating ${refs.toFile().relativePath()} directory".green())
39 | refs.toFile().mkdir()
40 | // heads
41 | println("Creating ${heads.toFile().relativePath()} for branches directory".green())
42 | heads.toFile().mkdir()
43 | // HEAD
44 | println("Creating ${head.toFile().relativePath()} file".green())
45 | head.toFile().writeText("ref: refs/heads/master")
46 | println("Writing default branch name to HEAD".green())
47 | // config
48 | // default config
49 | System.setProperty("user.dir", repositoryPath.toAbsolutePath().toString())
50 | Config.unset()
51 | Config.write()
52 | println("Writing default config to ${config.toFile().relativePath()}".green())
53 | return "Initialized empty Kit repository in ${File("$kitPath").absolutePath}".apply { println(this) }
54 | }
55 |
56 | /**
57 | * Add file to staging area
58 | * @param filePath the path of the file to be added
59 | */
60 | fun add(filePath: String) {
61 | // check if the file exists
62 | if (!File(filePath).exists()) {
63 | throw Exception("fatal: pathspec '$filePath' did not match any files")
64 | }
65 | // check if the file is in the working directory
66 | if (!File(filePath).absolutePath.startsWith(File(System.getProperty("user.dir")).absolutePath)) {
67 | throw Exception("fatal: pathspec '$filePath' is outside repository")
68 | }
69 | // check if the file is in the .kit directory
70 | if (File(filePath).absolutePath.startsWith(File("${System.getProperty("user.dir")}/.kit").absolutePath)) {
71 | return
72 | }
73 | val file = File(filePath)
74 | // update the index
75 | val mode = getMode(file)
76 | updateIndex(filePath, "-a", hashObject(filePath, write = true), mode)
77 | }
78 |
79 | /**
80 | * Remove file from staging area
81 | * @param filePath the path of the file to be removed
82 | */
83 | fun unstage(filePath: String) {
84 | // check if the file is in the working directory
85 | if (!File(filePath).absolutePath.startsWith(File(System.getProperty("user.dir")).absolutePath)) {
86 | throw Exception("fatal: pathspec '$filePath' is outside repository")
87 | }
88 | // check if the file exists or is in the index
89 | if (GitIndex.get(File(filePath).relativePath()) == null) {
90 | throw Exception("fatal: pathspec '$filePath' did not match any files")
91 | }
92 | updateIndex(filePath, "-d")
93 | }
94 |
95 | /**
96 | * return the status of the repository
97 | * @return the status of the repository
98 | */
99 | fun status(): String {
100 | // get all the files in the working directory except the .kit directory
101 | val workingDirectoryFiles =
102 | File(System.getProperty("user.dir")).walk().filter { it.isFile && !it.path.contains(".kit") }.toList()
103 | .map { it.relativePath() }
104 |
105 | // get all the files in the index
106 | val indexFiles = GitIndex.entries().map { it.path }
107 |
108 | // get all files in the HEAD commit tree
109 | val headCommitTreeFiles = getHeadCommitTreeFiles()
110 |
111 | val untrackedFiles = mutableListOf()
112 | val unStagedChanges = mutableListOf()
113 | val stagedChanges = mutableListOf()
114 |
115 | // untracked files are files that are in the working directory but not in the index
116 |
117 | untrackedFiles.addAll(workingDirectoryFiles.filter { !indexFiles.contains(it) }
118 | .map { Change(UNTRACKED, it) })
119 |
120 | /**
121 | * unStaged changes are the following (index vs working directory):
122 | * 1. content: the sha1 is different => modified
123 | * 2. mode: the mode is different => modified
124 | * 3. deleted: the file is deleted => deleted
125 | */
126 | // add modified files (content)
127 | unStagedChanges.addAll(
128 | workingDirectoryFiles.filter { indexFiles.contains(it) }
129 | .filter { GitIndex.get(it)!!.sha1 != hashObject("${System.getProperty("user.dir")}/$it") }
130 | .map { Change(MODIFIED, it) }
131 | )
132 | // add modified files (mode)
133 | unStagedChanges.addAll(
134 | workingDirectoryFiles.filter { indexFiles.contains(it) }
135 | .filter { GitIndex.get(it)!!.mode != getMode(File("${System.getProperty("user.dir")}/$it")).toInt(8) }
136 | .map { Change(MODIFIED, it) }
137 | )
138 | // add deleted files
139 | unStagedChanges.addAll(
140 | indexFiles.filter { !workingDirectoryFiles.contains(it) }
141 | .map { Change(DELETED, it) }
142 | )
143 | /**
144 | * staged changes are the following (index vs HEAD commit tree):
145 | * 1. content: the sha1 is different => modified
146 | * 2. mode: the mode is different => modified
147 | * 3. deleted: the file is deleted => deleted
148 | * 4. added: the file is added => added
149 | */
150 | if (headCommitTreeFiles.isEmpty()) {
151 | // add added files
152 | stagedChanges.addAll(
153 | workingDirectoryFiles.filter { indexFiles.contains(it) }
154 | .map { Change(ADDED, it) }
155 | )
156 | } else {
157 | // add added files
158 | stagedChanges.addAll(
159 | indexFiles.filter { !headCommitTreeFiles.map { headEntry -> headEntry.path }.contains(it) }
160 | .map { Change(ADDED, it) }
161 | )
162 | // add modified files (content)
163 | stagedChanges.addAll(
164 | indexFiles.filter { headCommitTreeFiles.map { headEntry -> headEntry.path }.contains(it) }
165 | .filter { common -> GitIndex.get(common)!!.sha1 != headCommitTreeFiles.find { it.path == common }!!.hash }
166 | .map { Change(MODIFIED, it) }
167 | )
168 | // add modified files (mode)
169 | stagedChanges.addAll(
170 | indexFiles.filter { headCommitTreeFiles.map { headEntry -> headEntry.path }.contains(it) }
171 | .filter { common ->
172 | GitIndex.get(common)!!.mode != headCommitTreeFiles.find { it.path == common }!!.mode.toInt(
173 | 8
174 | )
175 | }
176 | .map { Change(MODIFIED, it) }
177 | )
178 | // add deleted files
179 | stagedChanges.addAll(
180 | headCommitTreeFiles.map { it.path }.filter { !indexFiles.contains(it) }
181 | .map { Change(DELETED, it) }
182 | )
183 | }
184 |
185 |
186 | return statusString(untrackedFiles, stagedChanges, unStagedChanges).apply { println(this) }
187 | }
188 |
189 | // TODO think about adding amend option
190 | /**
191 | * commit the current index
192 | * @param message the commit message
193 | * @return the commit hash
194 | */
195 | fun commit(message: String): String {
196 | val head = getHead()
197 | val parent = when {
198 | head.matches(Regex("[0-9a-f]{40}")) -> head
199 | else -> {
200 | if (File("${System.getProperty("user.dir")}/.kit/refs/heads/$head").exists()) getBranchCommit(head)
201 | else ""
202 | }
203 | }
204 | if (parent.isNotEmpty()) {
205 | val parentTree = getTreeHash(parent)
206 | val indexTree = writeTree(System.getProperty("user.dir"), write = false)
207 | if (parentTree == indexTree) {
208 | throw Exception("nothing to commit, working tree clean")
209 | }
210 | }
211 | if (writeTree(System.getProperty("user.dir"), write = false).isEmpty()) {
212 | throw Exception("nothing to commit, working tree clean")
213 | }
214 |
215 | val treeHash = writeTree(System.getProperty("user.dir"), write = true)
216 | val commitHash = commitTree(treeHash, message, parent)
217 | if (head.matches(Regex("[0-9a-f]{40}"))) {
218 | File("${System.getProperty("user.dir")}/.kit/HEAD").writeText(commitHash)
219 | } else {
220 | val branch = File("${System.getProperty("user.dir")}/.kit/refs/heads/$head")
221 | branch.createNewFile()
222 | branch.writeText(commitHash)
223 | }
224 | return commitHash
225 | }
226 |
227 | /**
228 | * checkout HEAD to a specific commit or branch
229 | * @param ref the commit hash or branch name
230 | * @throws Exception if the ref is not a commit hash or a branch name
231 | */
232 | fun checkout(ref: String) {
233 | println("Checking out ${ref.red()}".blue())
234 | // ref could be a commit hash or a branch name
235 | val commitHash = when {
236 | // TODO think about supporting working with commit hashes less than 40 characters (short commit hashes)
237 | // TODO think about checking out files
238 | ref.matches(Regex("[0-9a-f]{40}")) -> {
239 | // change the HEAD
240 | File("${System.getProperty("user.dir")}/.kit/HEAD").writeText(ref)
241 | println("Writing ${ref.red()} to ${".kit/HEAD".blue()}")
242 | ref
243 | }
244 |
245 | else -> {
246 | val branch = File("${System.getProperty("user.dir")}/.kit/refs/heads/$ref")
247 | val tag = File("${System.getProperty("user.dir")}/.kit/refs/tags/$ref")
248 | if (!branch.exists() && !tag.exists()) {
249 | throw Exception("error: pathspec '$ref' did not match any file(s) known to kit")
250 | }
251 | if (branch.exists()) {
252 | // change the HEAD
253 | File("${System.getProperty("user.dir")}/.kit/HEAD").writeText("ref: refs/heads/$ref")
254 | println("Writing ${"ref: refs/heads/$ref".red()} to ${".kit/HEAD".blue()}")
255 | branch.readText()
256 | } else // tag
257 | {
258 | // change the HEAD
259 | val tagCommit = getContent(tag.readText()).split("\n")[0].split(" ")[1]
260 | File("${System.getProperty("user.dir")}/.kit/HEAD").writeText(tagCommit)
261 | println("Writing ${tagCommit.red()} to ${".kit/HEAD".blue()}")
262 | tagCommit
263 | }
264 |
265 | }
266 | }
267 |
268 | updateWorkingDirectory(commitHash)
269 | }
270 |
271 | /**
272 | * log the history of the repository
273 | * it traverses from the HEAD commit to the first commit
274 | */
275 | fun log() {
276 | val commits = getHistory()
277 | if (commits.isEmpty())
278 | return
279 | val branches = getBranches()
280 | val tags = getTags()
281 | val refs = branches + tags
282 | val head = when {
283 | File("${System.getProperty("user.dir")}/.kit/HEAD").readText().matches(Regex("[0-9a-f]{40}")) -> getHead()
284 | else -> {
285 | var head = File("${System.getProperty("user.dir")}/.kit/HEAD").readText().split(" ")[1]
286 | head =
287 | File("${System.getProperty("user.dir")}/.kit/$head").relativeTo(File("${System.getProperty("user.dir")}/.kit/refs/heads")).path
288 | head
289 | }
290 | }
291 | for (commit in commits) {
292 | val commitContent = getContent(commit)
293 | val hasParent = if (commitContent.contains("parent")) 0 else 1
294 | val authorLine = commitContent.split("\n")[2 - hasParent].split(" ").toMutableList()
295 | authorLine.removeFirst()
296 | authorLine.removeLast()
297 | val unixEpoch = authorLine.removeLast()
298 | val date = Date(unixEpoch.toLong() * 1000)
299 | val today = Date()
300 | val timeDifference = calculateDateTimeDifference(
301 | date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(),
302 | today.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()
303 | ).green()
304 | val author = "<${authorLine.joinToString(" ")}>".blue()
305 | val message = commitContent.split("\n")[4 - hasParent]
306 | val commitRefs = refs.filter { it.value == commit }.map { it.key }.toMutableList()
307 | if (commit == head) {
308 | commitRefs.add("HEAD")
309 | } else if (commitRefs.contains(head)) {
310 | commitRefs.remove(head)
311 | commitRefs.add("HEAD -> $head")
312 | }
313 | val commitHash = commit.substring(0, 7).red()
314 | val commitRefsString = if (commitRefs.isNotEmpty()) " (${commitRefs.joinToString()})".yellow() else ""
315 | println(
316 | "* $commitHash$commitRefsString $message $timeDifference $author"
317 | )
318 | }
319 | }
320 |
321 |
322 | /**
323 | * create a new branch
324 | * @param branchName the name of the branch
325 | * @param ref the commit hash to point to
326 | */
327 | fun branch(branchName: String, ref: String = "HEAD") {
328 | val branch = File("${System.getProperty("user.dir")}/.kit/refs/heads/$branchName")
329 | if (branch.exists()) {
330 | throw Exception("fatal: A branch named '$branchName' already exists.")
331 | } else {
332 | if (branchName.contains("/")) {
333 | branch.parentFile.mkdirs()
334 | }
335 | branch.createNewFile()
336 | }
337 | when (ref) {
338 | "HEAD" -> {
339 | val head = getHead()
340 | val dest = when {
341 | head.matches(Regex("[0-9a-f]{40}")) -> head
342 | else -> {
343 | getBranchCommit(head)
344 | }
345 | }
346 | println("Created branch " + branchName.green() + " at " + dest.substring(0, 7).red())
347 | println("file: " + branch.relativePath().green())
348 | branch.writeText(dest)
349 | }
350 |
351 | else -> {
352 | println("Created branch " + branchName.green() + " at " + ref.substring(0, 7).red())
353 | println("file: " + branch.relativePath().green())
354 | branch.writeText(ref)
355 | }
356 | }
357 |
358 | }
359 |
360 |
361 | /**
362 | * creates an annotated tag
363 | * @param tagName the name of the tag
364 | * @param tagMessage the message of the tag
365 | * @param objectHash the hash of the object to tag
366 | * @param tagType the type of the object to tag
367 | * @throws Exception if the tag already exists
368 | * @return the hash of the tag
369 | */
370 | fun tag(tagName: String, tagMessage: String, objectHash: String = getHEADHash(), tagType: String = "commit"): String {
371 | val tagFile = File("${System.getProperty("user.dir")}/.kit/refs/tags/$tagName")
372 | // if a tag has already been created, throw an exception
373 | if (tagFile.exists()) {
374 | throw Exception("fatal: tag '$tagName' already exists")
375 | }
376 | val content =
377 | "object $objectHash\ntype $tagType\ntag $tagName\ntagger ${Config.get("user.name")} <${Config.get("user.email")}> ${System.currentTimeMillis() / 1000} ${SimpleDateFormat("Z").format(Date())}\n\n$tagMessage\n"
378 | val hash = hashObject(content, "tag", true)
379 |
380 | // if refs/tags doesn't exist create it
381 | val tagsDirectory = File("${System.getProperty("user.dir")}/.kit/refs/tags")
382 | if (!tagsDirectory.exists()) {
383 | tagsDirectory.mkdirs()
384 | }
385 |
386 | // if tag has / create parent directories
387 | if (tagName.contains("/")) {
388 | if (!tagFile.parentFile.exists()) {
389 | tagFile.parentFile.mkdirs()
390 | }
391 | }
392 | println("Created tag " + tagName.green() + " for " + objectHash.substring(0, 7).red())
393 | println("file: " + tagFile.relativePath().green())
394 | tagFile.writeText(hash)
395 | return hash
396 | }
397 |
398 |
399 | /********** helper functions **********/
400 |
401 | /**
402 | * get the commit hash of a branch
403 | * @param branch the name of the branch
404 | * @return the commit hash of the branch
405 | * @throws Exception if the branch does not exist
406 | */
407 | private fun getBranchCommit(branch: String): String {
408 | if (!File("${System.getProperty("user.dir")}/.kit/refs/heads/$branch").exists()) {
409 | throw Exception("fatal: Not a valid object name: '$branch'.")
410 | }
411 | return File("${System.getProperty("user.dir")}/.kit/refs/heads/$branch").readText()
412 | }
413 |
414 | /**
415 | * update the working directory to the state of a commit
416 | * @param commitHash the hash of the commit
417 | */
418 | fun updateWorkingDirectory(commitHash: String) {
419 |
420 | println("Removing Files From Working Directory and Index".red())
421 | // delete all files that are in the index
422 | val index = GitIndex.entries()
423 | index.forEach {
424 | val file = File("${System.getProperty("user.dir")}/${it.path}")
425 | updateIndex(file.path, "-d")
426 | if (file.exists()) {
427 | file.delete()
428 | // delete empty directories
429 | var parent = file.parentFile
430 | while (parent.listFiles()!!.isEmpty()) {
431 | parent.delete()
432 | parent = parent.parentFile
433 | }
434 | }
435 | }
436 | println("Updating Files in Working Directory and Index".blue())
437 | // get the tree hash from the commit
438 | val treeHash = getTreeHash(commitHash)
439 | val treeEntries = getTreeEntries(treeHash)
440 | treeEntries.forEach {
441 | val file = File("${System.getProperty("user.dir")}/${it.path}")
442 | // create directories if necessary
443 | if (!file.parentFile.exists()) {
444 | file.parentFile.mkdirs()
445 | }
446 | file.createNewFile()
447 | file.writeText(getContent(it.hash))
448 | if (it.mode == "100755") {
449 | file.setExecutable(true)
450 | }
451 | updateIndex(file.path, "-a", hashObject(file.path), it.mode)
452 | }
453 | }
454 |
455 | /**
456 | * get the tree hash from a commit
457 | * @param commitHash the hash of the commit
458 | * @return the hash of the tree
459 | */
460 | fun getTreeHash(commitHash: String): String {
461 | val content = getContent(commitHash)
462 | return content.split("\n")[0].split(" ")[1]
463 | }
464 |
465 | /**
466 | * get the tree entries of a tree
467 | * @param treeHash the hash of the tree
468 | * @return a list of tree entries
469 | */
470 | fun getTreeEntries(treeHash: String): List {
471 | val content = getContent(treeHash)
472 | val treeEntries = mutableListOf()
473 | content.split("\n").map {
474 | val mode = it.split(" ")[0]
475 | val sha1 = it.split(" ")[2].split("\t")[0]
476 | val path = it.split("\t")[1]
477 | if (mode == "40000") {
478 | treeEntries.addAll(getTreeEntries(sha1).map { treeEntry ->
479 | TreeEntry(
480 | treeEntry.mode, "$path/${treeEntry.path}", treeEntry.hash
481 | )
482 | })
483 | } else {
484 | treeEntries.add(TreeEntry(mode, path, sha1))
485 | }
486 | }
487 | return treeEntries
488 | }
489 |
490 |
491 | enum class ChangeType {
492 | ADDED, MODIFIED, DELETED, UNTRACKED;
493 |
494 | override fun toString() = when (this) {
495 | ADDED -> "A"
496 | MODIFIED -> "M"
497 | DELETED -> "D"
498 | UNTRACKED -> "??"
499 | }
500 | }
501 |
502 | data class Change(val type: ChangeType, val path: String)
503 |
504 | /**
505 | * helper function that returns the status of the repository as a string
506 | * @param untrackedFiles the list of untracked files
507 | * @param staged the list of staged changes
508 | * @param unStaged the list of unStaged changes
509 | * @return the status of the repository as a string
510 | */
511 | fun statusString(
512 | untrackedFiles: List,
513 | staged: List,
514 | unStaged: List,
515 | ): String {
516 | val head = getHead()
517 | val onWhat = if (head.matches(Regex("[0-9a-f]{40}"))) {
518 | "Head detached at ${head.substring(0, 7).red()}"
519 | } else {
520 | "On branch ${head.green()}"
521 | }
522 | val status = mutableListOf()
523 | status += onWhat
524 | status += untrackedFiles.sortedBy { it.path }.joinToString("\n\t") { "${it.type} ${it.path}".red() }
525 | .ifEmpty { "" }
526 | status += staged.sortedBy { it.path }.joinToString("\n\t") { "${it.type} ${it.path}".green() }
527 | .ifEmpty { "" }
528 | status += unStaged.sortedBy { it.path }.joinToString("\n\t") { "${it.type} ${it.path}".yellow() }
529 | .ifEmpty { "" }
530 | status.removeIf { it.isEmpty() }
531 | return status.joinToString("\n\t")
532 | }
533 |
534 | /**
535 | * helper function that returns the list of files in the HEAD commit tree
536 | * @return the list of files in the HEAD commit tree
537 | */
538 | fun getHeadCommitTreeFiles(): MutableList {
539 | val head = getHead()
540 | return if (!head.matches(Regex("[0-9a-f]{40}"))) {
541 | return try {
542 | val branch = getBranchCommit(head)
543 | getTreeEntries(getTreeHash(branch)).toMutableList()
544 | } catch (e: Exception) {
545 | mutableListOf()
546 | }
547 | } else {
548 | getTreeEntries(getTreeHash(head)).toMutableList()
549 | }
550 |
551 | }
552 |
553 |
554 | /**
555 | * get the commit hash of a branch that is pointed to by HEAD
556 | */
557 | fun getHead(): String {
558 | val head = File("${System.getProperty("user.dir")}/.kit/HEAD").readText()
559 | return if (head.contains("ref: refs/heads/")) {
560 | val branch =
561 | File(System.getProperty("user.dir") + "/.kit/" + head.split(" ")[1]).relativePath("${System.getProperty("user.dir")}/.kit/refs/heads")
562 | branch
563 | } else {
564 | head
565 | }
566 | }
567 |
568 | /**
569 | * this is a helper function for the actual log command
570 | * @return the list of commits
571 | */
572 | fun getHistory(): List {
573 | val commits = mutableListOf()
574 | val head = getHead()
575 | var it = head
576 | if (!head.matches(Regex("[0-9a-f]{40}"))) { // if the head is a branch
577 | try {
578 | it = getBranchCommit(head) // throw an exception if the branch doesn't exist
579 | } catch (e: Exception) {
580 | return commits // return an empty list
581 | }
582 | }
583 | do {
584 | commits.add(it)
585 | it = getParent(it)
586 | } while (it != "")
587 |
588 | return commits
589 | }
590 |
591 | /**
592 | * calculate the difference between two dates
593 | * @param startDate the start date
594 | * @param endDate the end date
595 | * @return the difference between the two dates in the most significant time unit
596 | */
597 | fun calculateDateTimeDifference(startDate: LocalDateTime, endDate: LocalDateTime): String {
598 | val duration = Duration.between(startDate, endDate)
599 | val period = Period.between(startDate.toLocalDate(), endDate.toLocalDate())
600 |
601 | val years = period.years
602 | val months = period.months
603 | val days = period.days
604 |
605 | val hours = duration.toHours() % 24
606 | val minutes = duration.toMinutes() % 60
607 | val seconds = duration.seconds % 60
608 |
609 | // return the most significant time unit
610 | return "(${
611 | when {
612 | years > 0 -> "$years year${if (years > 1) "s" else ""} ago"
613 | months > 0 -> "$months month${if (months > 1) "s" else ""} ago"
614 | days > 0 -> "$days day${if (days > 1) "s" else ""} ago"
615 | hours > 0 -> "$hours hour${if (hours > 1) "s" else ""} ago"
616 | minutes > 0 -> "$minutes minute${if (minutes > 1) "s" else ""} ago"
617 | seconds > 0 -> "$seconds second${if (seconds > 1) "s" else ""} ago"
618 | else -> "just now"
619 | }
620 | })"
621 | }
622 |
623 | /**
624 | * get the parent of a commit
625 | * @param commitHash the hash of the commit
626 | * @return the hash of the parent commit
627 | */
628 | fun getParent(commitHash: String): String {
629 | val content = getContent(commitHash)
630 | return if (content.contains("parent")) {
631 | content.split("\n")[1].split(" ")[1]
632 | } else {
633 | ""
634 | }
635 | }
636 |
637 |
638 | /**
639 | * this is a helper function for the actual log command
640 | * @return the list of branches
641 | */
642 | fun getBranches(): MutableMap {
643 | val refs = mutableMapOf()
644 | val branches = File("${System.getProperty("user.dir")}/.kit/refs/heads").walk().filter { it.isFile }.toList()
645 | for (branch in branches) {
646 | refs[branch.relativePath("${System.getProperty("user.dir")}/.kit/refs/heads")] = branch.readText()
647 | }
648 | return refs
649 | }
650 |
651 | fun getTags(): Map {
652 | val tags = mutableMapOf()
653 | val tagDir = File("${System.getProperty("user.dir")}/.kit/refs/tags")
654 | if (tagDir.exists()) {
655 | tagDir.walk().forEach {
656 | if (it.isFile) {
657 | val tagHash = it.readText()
658 | val tagName = it.relativePath("${System.getProperty("user.dir")}/.kit/refs/tags")
659 | val tagContent = getContent(tagHash)
660 | val tagCommit = tagContent.split("\n")[0].split(" ")[1]
661 | tags["tag: $tagName"] = tagCommit
662 | }
663 | }
664 | }
665 | return tags
666 | }
667 |
668 |
669 | /**
670 | * get the commit hash of the HEAD
671 | * @return the commit hash of the HEAD
672 | */
673 | fun getHEADHash(): String {
674 | val workingDirectory = System.getProperty("user.dir")
675 | val headFile = File("${workingDirectory}/.kit/HEAD")
676 | val head = headFile.readText()
677 | return if (head.startsWith("ref:")) {
678 | val ref = head.substringAfter("ref:").trim()
679 | val refFile = File("${workingDirectory}/.kit/$ref")
680 | if (!refFile.exists()) {
681 | throw Exception("fatal: Failed to resolve 'HEAD' as a valid ref.")
682 | }
683 | refFile.readText()
684 | } else {
685 | head
686 | }
687 | }
688 | /****************************************/
--------------------------------------------------------------------------------
/src/test/kotlin/kit/porcelain/PorcelainKtTest.kt:
--------------------------------------------------------------------------------
1 | package kit.porcelain
2 |
3 | import org.junit.jupiter.api.AfterEach
4 | import org.junit.jupiter.api.Assertions.assertEquals
5 | import org.junit.jupiter.api.Assertions.assertTrue
6 | import org.junit.jupiter.api.BeforeEach
7 | import org.junit.jupiter.api.Test
8 | import org.junit.jupiter.api.assertThrows
9 | import kit.plumbing.GitIndex
10 | import java.io.File
11 | import java.nio.file.Path
12 | import kotlin.io.path.absolutePathString
13 | import kotlin.io.path.pathString
14 |
15 | class PorcelainKtTest {
16 |
17 | @BeforeEach
18 | @AfterEach
19 | fun cleanUp() {
20 | // clean up
21 | val workingDirectory = File("src/test/resources/workingDirectory")
22 | workingDirectory.deleteRecursively()
23 | }
24 |
25 | @Test
26 | fun `initialize a repository`() {
27 | // create working directory
28 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
29 | workingDirectory.toFile().mkdir()
30 | // set the working directory
31 | System.setProperty("user.dir", workingDirectory.toString())
32 | assertEquals(
33 | "Initialized empty Kit repository in ${File("${workingDirectory}/.kit").absolutePath}",
34 | init(workingDirectory)
35 | )
36 | assert(File("$workingDirectory/.kit").exists())
37 | assert(File("$workingDirectory/.kit/objects").exists())
38 | assert(File("$workingDirectory/.kit/refs").exists())
39 | assert(File("$workingDirectory/.kit/refs/heads").exists())
40 | assert(File("$workingDirectory/.kit/HEAD").exists())
41 | assert(File("$workingDirectory/.kit/HEAD").readText() == "ref: refs/heads/master")
42 | assert(File("$workingDirectory/.kit/config").exists())
43 | }
44 |
45 | @Test
46 | fun `initialize a repository with name`() {
47 | // create working directory
48 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
49 | workingDirectory.toFile().mkdir()
50 | // set the working directory
51 | System.setProperty("user.dir", workingDirectory.toString())
52 | val repositoryName = "demo"
53 | assertEquals(
54 | "Initialized empty Kit repository in ${File("${workingDirectory}/$repositoryName/.kit").absolutePath}",
55 | init(Path.of(workingDirectory.pathString, repositoryName).toAbsolutePath())
56 | )
57 | assert(File("$workingDirectory/$repositoryName/.kit").exists())
58 | assert(File("$workingDirectory/$repositoryName/.kit/objects").exists())
59 | assert(File("$workingDirectory/$repositoryName/.kit/refs").exists())
60 | assert(File("$workingDirectory/$repositoryName/.kit/refs/heads").exists())
61 | assert(File("$workingDirectory/$repositoryName/.kit/HEAD").exists())
62 | assert(File("$workingDirectory/$repositoryName/.kit/HEAD").readText() == "ref: refs/heads/master")
63 | assert(File("$workingDirectory/$repositoryName/.kit/config").exists())
64 | }
65 |
66 | @Test
67 | fun `initialize a repository with name in an existing repository`() {
68 | // create working directory
69 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
70 | workingDirectory.toFile().mkdir()
71 | // set the working directory
72 | System.setProperty("user.dir", workingDirectory.toString())
73 | val repositoryName = "demo"
74 | assertEquals(
75 | "Initialized empty Kit repository in ${File("${workingDirectory}/$repositoryName/.kit").absolutePath}",
76 | init(Path.of(workingDirectory.pathString,repositoryName).toAbsolutePath())
77 | )
78 | assertEquals(
79 | "Reinitialized existing Kit repository in ${File("${workingDirectory}/$repositoryName/.kit").absolutePath}",
80 | init(Path.of(workingDirectory.pathString,repositoryName).toAbsolutePath())
81 | )
82 | }
83 |
84 | @Test
85 | fun `add non-existent file`() {
86 | // create working directory
87 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
88 | workingDirectory.toFile().mkdir()
89 | // set the working directory
90 | System.setProperty("user.dir", workingDirectory.toString())
91 | init()
92 | val filePath = "src/test/resources/workingDirectory/non-existent-file"
93 | val exception = assertThrows {
94 | add(filePath)
95 | }
96 | assertEquals("fatal: pathspec '$filePath' did not match any files", exception.message)
97 | }
98 |
99 | @Test
100 | fun `add a file outside the repo`() {
101 | // create working directory
102 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
103 | workingDirectory.toFile().mkdir()
104 | // set the working directory
105 | System.setProperty("user.dir", workingDirectory.toString())
106 | init()
107 | val filePath = "${workingDirectory.parent}/test.txt"
108 | File(filePath).writeText("test text")
109 | val exception = assertThrows {
110 | add(filePath)
111 | }
112 | assertEquals("fatal: pathspec '$filePath' is outside repository", exception.message)
113 | File(filePath).delete()
114 | }
115 |
116 | @Test
117 | fun `add file inside the kit directory`() {
118 | // create working directory
119 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
120 | workingDirectory.toFile().mkdir()
121 | // set the working directory
122 | System.setProperty("user.dir", workingDirectory.toString())
123 | init()
124 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
125 | val filePath = "${workingDirectory.absolutePathString()}/.kit/test.txt"
126 | File(filePath).writeText("test text")
127 |
128 | assertEquals(0, GitIndex.getEntryCount())
129 | }
130 |
131 | @Test
132 | fun `add a file`() {
133 | // create working directory
134 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
135 | workingDirectory.toFile().mkdir()
136 | // set the working directory
137 | System.setProperty("user.dir", workingDirectory.toString())
138 | init()
139 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
140 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
141 | File(filePath).writeText("test text")
142 |
143 | assertEquals(0, GitIndex.getEntryCount())
144 | add(filePath)
145 | assertEquals(1, GitIndex.getEntryCount())
146 | }
147 |
148 | @Test
149 | fun `remove a file that's not in the index`() {
150 | // create working directory
151 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
152 | workingDirectory.toFile().mkdir()
153 | // set the working directory
154 | System.setProperty("user.dir", workingDirectory.toString())
155 | init()
156 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
157 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
158 | File(filePath).writeText("test text")
159 | val exception = assertThrows {
160 | unstage(filePath)
161 | }
162 | assertEquals("fatal: pathspec '$filePath' did not match any files", exception.message)
163 | }
164 |
165 | @Test
166 | fun `remove a file that's outside the repo`() {
167 | // create working directory
168 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
169 | workingDirectory.toFile().mkdir()
170 | // set the working directory
171 | System.setProperty("user.dir", workingDirectory.toString())
172 | init()
173 | val filePath = "${workingDirectory.parent}/test.txt"
174 | File(filePath).writeText("test text")
175 | val exception = assertThrows {
176 | unstage(filePath)
177 | }
178 | assertEquals("fatal: pathspec '$filePath' is outside repository", exception.message)
179 | File(filePath).delete()
180 | }
181 |
182 | @Test
183 | fun `remove a file inside the kit directory`() {
184 | // create working directory
185 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
186 | workingDirectory.toFile().mkdir()
187 | // set the working directory
188 | System.setProperty("user.dir", workingDirectory.toString())
189 | init()
190 | val filePath = "${workingDirectory.absolutePathString()}/.kit/test.txt"
191 | File(filePath).writeText("test text")
192 | val exception = assertThrows {
193 | unstage(filePath)
194 | }
195 | assertEquals("fatal: pathspec '$filePath' did not match any files", exception.message)
196 | File(filePath).delete()
197 | }
198 |
199 | @Test
200 | fun `remove a file from index`() {
201 | // create working directory
202 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
203 | workingDirectory.toFile().mkdir()
204 | // set the working directory
205 | System.setProperty("user.dir", workingDirectory.toString())
206 | init()
207 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
208 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
209 | File(filePath).writeText("test text")
210 | add(filePath)
211 | assertEquals(1, GitIndex.getEntryCount())
212 | unstage(filePath)
213 | assertEquals(0, GitIndex.getEntryCount())
214 | }
215 |
216 | @Test
217 | fun `status without a commit`() {
218 | // create working directory
219 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
220 | workingDirectory.toFile().mkdir()
221 | // set the working directory
222 | System.setProperty("user.dir", workingDirectory.toString())
223 | init()
224 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
225 | // array of files
226 | val files = arrayOf(
227 | "test.txt",
228 | "test2.txt",
229 | "test3.txt",
230 | "test4.txt"
231 | ).map { File("${workingDirectory.absolutePathString()}/$it") }.onEach { it.writeText("test text") }
232 | add(files[0].path)
233 | File("$workingDirectory/test").mkdir()
234 | files[0].renameTo(File("$workingDirectory/test/${files[0].name}"))
235 | add(files[1].path)
236 | status()
237 | }
238 |
239 | @Test
240 | fun `status with a head on a branch`() {
241 | // create working directory
242 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
243 | workingDirectory.toFile().mkdir()
244 | // set the working directory
245 | System.setProperty("user.dir", workingDirectory.toString())
246 | init()
247 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
248 | // array of files
249 | val files = arrayOf(
250 | "test.txt",
251 | "test2.txt",
252 | "test3.txt",
253 | "test4.txt"
254 | ).map { File("${workingDirectory.absolutePathString()}/$it") }.onEach { it.writeText("test text") }
255 | add(files[0].path)
256 | add(files[1].path)
257 | add(files[2].path)
258 | commit("test commit")
259 | // modify one of the files
260 | files[0].setExecutable(true)
261 | // delete one of the files
262 | files[1].delete()
263 | add(files[3].path)
264 | add(files[0].path)
265 | unstage(files[1].path)
266 | status()
267 | }
268 |
269 | @Test
270 | fun `status with a head on a detached state`() {
271 | // create working directory
272 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
273 | workingDirectory.toFile().mkdir()
274 | // set the working directory
275 | System.setProperty("user.dir", workingDirectory.toString())
276 | init()
277 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
278 | // array of files
279 | val files = arrayOf(
280 | "test.txt",
281 | "test2.txt",
282 | "test3.txt",
283 | "test4.txt"
284 | ).map { File("${workingDirectory.absolutePathString()}/$it") }.onEach { it.writeText("test text") }
285 | add(files[0].path)
286 | add(files[1].path)
287 | add(files[2].path)
288 | val hash = commit("test commit")
289 | checkout(hash)
290 | // modify one of the files
291 | files[0].setExecutable(true)
292 | // delete one of the files
293 | files[1].delete()
294 | add(files[3].path)
295 | add(files[0].path)
296 | unstage(files[1].path)
297 | status()
298 | }
299 |
300 | @Test
301 | fun `first commit on a clean tree`() {
302 | // create working directory
303 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
304 | workingDirectory.toFile().mkdir()
305 | // set the working directory
306 | System.setProperty("user.dir", workingDirectory.toString())
307 | init()
308 | File("${workingDirectory.absolutePathString()}/test.txt").writeText("test text")
309 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
310 | val exception = assertThrows {
311 | commit("test commit")
312 | }
313 | assertEquals("nothing to commit, working tree clean", exception.message)
314 | }
315 |
316 | @Test
317 | fun `second commit on a clean tree`() {
318 | // create working directory
319 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
320 | workingDirectory.toFile().mkdir()
321 | // set the working directory
322 | System.setProperty("user.dir", workingDirectory.toString())
323 | init()
324 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
325 | // create a file
326 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
327 | File(filePath).writeText("test text")
328 | add(filePath)
329 | commit("test commit")
330 | val exception = assertThrows {
331 | commit("test commit")
332 | }
333 | assertEquals("nothing to commit, working tree clean", exception.message)
334 | }
335 |
336 |
337 | @Test
338 | fun `commit on master`() {
339 | // create working directory
340 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
341 | workingDirectory.toFile().mkdir()
342 | // set the working directory
343 | System.setProperty("user.dir", workingDirectory.toString())
344 | init()
345 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
346 | // create a file
347 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
348 | File(filePath).writeText("test text")
349 | add(filePath)
350 | val commitHash = commit("test commit")
351 | // this should create a file in the .kit/refs/heads named master
352 | assertTrue(File("$workingDirectory/.kit/refs/heads/master").exists())
353 | assertEquals(commitHash, File("$workingDirectory/.kit/refs/heads/master").readText())
354 | }
355 |
356 | @Test
357 | fun `commit twice`() {
358 | // create working directory
359 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
360 | workingDirectory.toFile().mkdir()
361 | // set the working directory
362 | System.setProperty("user.dir", workingDirectory.toString())
363 | init()
364 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
365 | // create a file
366 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
367 | File(filePath).writeText("test text")
368 | add(filePath)
369 | commit("test commit")
370 | File(filePath).writeText("test text 2")
371 | add(filePath)
372 | val commitHash2 = commit("test commit 2")
373 | assertEquals(commitHash2, File("$workingDirectory/.kit/refs/heads/master").readText())
374 | }
375 |
376 | @Test
377 | fun `commit when HEAD is at Detached state`() {
378 | // create working directory
379 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
380 | workingDirectory.toFile().mkdir()
381 | // set the working directory
382 | System.setProperty("user.dir", workingDirectory.toString())
383 | init()
384 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
385 | // create a file
386 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
387 | File(filePath).writeText("test text")
388 | add(filePath)
389 | val commitHash = commit("test commit")
390 | // checkout to the commit
391 | File("$workingDirectory/.kit/HEAD").writeText(commitHash)
392 | File(filePath).writeText("test text 2")
393 | add(filePath)
394 | val commitHash2 = commit("test commit 2")
395 |
396 | assertEquals(commitHash2, File("$workingDirectory/.kit/HEAD").readText())
397 | }
398 |
399 | @Test
400 | fun `checkout non-existent branch`() {
401 | // create working directory
402 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
403 | workingDirectory.toFile().mkdir()
404 | // set the working directory
405 | System.setProperty("user.dir", workingDirectory.toString())
406 | init()
407 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
408 | // create a file
409 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
410 | File(filePath).writeText("test text")
411 | add(filePath)
412 | commit("test commit")
413 | val exception = assertThrows {
414 | checkout("test")
415 | }
416 | assertEquals("error: pathspec 'test' did not match any file(s) known to kit", exception.message)
417 | }
418 |
419 | @Test
420 | fun `checkout a commit`() {
421 | // create working directory
422 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
423 | workingDirectory.toFile().mkdir()
424 | // set the working directory
425 | System.setProperty("user.dir", workingDirectory.toString())
426 | init()
427 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
428 | // create a file
429 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
430 | File(filePath).writeText("test text")
431 | add(filePath)
432 | val commitHash = commit("test commit")
433 | File(filePath).writeText("test text 2")
434 | add(filePath)
435 | commit("test commit 2")
436 | checkout(commitHash)
437 | assertEquals(commitHash, File("$workingDirectory/.kit/HEAD").readText())
438 | // the content of the file should be the same as the checkout commit
439 | assertEquals("test text", File(filePath).readText())
440 | }
441 |
442 | @Test
443 | fun `checkout a tag`() {
444 | // create working directory
445 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
446 | workingDirectory.toFile().mkdir()
447 | // set the working directory
448 | System.setProperty("user.dir", workingDirectory.toString())
449 | init()
450 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
451 | // create a file
452 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
453 | File(filePath).writeText("test text")
454 | add(filePath)
455 | val commitHash = commit("test commit")
456 | tag("test", "test tag")
457 | File(filePath).writeText("test text 2")
458 | add(filePath)
459 | commit("test commit 2")
460 | checkout("test")
461 | assertEquals(commitHash, File("$workingDirectory/.kit/HEAD").readText())
462 | // the content of the file should be the same as the checkout commit
463 | assertEquals("test text", File(filePath).readText())
464 | log()
465 | }
466 |
467 | @Test
468 | fun `checkout a branch`() {
469 | // create working directory
470 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
471 | workingDirectory.toFile().mkdir()
472 | // set the working directory
473 | System.setProperty("user.dir", workingDirectory.toString())
474 | init()
475 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
476 | // create a file
477 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
478 | File(filePath).writeText("test text")
479 | add(filePath)
480 | commit("test commit")
481 | File(filePath).writeText("test text 2")
482 | add(filePath)
483 | val commitHash = commit("test commit 2")
484 | checkout("master")
485 | assertEquals(commitHash, File("$workingDirectory/.kit/refs/heads/master").readText())
486 | }
487 |
488 | @Test
489 | fun `checkout an executable file`() {
490 | // create working directory
491 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
492 | workingDirectory.toFile().mkdir()
493 | // set the working directory
494 | System.setProperty("user.dir", workingDirectory.toString())
495 | init()
496 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
497 | // create a file
498 | val filePath = "${workingDirectory.absolutePathString()}/test.sh"
499 | File(filePath).writeText("echo test")
500 | File(filePath).setExecutable(true)
501 | add(filePath)
502 | val commitHash = commit("test commit")
503 | File(filePath).setExecutable(false)
504 | add(filePath)
505 | commit("test commit 2")
506 | checkout(commitHash)
507 | assertEquals(commitHash, File("$workingDirectory/.kit/HEAD").readText())
508 | // the content of the file should be the same as the checkout commit
509 | assertEquals("echo test", File(filePath).readText())
510 | assertTrue(File(filePath).canExecute())
511 | }
512 |
513 | @Test
514 | fun `checkout a directory`() {
515 | // create working directory
516 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
517 | workingDirectory.toFile().mkdir()
518 | // set the working directory
519 | System.setProperty("user.dir", workingDirectory.toString())
520 | init()
521 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
522 | // create a file
523 | val filePath = "${workingDirectory.absolutePathString()}/test/test.txt"
524 | File(filePath).parentFile.mkdirs()
525 | File(filePath).writeText("test text")
526 | add(filePath)
527 | val commitHash = commit("test commit")
528 | File(filePath).writeText("test text 2")
529 | add(filePath)
530 | commit("test commit 2")
531 | checkout(commitHash)
532 | assertEquals(commitHash, File("$workingDirectory/.kit/HEAD").readText())
533 | // the content of the file should be the same as the checkout commit
534 | assertEquals("test text", File(filePath).readText())
535 | }
536 |
537 | @Test
538 | fun `make an existent branch`() {
539 | // create working directory
540 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
541 | workingDirectory.toFile().mkdir()
542 | // set the working directory
543 | System.setProperty("user.dir", workingDirectory.toString())
544 | init()
545 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
546 | // create a file
547 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
548 | File(filePath).writeText("test text")
549 | add(filePath)
550 | commit("test commit")
551 | val exception = assertThrows {
552 | branch("master")
553 | }
554 | assertEquals("fatal: A branch named 'master' already exists.", exception.message)
555 | }
556 |
557 | @Test
558 | fun `make a new branch without directories`() {
559 | // create working directory
560 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
561 | workingDirectory.toFile().mkdir()
562 | // set the working directory
563 | System.setProperty("user.dir", workingDirectory.toString())
564 | init()
565 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
566 | // create a file
567 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
568 | File(filePath).writeText("test text")
569 | add(filePath)
570 | val hash = commit("test commit")
571 | branch("test")
572 | assertEquals(hash, File("$workingDirectory/.kit/refs/heads/test").readText())
573 |
574 | }
575 |
576 | @Test
577 | fun `make a new branch with directories`() {
578 | // create working directory
579 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
580 | workingDirectory.toFile().mkdir()
581 | // set the working directory
582 | System.setProperty("user.dir", workingDirectory.toString())
583 | init()
584 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
585 | // create a file
586 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
587 | File(filePath).writeText("test text")
588 | add(filePath)
589 | val hash = commit("test commit")
590 | branch("feature/test")
591 | assertEquals(hash, File("$workingDirectory/.kit/refs/heads/feature/test").readText())
592 |
593 | }
594 |
595 | @Test
596 | fun `make a new branch with a ref`() {
597 | // create working directory
598 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
599 | workingDirectory.toFile().mkdir()
600 | // set the working directory
601 | System.setProperty("user.dir", workingDirectory.toString())
602 | init()
603 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
604 | // create a file
605 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
606 | File(filePath).writeText("test text")
607 | add(filePath)
608 | val hash = commit("test commit")
609 | branch("test", hash)
610 | assertEquals(hash, File("$workingDirectory/.kit/refs/heads/test").readText())
611 |
612 | }
613 |
614 | @Test
615 | fun `make a new branch without a ref`() {
616 | // create working directory
617 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
618 | workingDirectory.toFile().mkdir()
619 | // set the working directory
620 | System.setProperty("user.dir", workingDirectory.toString())
621 | init()
622 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
623 | // create a file
624 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
625 | File(filePath).writeText("test text")
626 | add(filePath)
627 | val hash = commit("test commit")
628 | checkout(hash)
629 | branch("test")
630 | assertEquals(hash, File("$workingDirectory/.kit/refs/heads/test").readText())
631 |
632 | }
633 |
634 | @Test
635 | fun `make a new branch without a commit`() {
636 | // create working directory
637 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
638 | workingDirectory.toFile().mkdir()
639 | // set the working directory
640 | System.setProperty("user.dir", workingDirectory.toString())
641 | init()
642 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
643 | val exception = assertThrows {
644 | branch("test")
645 | }
646 | assertEquals("fatal: Not a valid object name: 'master'.", exception.message)
647 | }
648 |
649 |
650 | @Test
651 | fun `set config`() {
652 | // create working directory
653 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
654 | workingDirectory.toFile().mkdir()
655 | // set the working directory
656 | System.setProperty("user.dir", workingDirectory.toString())
657 | init()
658 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
659 | Config.set("user.name", "test")
660 | Config.set("user.email", "test@gmail.com")
661 |
662 | assertEquals("test", Config.get("user.name"))
663 | assertEquals("test@gmail.com", Config.get("user.email"))
664 | }
665 |
666 | @Test
667 | fun `get history one commit`() {
668 | // create working directory
669 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
670 | workingDirectory.toFile().mkdir()
671 | // set the working directory
672 | System.setProperty("user.dir", workingDirectory.toString())
673 | init()
674 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
675 | // create a file
676 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
677 | File(filePath).writeText("test text")
678 | add(filePath)
679 | val hash = commit("test commit")
680 | val history = getHistory()
681 | assertEquals(hash, history[0])
682 | log()
683 | }
684 |
685 |
686 | @Test
687 | fun `get history multiple commit`() {
688 | // create working directory
689 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
690 | workingDirectory.toFile().mkdir()
691 | // set the working directory
692 | System.setProperty("user.dir", workingDirectory.toString())
693 | init()
694 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
695 | val commits = mutableListOf()
696 | for (i in 1..5) {
697 | // create a file
698 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
699 | File(filePath).writeText("test text $i")
700 | add(filePath)
701 | val hash = commit("test commit")
702 | commits.add(hash)
703 | if (i == 3) {
704 | branch("demo")
705 | }
706 | if (i == 4) {
707 | tag("v1.0.0", "Version 1.0.0")
708 | }
709 | if (i == 5) {
710 | tag("v1.0.1", "Version 1.0.1")
711 | }
712 | }
713 | val history = getHistory()
714 | for (i in 0..4) {
715 | assertEquals(commits[4 - i], history[i])
716 | }
717 | log()
718 | }
719 |
720 | @Test
721 | fun `get history multiple commit with Detached HEAD`() {
722 | // create working directory
723 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
724 | workingDirectory.toFile().mkdir()
725 | // set the working directory
726 | System.setProperty("user.dir", workingDirectory.toString())
727 | init()
728 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
729 | val commits = mutableListOf()
730 | for (i in 1..5) {
731 | // create a file
732 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
733 | File(filePath).writeText("test text $i")
734 | add(filePath)
735 | val hash = commit("test commit")
736 | commits.add(hash)
737 | if (i == 3) {
738 | branch("demo")
739 | }
740 | if (i == 5) {
741 | checkout(hash)
742 | }
743 | }
744 | val history = getHistory()
745 | for (i in 0..4) {
746 | assertEquals(commits[4 - i], history[i])
747 | }
748 | log()
749 | }
750 |
751 | @Test
752 | fun `get empty history`() {
753 | // create working directory
754 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
755 | workingDirectory.toFile().mkdir()
756 | // set the working directory
757 | System.setProperty("user.dir", workingDirectory.toString())
758 | init()
759 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
760 | val history = getHistory()
761 | assertEquals(0, history.size)
762 | }
763 |
764 | @Test
765 | fun `create a tag`() {
766 | // create working directory
767 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
768 | workingDirectory.toFile().mkdir()
769 | // set the working directory
770 | System.setProperty("user.dir", workingDirectory.toString())
771 | init()
772 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
773 | // create a file
774 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
775 | File(filePath).writeText("test text")
776 | add(filePath)
777 | commit("test commit")
778 | val hash = tag("test", "test tag")
779 | assertEquals(hash, File("$workingDirectory/.kit/refs/tags/test").readText())
780 | }
781 |
782 | @Test
783 | fun `create an existing tag`() {
784 | // create working directory
785 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
786 | workingDirectory.toFile().mkdir()
787 | // set the working directory
788 | System.setProperty("user.dir", workingDirectory.toString())
789 | init()
790 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
791 | // create a file
792 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
793 | File(filePath).writeText("test text")
794 | add(filePath)
795 | commit("test commit")
796 | tag("test", "test tag")
797 | val exception = assertThrows {
798 | tag("test", "test tag")
799 | }
800 | assertEquals("fatal: tag 'test' already exists", exception.message)
801 | }
802 |
803 | @Test
804 | fun `create a tag with directories`() {
805 | // create working directory
806 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
807 | workingDirectory.toFile().mkdir()
808 | // set the working directory
809 | System.setProperty("user.dir", workingDirectory.toString())
810 | init()
811 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
812 | // create a file
813 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
814 | File(filePath).writeText("test text")
815 | add(filePath)
816 | commit("test commit")
817 | val hash = tag("test/test", "test tag")
818 | assertEquals(hash, File("$workingDirectory/.kit/refs/tags/test/test").readText())
819 | }
820 |
821 | @Test
822 | fun `create tag with HEAD pointing to a commit`() {
823 | // create working directory
824 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
825 | workingDirectory.toFile().mkdir()
826 | // set the working directory
827 | System.setProperty("user.dir", workingDirectory.toString())
828 | init()
829 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
830 | // create a file
831 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
832 | File(filePath).writeText("test text")
833 | add(filePath)
834 | checkout(commit("test commit"))
835 | val hash = tag("test", "test tag")
836 | assertEquals(hash, File("$workingDirectory/.kit/refs/tags/test").readText())
837 | }
838 |
839 | @Test
840 | fun `create a tag on a commit other than HEAD`() {
841 | // create working directory
842 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
843 | workingDirectory.toFile().mkdir()
844 | // set the working directory
845 | System.setProperty("user.dir", workingDirectory.toString())
846 | init()
847 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
848 | val commits = mutableListOf()
849 | for (i in 1..5) {
850 | // create a file
851 | val filePath = "${workingDirectory.absolutePathString()}/test.txt"
852 | File(filePath).writeText("test text $i")
853 | add(filePath)
854 | val hash = commit("test commit")
855 | commits.add(hash)
856 | }
857 | val hash = tag("test", "test tag", commits[2])
858 | assertEquals(hash, File("$workingDirectory/.kit/refs/tags/test").readText())
859 | }
860 |
861 | @Test
862 | fun `create a tag without any commit`() {
863 | // create working directory
864 | val workingDirectory = Path.of("src/test/resources/workingDirectory").toAbsolutePath()
865 | workingDirectory.toFile().mkdir()
866 | // set the working directory
867 | System.setProperty("user.dir", workingDirectory.toString())
868 | init()
869 | if (GitIndex.getEntryCount() != 0) GitIndex.clearIndex()
870 | val exception = assertThrows {
871 | tag("test", "test tag")
872 | }
873 | assertEquals("fatal: Failed to resolve 'HEAD' as a valid ref.", exception.message)
874 | }
875 |
876 | }
877 |
--------------------------------------------------------------------------------
/src/test/kotlin/kit/plumbing/PlumbingKtTest.kt:
--------------------------------------------------------------------------------
1 | package kit.plumbing
2 |
3 | import org.junit.jupiter.api.AfterEach
4 | import org.junit.jupiter.api.Test
5 |
6 | import org.junit.jupiter.api.Assertions.*
7 | import org.junit.jupiter.api.BeforeEach
8 | import org.junit.jupiter.api.assertThrows
9 | import kit.plumbing.GitIndex.clearIndex
10 | import kit.plumbing.GitIndex.readIndex
11 | import kit.utils.*
12 | import java.io.File
13 | import kotlin.random.Random
14 | import kotlin.test.fail
15 |
16 | class PlumbingKtTest {
17 |
18 | @BeforeEach
19 | @AfterEach
20 | fun cleanUp() {
21 | // clean up
22 | val workingDirectory = File("src/test/resources/workingDirectory")
23 | workingDirectory.deleteRecursively()
24 | }
25 |
26 | /**
27 | * Testing whether the sha1 function returns the correct hash
28 | */
29 | @Test
30 | fun sha1() {
31 | val bytes = "Hello World".toByteArray()
32 | val hash = sha1(bytes)
33 | val command = "echo -n \"Hello World\" | shasum -a 1"
34 | val process = ProcessBuilder("/bin/bash", "-c", command)
35 | .redirectOutput(ProcessBuilder.Redirect.PIPE)
36 | .redirectError(ProcessBuilder.Redirect.PIPE)
37 | .start()
38 | val cmdHash = process.inputStream.bufferedReader().readText().substring(0, 40)
39 | assertEquals(
40 | /* expected = */ cmdHash,
41 | /* actual = */ hash,
42 | /* message = */ "The hashes should be equal"
43 | )
44 | }
45 |
46 | /**
47 | * Testing whether the hashObject function returns the correct hash
48 | */
49 | @Test
50 | fun `hashObject file exists`() {
51 | // create a file with the content "Hello World"
52 | val file = File("src/test/resources/test.txt")
53 | file.createNewFile()
54 | file.writeText("Hello World")
55 | // hash the file
56 | val kHash = hashObject(file.path)
57 | // hash the file using the command line
58 | val cmd = "git hash-object ${file.absolutePath}"
59 | val cmdHash = cmd.runCommand()
60 | file.delete()
61 | // compare the hashes
62 | assertEquals(
63 | /* expected = */ cmdHash,
64 | /* actual = */ kHash,
65 | /* message = */ "The hashes should be equal"
66 | )
67 | }
68 |
69 | /**
70 | * Testing whether the hashObject function throws an exception when the file does not exist
71 | */
72 | @Test
73 | fun `hashObject file does not exist`() {
74 | // create a file with the content "Hello World"
75 | val file = File("src/test/resources/test.txt")
76 | // hash the file
77 | try {
78 | hashObject(file.path)
79 | fail("The hashObject function should throw an exception")
80 | } catch (e: Exception) {
81 | assertEquals(
82 | /* expected = */ "File does not exist",
83 | /* actual = */ e.message,
84 | /* message = */ "The exception message should be 'File does not exist'"
85 | )
86 | }
87 | }
88 |
89 | /**
90 | * Testing whether the hashObject function throws an exception when the file is a directory
91 | */
92 | @Test
93 | fun `hashObject file and write to object database`() {
94 | // create working directory
95 | val workingDirectory = File("src/test/resources/workingDirectory")
96 | workingDirectory.mkdir()
97 | // set the working directory
98 | System.setProperty("user.dir", workingDirectory.path)
99 | // create object database
100 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
101 | objectDatabase.mkdirs()
102 | // create a file with the content "Hello World"
103 | val file = File("src/test/resources/workingDirectory/test.txt")
104 | file.createNewFile()
105 | file.writeText("Hello World")
106 | // hash the file
107 | val kHash = hashObject(file.path, write = true)
108 | // check if the file was written to the object database
109 | val objectFile =
110 | File("src/test/resources/workingDirectory/.kit/objects/${kHash.substring(0, 2)}/${kHash.substring(2)}")
111 | assertTrue(objectFile.exists())
112 | }
113 |
114 | /**
115 | * Testing whether the hashObject function throws an exception when the file is a directory
116 | */
117 | @Test
118 | fun `hashObject file and write but object database doesn't exist`() {
119 | // create working directory
120 | val workingDirectory = File("src/test/resources/workingDirectory")
121 | workingDirectory.mkdir()
122 | // set the working directory
123 | System.setProperty("user.dir", workingDirectory.path)
124 | // create a file with the content "Hello World"
125 | val file = File("src/test/resources/workingDirectory/test.txt")
126 | file.createNewFile()
127 | file.writeText("Hello World")
128 | // hash the file
129 | try {
130 | hashObject(file.path, write = true)
131 | } catch (e: Exception) {
132 | assertEquals(
133 | /* expected = */ "The repository doesn't exist",
134 | /* actual = */ e.message,
135 | /* message = */ "The exception message should be 'The repository doesn't exist'"
136 | )
137 | }
138 | }
139 |
140 | /**
141 | * Testing whether it match git functionality
142 | */
143 | @Test
144 | fun `hashObject file and write to object database and match git functionality`() {
145 | // create working directory
146 | val workingDirectory = File("src/test/resources/workingDirectory")
147 | workingDirectory.mkdir()
148 | // set the working directory
149 | System.setProperty("user.dir", workingDirectory.path)
150 | // create object database
151 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
152 | objectDatabase.mkdirs()
153 | // create a file with the content "Hello World"
154 | val file = File("src/test/resources/workingDirectory/test.txt")
155 | file.createNewFile()
156 | file.writeText("Hello World")
157 | // hash the file
158 | val kHash = hashObject(file.path, write = true)
159 | // hash the file using the command line
160 | val gHash = "git hash-object -w ${file.absolutePath}".runCommand()
161 | // NOTE: this affect the project repo due to the limitation that kotlin can't change the working directory
162 |
163 | // compare the content of the files
164 | val kObjectFile =
165 | File("src/test/resources/workingDirectory/.kit/objects/${kHash.substring(0, 2)}/${kHash.substring(2)}")
166 | val gObjectFile =
167 | File(".git/objects/${gHash.substring(0, 2)}/${gHash.substring(2)}")
168 | assertEquals(
169 | /* expected = */ Zlib.inflate(gObjectFile.readBytes()).toString(Charsets.UTF_8),
170 | /* actual = */ Zlib.inflate(kObjectFile.readBytes()).toString(Charsets.UTF_8),
171 | /* message = */ "The content of the files should be equal"
172 | )
173 |
174 | // clean up
175 | gObjectFile.delete()
176 | }
177 |
178 |
179 | /**
180 | * Testing whether the updateIndex function adds the file to the index
181 | */
182 | @Test
183 | fun `updateIndex add file`() {
184 | val workingDirectory = File("src/test/resources/workingDirectory")
185 | workingDirectory.mkdir()
186 | // set the working directory
187 | System.setProperty("user.dir", workingDirectory.path)
188 | // create object database
189 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
190 | objectDatabase.mkdirs()
191 | // create refs and refs/heads
192 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
193 | refs.mkdirs()
194 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
195 | heads.mkdirs()
196 | // create HEAD file
197 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
198 | head.createNewFile()
199 | head.writeText("ref: refs/heads/master")
200 | // create a file with the content "Hello World"
201 | val file = File("src/test/resources/workingDirectory/test.txt")
202 | file.createNewFile()
203 | file.writeText("Hello World")
204 | // update the index
205 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
206 |
207 | // check if the file was written to the index
208 | val indexFile = File("src/test/resources/workingDirectory/.kit/index")
209 | assertTrue(indexFile.exists())
210 | assert(GitIndex.getEntryCount() == 1)
211 |
212 | // clean up
213 | clearIndex()
214 | }
215 |
216 | /**
217 | * Testing whether the index update file that was already added after change
218 | */
219 | @Test
220 | fun `updateIndex add file that was already added after change`() {
221 | val workingDirectory = File("src/test/resources/workingDirectory")
222 | workingDirectory.mkdir()
223 | // set the working directory
224 | System.setProperty("user.dir", workingDirectory.path)
225 | // create object database
226 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
227 | objectDatabase.mkdirs()
228 | // create refs and refs/heads
229 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
230 | refs.mkdirs()
231 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
232 | heads.mkdirs()
233 | // create HEAD file
234 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
235 | head.createNewFile()
236 | head.writeText("ref: refs/heads/master")
237 | // create a file with the content "Hello World"
238 | val file = File("src/test/resources/workingDirectory/test.txt")
239 | file.createNewFile()
240 | file.writeText("Hello World")
241 | // update the index
242 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
243 |
244 | // change file content
245 | file.writeText("Hello World (2)")
246 | // update the index
247 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
248 |
249 |
250 | // check if the file was written to the index
251 | val indexFile = File("src/test/resources/workingDirectory/.kit/index")
252 | assertTrue(indexFile.exists())
253 | assert(GitIndex.getEntryCount() == 1)
254 | }
255 |
256 | /**
257 | * Testing whether the updateIndex function removes the file from the index
258 | */
259 | @Test
260 | fun `updateIndex remove file`() {
261 | val workingDirectory = File("src/test/resources/workingDirectory")
262 | workingDirectory.mkdir()
263 | // set the working directory
264 | System.setProperty("user.dir", workingDirectory.path)
265 | // create object database
266 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
267 | objectDatabase.mkdirs()
268 | // create refs and refs/heads
269 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
270 | refs.mkdirs()
271 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
272 | heads.mkdirs()
273 | // create HEAD file
274 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
275 | head.createNewFile()
276 | head.writeText("ref: refs/heads/master")
277 | // create a file with the content "Hello World"
278 | val file = File("src/test/resources/workingDirectory/test.txt")
279 | file.createNewFile()
280 | file.writeText("Hello World")
281 | // update the index
282 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
283 |
284 | // check if the file was written to the index
285 | val indexFile = File("src/test/resources/workingDirectory/.kit/index")
286 | assertTrue(indexFile.exists())
287 | assert(GitIndex.getEntryCount() == 1)
288 |
289 | // remove the file
290 | updateIndex(file.path, "-d")
291 | assert(GitIndex.getEntryCount() == 0)
292 | clearIndex()
293 | }
294 |
295 | /**
296 | * Testing whether the updateIndex function removes the selected file without removing the other files
297 | */
298 | @Test
299 | fun `updateIndex remove file leave the rest`() {
300 | val workingDirectory = File("src/test/resources/workingDirectory")
301 | workingDirectory.mkdir()
302 | // set the working directory
303 | System.setProperty("user.dir", workingDirectory.path)
304 | // create object database
305 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
306 | objectDatabase.mkdirs()
307 | // create refs and refs/heads
308 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
309 | refs.mkdirs()
310 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
311 | heads.mkdirs()
312 | // create HEAD file
313 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
314 | head.createNewFile()
315 | head.writeText("ref: refs/heads/master")
316 | // create a file with the content "Hello World"
317 | val file = File("src/test/resources/workingDirectory/test.txt")
318 | val file2 = File("src/test/resources/workingDirectory/test2.txt")
319 | file.createNewFile()
320 | file2.createNewFile()
321 | file.writeText("Hello World")
322 | file2.writeText("Hello World")
323 | // update the index
324 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
325 | updateIndex(file2.path, "-a", hashObject(file2.path, write = true), "100644")
326 |
327 | // check if the file was written to the index
328 | val indexFile = File("src/test/resources/workingDirectory/.kit/index")
329 | assertTrue(indexFile.exists())
330 | assert(GitIndex.getEntryCount() == 2)
331 |
332 | // update index remove file
333 | updateIndex(file.path, "-d")
334 | assert(GitIndex.getEntryCount() == 1)
335 |
336 | // update index remove file2
337 | updateIndex(file2.path, "-d")
338 | assert(GitIndex.getEntryCount() == 0)
339 | }
340 |
341 | /**
342 | * Testing whether the init of index works
343 | */
344 | @Test
345 | fun `loadIndex file`() {
346 | // create a working directory
347 | val workingDirectory = File("src/test/resources/workingDirectory")
348 | workingDirectory.mkdir()
349 | // set the working directory
350 | System.setProperty("user.dir", workingDirectory.path)
351 | // create object database
352 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
353 | objectDatabase.mkdirs()
354 | // create refs and refs/heads
355 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
356 | refs.mkdirs()
357 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
358 | heads.mkdirs()
359 | // create HEAD file
360 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
361 | head.createNewFile()
362 | head.writeText("ref: refs/heads/master")
363 | // create a file with the content "Hello World"
364 | val file = File("src/test/resources/workingDirectory/test.txt")
365 | file.createNewFile()
366 | file.writeText("Hello World")
367 | // update the index
368 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
369 |
370 | // check if the file was written to the index
371 | readIndex() // this is a refresh of the index
372 |
373 | val indexFile = File("src/test/resources/workingDirectory/.kit/index")
374 | assertTrue(indexFile.exists())
375 | assert(GitIndex.getEntryCount() == 1)
376 | }
377 |
378 | /**
379 | * Testing invalid option with updateIndex
380 | */
381 | @Test
382 | fun `updateIndex invalid option`() {
383 | val workingDirectory = File("src/test/resources/workingDirectory")
384 | workingDirectory.mkdir()
385 | // set the working directory
386 | System.setProperty("user.dir", workingDirectory.path)
387 | // create object database
388 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
389 | objectDatabase.mkdirs()
390 | // create refs and refs/heads
391 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
392 | refs.mkdirs()
393 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
394 | heads.mkdirs()
395 | // create HEAD file
396 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
397 | head.createNewFile()
398 | head.writeText("ref: refs/heads/master")
399 | // create a file with the content "Hello World"
400 | val file = File("src/test/resources/workingDirectory/test.txt")
401 | file.createNewFile()
402 | file.writeText("Hello World")
403 | // update the index
404 | val exception = assertThrows {
405 | updateIndex(file.path, "-w", hashObject(file.path, write = true), "100644")
406 | }
407 | assertEquals("usage: update-index (-a|-d) ", exception.message)
408 | }
409 |
410 | /**
411 | * Testing list files empty index
412 | */
413 | @Test
414 | fun `ls-files`() {
415 | // create a working directory
416 | val workingDirectory = File("src/test/resources/workingDirectory")
417 | workingDirectory.mkdir()
418 | // set the working directory
419 | System.setProperty("user.dir", workingDirectory.path)
420 | // create object database
421 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
422 | objectDatabase.mkdirs()
423 | // create refs and refs/heads
424 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
425 | refs.mkdirs()
426 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
427 | heads.mkdirs()
428 | // create HEAD file
429 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
430 | head.createNewFile()
431 | head.writeText("ref: refs/heads/master")
432 | // create a file with the content "Hello World"
433 | val file = File("src/test/resources/workingDirectory/test.txt")
434 | file.createNewFile()
435 | file.writeText("Hello World")
436 | // list files
437 | val output = lsFiles()
438 | assertEquals("", output)
439 |
440 | // update the index
441 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
442 |
443 | // list files
444 | val output2 = lsFiles()
445 | assertEquals("test.txt", output2)
446 |
447 | // clean up
448 | clearIndex()
449 |
450 | }
451 |
452 |
453 | /**
454 | * Testing write-tree with non-existing directory
455 | */
456 | @Test
457 | fun `write-tree directory doesn't exists`() {
458 | // create a working directory
459 | val workingDirectory = File("src/test/resources/workingDirectory")
460 | workingDirectory.mkdir()
461 | // set the working directory
462 | System.setProperty("user.dir", workingDirectory.path)
463 | // create object database
464 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
465 | objectDatabase.mkdirs()
466 | // create refs and refs/heads
467 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
468 | refs.mkdirs()
469 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
470 | heads.mkdirs()
471 | // create HEAD file
472 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
473 | head.createNewFile()
474 | head.writeText("ref: refs/heads/master")
475 | // create a file with the content "Hello World"
476 | val file = File("src/test/resources/workingDirectory/test.txt")
477 | file.createNewFile()
478 | file.writeText("Hello World")
479 | // update the index
480 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
481 | try {
482 | // write tree
483 | writeTree("nonExistingDirectory")
484 | // clean up
485 | clearIndex()
486 | fail("Should have thrown an exception")
487 | } catch (e: Exception) {
488 | assertEquals(
489 | "java.io.FileNotFoundException: nonExistingDirectory (No such file or directory)",
490 | e.toString()
491 | )
492 | // clean up
493 | clearIndex()
494 | }
495 | }
496 |
497 | /**
498 | * Testing write-tree with empty directory
499 | * @return empty string
500 | */
501 | @Test
502 | fun `write-tree directory is empty`() {
503 | // create a working directory
504 | val workingDirectory = File("src/test/resources/workingDirectory")
505 | workingDirectory.mkdir()
506 | // set the working directory
507 | System.setProperty("user.dir", workingDirectory.path)
508 | // create object database
509 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
510 | objectDatabase.mkdirs()
511 | // create refs and refs/heads
512 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
513 | refs.mkdirs()
514 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
515 | heads.mkdirs()
516 | // create HEAD file
517 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
518 | head.createNewFile()
519 | head.writeText("ref: refs/heads/master")
520 | // write tree
521 | val sha1 = writeTree("src/test/resources/workingDirectory")
522 |
523 | assertEquals("", sha1) // empty directory
524 | }
525 |
526 | /**
527 | * Testing write-tree sha1 against git sha1
528 | */
529 | @Test
530 | fun `write-tree with write = false`() {
531 | // create a working directory
532 | val workingDirectory = File("src/test/resources/workingDirectory")
533 | workingDirectory.mkdir()
534 | // set the working directory
535 | System.setProperty("user.dir", workingDirectory.path)
536 | // create object database
537 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
538 | objectDatabase.mkdirs()
539 | // create refs and refs/heads
540 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
541 | refs.mkdirs()
542 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
543 | heads.mkdirs()
544 | // create HEAD file
545 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
546 | head.createNewFile()
547 | head.writeText("ref: refs/heads/master")
548 | // create a file with the content "Hello World"
549 | val file = File("src/test/resources/workingDirectory/test.txt")
550 | file.createNewFile()
551 | file.writeText("Hello World")
552 | // update the index
553 | var sha1 = hashObject(file.path, write = true)
554 | assertEquals("5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689", sha1) //
555 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
556 | // write tree
557 | sha1 = writeTree("src/test/resources/workingDirectory", false)
558 |
559 | assertEquals("4f11af3e4e067fc319abd053205f39bc40652f05", sha1) // this sha1 is generated by git
560 | // clean up
561 | clearIndex()
562 | }
563 |
564 | /**
565 | * Testing write-tree that writes to the object database
566 | */
567 | @Test
568 | fun `write-tree with write = true`() {
569 | // create a working directory
570 | val workingDirectory = File("src/test/resources/workingDirectory")
571 | workingDirectory.mkdir()
572 | // set the working directory
573 | System.setProperty("user.dir", workingDirectory.path)
574 | // create object database
575 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
576 | objectDatabase.mkdirs()
577 | // create refs and refs/heads
578 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
579 | refs.mkdirs()
580 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
581 | heads.mkdirs()
582 | // create HEAD file
583 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
584 | head.createNewFile()
585 | head.writeText("ref: refs/heads/master")
586 | // create a file with the content "Hello World"
587 | val file = File("src/test/resources/workingDirectory/test.txt")
588 | file.createNewFile()
589 | file.writeText("Hello World")
590 | // update the index
591 | var sha1 = hashObject(file.path, write = true)
592 | assertEquals("5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689", sha1) //
593 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
594 | // write tree
595 | sha1 = writeTree("src/test/resources/workingDirectory", true)
596 |
597 | assertEquals("4f11af3e4e067fc319abd053205f39bc40652f05", sha1) // this sha1 is generated by git
598 | assert(
599 | File(
600 | "src/test/resources/workingDirectory/.kit/objects/" + sha1.substring(
601 | 0,
602 | 2
603 | ) + "/" + sha1.substring(2)
604 | ).exists()
605 | ) { "File does not exist" }
606 | // clean up
607 | clearIndex()
608 | }
609 |
610 | @Test
611 | fun `write-tree with write but object database doesn't exist`() {
612 | // create working directory
613 | val workingDirectory = File("src/test/resources/workingDirectory")
614 | workingDirectory.mkdir()
615 | // set the working directory
616 | System.setProperty("user.dir", workingDirectory.path)
617 | // create a file with the content "Hello World"
618 | val file = File("src/test/resources/workingDirectory/test.txt")
619 | file.createNewFile()
620 | file.writeText("Hello World")
621 | // hash the file
622 | try {
623 | // create object database
624 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
625 | objectDatabase.mkdirs()
626 | // create refs and refs/heads
627 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
628 | refs.mkdirs()
629 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
630 | heads.mkdirs()
631 | // create HEAD file
632 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
633 | head.createNewFile()
634 | head.writeText("ref: refs/heads/master")
635 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
636 | objectDatabase.deleteRecursively() // to simulate the object database doesn't exist
637 | writeTree("src/test/resources/workingDirectory", true)
638 | } catch (e: Exception) {
639 | assertEquals(
640 | /* expected = */ "The repository doesn't exist",
641 | /* actual = */ e.message,
642 | /* message = */ "The exception message should be 'The repository doesn't exist'"
643 | )
644 | }
645 | }
646 |
647 |
648 | @Test
649 | fun `write-tree ignore files that aren't in the index`() {
650 | // create a working directory
651 | val workingDirectory = File("src/test/resources/workingDirectory")
652 | workingDirectory.mkdir()
653 | // set the working directory
654 | System.setProperty("user.dir", workingDirectory.path)
655 | // create object database
656 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
657 | objectDatabase.mkdirs()
658 | // create refs and refs/heads
659 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
660 | refs.mkdirs()
661 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
662 | heads.mkdirs()
663 | // create HEAD file
664 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
665 | head.createNewFile()
666 | head.writeText("ref: refs/heads/master")
667 | // create a file with the content "Hello World"
668 | val file = File("src/test/resources/workingDirectory/test.txt")
669 | file.createNewFile()
670 | file.writeText("Hello World")
671 | val file2 = File("src/test/resources/workingDirectory/test2.txt")
672 | file2.createNewFile()
673 | file2.writeText("Hello World 2")
674 | // update the index
675 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
676 | // ignore file2
677 | // write tree
678 | val sha1 = writeTree("src/test/resources/workingDirectory", true)
679 | assertEquals("4f11af3e4e067fc319abd053205f39bc40652f05", sha1) // this sha1 is generated by git
680 |
681 | // clean up
682 | clearIndex()
683 | }
684 |
685 | @Test
686 | fun `write-tree work with subdirectories`() {
687 | // create a working directory
688 | val workingDirectory = File("src/test/resources/workingDirectory")
689 | workingDirectory.mkdir()
690 | // set the working directory
691 | System.setProperty("user.dir", workingDirectory.path)
692 | // create object database
693 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
694 | objectDatabase.mkdirs()
695 | // create refs and refs/heads
696 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
697 | refs.mkdirs()
698 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
699 | heads.mkdirs()
700 | // create HEAD file
701 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
702 | head.createNewFile()
703 | head.writeText("ref: refs/heads/master")
704 | // create a dummy directory
705 | val dummyDirectory = File("src/test/resources/workingDirectory/dummy")
706 | dummyDirectory.mkdir()
707 | // create a file with the content "Hello World"
708 | val file = File("src/test/resources/workingDirectory/dummy/test.txt")
709 | file.createNewFile()
710 | file.writeText("Hello World")
711 | // update the index
712 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
713 | // ignore file2
714 | // write tree
715 | val sha1 = writeTree("src/test/resources/workingDirectory", true)
716 | assertEquals("7c3ab9742549cc307a65f3c20b9aa488507a10da", sha1) // this sha1 is generated by git
717 | }
718 |
719 | @Test
720 | fun `commit-tree invalid tree`() {
721 | // create a working directory
722 | val workingDirectory = File("src/test/resources/workingDirectory")
723 | workingDirectory.mkdir()
724 | // set the working directory
725 | System.setProperty("user.dir", workingDirectory.path)
726 | // create object database
727 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
728 | objectDatabase.mkdirs()
729 | // create refs and refs/heads
730 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
731 | refs.mkdirs()
732 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
733 | heads.mkdirs()
734 | // create HEAD file
735 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
736 | head.createNewFile()
737 | head.writeText("ref: refs/heads/master")
738 | // create a file with the content "Hello World"
739 | val file = File("src/test/resources/workingDirectory/test.txt")
740 | file.createNewFile()
741 | file.writeText("Hello World")
742 | val randomSha1 = Random.nextBytes(20).joinToString { "%02x".format(it) }
743 |
744 | try {
745 | commitTree(randomSha1, "master")
746 | fail("The commit-tree should fail because the tree doesn't exist")
747 | } catch (e: Exception) {
748 | assertEquals(
749 | /* expected = */ "The tree doesn't exist",
750 | /* actual = */ e.message,
751 | /* message = */ "The exception message should be 'The tree doesn't exist'"
752 | )
753 | }
754 | }
755 |
756 | @Test
757 | fun `commit-tree invalid parent`() {
758 | // create a working directory
759 | val workingDirectory = File("src/test/resources/workingDirectory")
760 | workingDirectory.mkdir()
761 | // set the working directory
762 | System.setProperty("user.dir", workingDirectory.path)
763 | // create object database
764 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
765 | objectDatabase.mkdirs()
766 | // create refs and refs/heads
767 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
768 | refs.mkdirs()
769 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
770 | heads.mkdirs()
771 | // create HEAD file
772 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
773 | head.createNewFile()
774 | head.writeText("ref: refs/heads/master")
775 | // create a file with the content "Hello World"
776 | val file = File("src/test/resources/workingDirectory/test.txt")
777 | file.createNewFile()
778 | file.writeText("Hello World")
779 | // update the index
780 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
781 | // write tree
782 | val sha1 = writeTree("src/test/resources/workingDirectory", true)
783 | val randomSha1 = Random.nextBytes(20).joinToString { "%02x".format(it) }
784 |
785 | try {
786 | commitTree(sha1, "init", randomSha1)
787 | fail("The commit-tree should fail because the tree doesn't exist")
788 | } catch (e: Exception) {
789 | assertEquals(
790 | /* expected = */ "parent commit doesn't exist",
791 | /* actual = */ e.message,
792 | /* message = */ "The exception message should be 'The tree doesn't exist'"
793 | )
794 | // clean up
795 | clearIndex()
796 | }
797 | }
798 |
799 | @Test
800 | fun `commit-tree valid commit`() {
801 | // create a working directory
802 | val workingDirectory = File("src/test/resources/workingDirectory")
803 | workingDirectory.mkdir()
804 | // set the working directory
805 | System.setProperty("user.dir", workingDirectory.path)
806 | // create object database
807 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
808 | objectDatabase.mkdirs()
809 | // create refs and refs/heads
810 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
811 | refs.mkdirs()
812 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
813 | heads.mkdirs()
814 | // create HEAD file
815 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
816 | head.createNewFile()
817 | head.writeText("ref: refs/heads/master")
818 | // create a file
819 | val file = File("src/test/resources/workingDirectory/test.txt")
820 | file.createNewFile()
821 | file.writeText("Hello World")
822 | // update the index
823 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
824 | // write tree
825 | val tree = writeTree("src/test/resources/workingDirectory", true)
826 | // commit tree
827 | val commit = commitTree(tree, "test commit")
828 | assert(commit.objectExists())
829 | // due to the fact that the commit hash always changes, we can't check the hash
830 | // we check the content of the commit
831 | val commitContent =
832 | Zlib.inflate(File(commit.objectPath()).readBytes()).toString(Charsets.UTF_8).substringAfter("\u0000")
833 | assertEquals("tree $tree", commitContent.split("\n")[0])
834 | assertEquals("test commit", commitContent.split("\n")[4])
835 |
836 | // clean up
837 | clearIndex()
838 | }
839 |
840 | @Test
841 | fun `commit-tree valid commit with parent`() {
842 | // create a working directory
843 | val workingDirectory = File("src/test/resources/workingDirectory")
844 | workingDirectory.mkdir()
845 | // set the working directory
846 | System.setProperty("user.dir", workingDirectory.path)
847 | // create object database
848 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
849 | objectDatabase.mkdirs()
850 | // create refs and refs/heads
851 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
852 | refs.mkdirs()
853 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
854 | heads.mkdirs()
855 | // create HEAD file
856 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
857 | head.createNewFile()
858 | head.writeText("ref: refs/heads/master")
859 | // create a file
860 | val file = File("src/test/resources/workingDirectory/test.txt")
861 | file.createNewFile()
862 | file.writeText("Hello World")
863 | // update the index
864 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
865 | // write tree
866 | val tree = writeTree("src/test/resources/workingDirectory", true)
867 | // commit tree
868 | val commit = commitTree(tree, "test commit")
869 | assert(commit.objectExists())
870 | // change the file
871 | file.writeText("Hello World 2")
872 | // update the index
873 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
874 | // write tree
875 | val tree2 = writeTree("src/test/resources/workingDirectory", true)
876 | // commit tree
877 | val commit2 = commitTree(tree2, "test commit 2", commit)
878 | assert(commit2.objectExists())
879 | }
880 |
881 | @Test
882 | fun `cat-file blob`() {
883 | // create a working directory
884 | val workingDirectory = File("src/test/resources/workingDirectory")
885 | workingDirectory.mkdir()
886 | // set the working directory
887 | System.setProperty("user.dir", workingDirectory.path)
888 | // create object database
889 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
890 | objectDatabase.mkdirs()
891 | // create a file
892 | val file = File("src/test/resources/workingDirectory/test.txt")
893 | file.createNewFile()
894 | file.writeText("Hello World")
895 | // hash the file
896 | val sha1 = hashObject(file.path, write = true)
897 | // cat the file
898 | val content = catFile(sha1, "-p")
899 | assertEquals("Hello World", content)
900 | val size = catFile(sha1, "-s")
901 | assertEquals("11", size)
902 | val type = catFile(sha1, "-t")
903 | assertEquals("blob", type)
904 | }
905 |
906 | @Test
907 | fun `cat-file tree`() {
908 | // create a working directory
909 | val workingDirectory = File("src/test/resources/workingDirectory")
910 | workingDirectory.mkdir()
911 | // set the working directory
912 | System.setProperty("user.dir", workingDirectory.path)
913 | // create object database
914 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
915 | objectDatabase.mkdirs()
916 | // create a file
917 | val file = File("src/test/resources/workingDirectory/test.txt")
918 | file.createNewFile()
919 | file.writeText("Hello World")
920 | // update the index
921 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
922 | // write tree
923 | val sha1 = writeTree("src/test/resources/workingDirectory", true)
924 | // cat the file
925 | val content = catFile(sha1, "-p")
926 | assertEquals("100644 blob 5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689\ttest.txt", content)
927 | val size = catFile(sha1, "-s")
928 | assertEquals("36", size)
929 | val type = catFile(sha1, "-t")
930 | assertEquals("tree", type)
931 |
932 | // clean up
933 | clearIndex()
934 | }
935 |
936 | @Test
937 | fun `cat-file commit`() {
938 | // create a working directory
939 | val workingDirectory = File("src/test/resources/workingDirectory")
940 | workingDirectory.mkdir()
941 | // set the working directory
942 | System.setProperty("user.dir", workingDirectory.path)
943 | // create object database
944 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
945 | objectDatabase.mkdirs()
946 | // create refs and refs/heads
947 | val refs = File("src/test/resources/workingDirectory/.kit/refs")
948 | refs.mkdirs()
949 | val heads = File("src/test/resources/workingDirectory/.kit/refs/heads")
950 | heads.mkdirs()
951 | // create HEAD file
952 | val head = File("src/test/resources/workingDirectory/.kit/HEAD")
953 | head.createNewFile()
954 | head.writeText("ref: refs/heads/master")
955 | // create a file
956 | val file = File("src/test/resources/workingDirectory/test.txt")
957 | file.createNewFile()
958 | file.writeText("Hello World")
959 | // update the index
960 | updateIndex(file.path, "-a", hashObject(file.path, write = true), "100644")
961 | // write tree
962 | val tree = writeTree("src/test/resources/workingDirectory", true)
963 | // commit tree
964 | val commit = commitTree(tree, "test commit")
965 | assert(commit.objectExists())
966 | // cat the file
967 | val content = catFile(commit, "-p")
968 | assertEquals(6, content.split("\n").size)
969 | val type = catFile(commit, "-t")
970 | assertEquals("commit", type)
971 |
972 | // clean up
973 | clearIndex()
974 | }
975 |
976 | @Test
977 | fun `cat-file invalid option`() {
978 | // create a working directory
979 | val workingDirectory = File("src/test/resources/workingDirectory")
980 | workingDirectory.mkdir()
981 | // set the working directory
982 | System.setProperty("user.dir", workingDirectory.path)
983 | // create object database
984 | val objectDatabase = File("src/test/resources/workingDirectory/.kit/objects")
985 | objectDatabase.mkdirs()
986 | // create a file
987 | val file = File("src/test/resources/workingDirectory/test.txt")
988 | file.createNewFile()
989 | file.writeText("Hello World")
990 | // hash the file
991 | val sha1 = hashObject(file.path, write = true)
992 | // cat the file
993 | val exception = assertThrows {
994 | catFile(sha1, "-x")
995 | }
996 | assertEquals("usage: cat-file [-t | -s | -p] ", exception.message)
997 | }
998 | }
--------------------------------------------------------------------------------