Skip to content

Commit 7764669

Browse files
committed
Add Git and GitRepo and tests and fixtures
1 parent a17ac25 commit 7764669

File tree

11 files changed

+1095
-0
lines changed

11 files changed

+1095
-0
lines changed

git-library/build.gradle.kts

+12
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,24 @@
33
@Suppress("DSL_SCOPE_VIOLATION")
44
plugins {
55
`java-library`
6+
`java-test-fixtures`
67
id("org.metaborg.convention.java")
78
id("org.metaborg.convention.maven-publish")
89
id("org.metaborg.convention.junit")
910
alias(libs.plugins.kotlin.jvm)
1011
}
1112

13+
description = "A library for Git."
14+
15+
dependencies {
16+
testImplementation (libs.kotest)
17+
testImplementation (libs.kotest.assertions)
18+
testImplementation (libs.kotest.datatest)
19+
testImplementation (libs.kotest.property)
20+
21+
testFixturesImplementation(libs.kotest)
22+
}
23+
1224
publishing {
1325
publications {
1426
create<MavenPublication>("mavenJava") {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.metaborg.git
2+
3+
import java.io.IOException
4+
5+
/** An exception that is thrown when a command fails. */
6+
class CommandException(
7+
/** The command that failed. */
8+
val command: String,
9+
/** The exit code of the command. */
10+
val exitCode: Int,
11+
/** The standard error output of the command. */
12+
val stderr: String,
13+
/** The error message, or null to use a default message. */
14+
message: String? = null,
15+
): IOException(
16+
(message ?: "$command: Command failed with exit code $exitCode") + if(stderr.isNotBlank()) ": $stderr" else "."
17+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.metaborg.git
2+
3+
import java.io.File
4+
import java.io.IOException
5+
import kotlin.jvm.Throws
6+
7+
/**
8+
* Runs a command.
9+
*
10+
* @param command The command.
11+
* @param workingDirectory The working directory to use; or `null` to use the current JVM working directory.
12+
* @param environment The environment variables to use.
13+
* @return The standard output of the command.
14+
* @throws CommandException If the command fails or returns a non-zero exit code.
15+
* @throws IOException If an I/O error occurs.
16+
*/
17+
@Throws(CommandException::class, IOException::class, InterruptedException::class)
18+
internal fun runCommand(
19+
command: List<String>,
20+
workingDirectory: File?,
21+
environment: Map<String, String> = emptyMap(),
22+
): String {
23+
require(command.isNotEmpty()) { "No command arguments provided." }
24+
25+
val processBuilder = ProcessBuilder().command(*command.toTypedArray())
26+
processBuilder.environment().putAll(environment)
27+
processBuilder.directory(workingDirectory)
28+
29+
// THROWS: IOException, SecurityException, UnsupportedOperationException
30+
val process = processBuilder.start()
31+
// NOTE: We don't close streams that we didn't open.
32+
val stdout = process.inputStream.bufferedReader().readText()
33+
val stderr = process.errorStream.bufferedReader().readText()
34+
// THROWS: InterruptedException
35+
val exitCode = process.waitFor()
36+
if (exitCode != 0) throw CommandException(command.joinToString(" "), exitCode, stderr)
37+
return stdout.trim()
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.metaborg.git
2+
3+
import java.io.File
4+
import java.io.IOException
5+
6+
/** Interface for interacting with Git. */
7+
interface Git {
8+
9+
/**
10+
* Gets the version of Git that is locally installed.
11+
*
12+
* @return The version of Git that is locally installed;
13+
* or `null` if Git is not installed or the version could not be determined.
14+
* @throws IOException If an I/O error occurs.
15+
*/
16+
fun getGitVersion(): String?
17+
18+
/**
19+
* Opens the Git repository at the specified directory.
20+
*
21+
* @param directory The directory of the Git repository.
22+
* @param globalConfig The global Git configuration file to use;
23+
* or `null` to use the default global Git configuration.
24+
* @return The Git repository object.
25+
*/
26+
fun open(directory: File, globalConfig: File? = null): GitRepo
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package org.metaborg.git
2+
3+
import java.io.File
4+
import java.io.IOException
5+
import kotlin.jvm.Throws
6+
7+
/**
8+
* Calls Git commands for a specific repository.
9+
*/
10+
interface GitRepo {
11+
12+
/** The repository working directory. */
13+
val directory: File
14+
15+
/**
16+
* Gets the name of the current branch. If the repository is not on a branch (i.e., detached HEAD),
17+
* this returns an empty string.
18+
*
19+
* @throws CommandException If the command fails or returns a non-zero exit code.
20+
* @throws IOException If an I/O error occurs.
21+
*/
22+
@Throws(IOException::class)
23+
fun getCurrentBranch(): String
24+
25+
/**
26+
* Gets the current commit hash.
27+
*
28+
* @param short Whether to return the short commit hash (7 characters or more) or the full (40 character) commit hash.
29+
* @return The current commit hash.
30+
* @throws CommandException If the command fails or returns a non-zero exit code.
31+
* @throws IOException If an I/O error occurs.
32+
*/
33+
@Throws(IOException::class)
34+
fun getCurrentCommitHash(short: Boolean = false): String
35+
36+
/**
37+
* Gets whether the repository is clean (i.e., has no uncommitted changes).
38+
*
39+
* @return `true` when the repository is clean; `false` when there are uncommitted changes.
40+
* @throws CommandException If the command fails or returns a non-zero exit code.
41+
* @throws IOException If an I/O error occurs.
42+
*/
43+
@Throws(IOException::class)
44+
fun getIsClean(): Boolean
45+
46+
/**
47+
* Gets a human-readable name for this commit based on the last tag and the commit hash.
48+
*
49+
* This returns a name of the form `"<tag>[-<numCommits>-g<shortHash>]"`, where `<tag>` is the last matching tag
50+
* on this branch, `<numCommits>` is the number of commits since the tag, and `<shortHash>`
51+
* is the short commit hash. If the matching tag points to the current commit, this returns just the tag.
52+
* This is used as the version number.
53+
*
54+
* @param patterns The glob patterns to match tags against; or none to match all tags.
55+
* @param withHash Whether to include the short commit hash in the name if the tag does not point to the current commit.
56+
* @param firstParentOnly Whether to only consider the first parent when looking for tags across merge commits.
57+
* @param commit The commit for which to find the tag description.
58+
* @return The human-readable name of the last tag on this branch, optionally with a short commit hash suffix
59+
* if the tag does not point to the current commit. If there is no tag, this just returns the short commit hash.
60+
* @throws CommandException If the command fails or returns a non-zero exit code.
61+
* @throws IOException If an I/O error occurs.
62+
*/
63+
@Throws(IOException::class)
64+
fun getTagDescription(
65+
vararg patterns: String,
66+
withHash: Boolean = false,
67+
firstParentOnly: Boolean = false,
68+
commit: String = "HEAD",
69+
): String
70+
71+
/**
72+
* Gets the status of the repository.
73+
*
74+
* This is equivalent to `git status --porcelain`.
75+
*
76+
* @param showUntracked Whether to show untracked files.
77+
* @return The status of the repository.
78+
* @throws CommandException If the command fails or returns a non-zero exit code.
79+
* @throws IOException If an I/O error occurs.
80+
*/
81+
@Throws(IOException::class)
82+
fun getStatus(showUntracked: Boolean = true): String
83+
84+
/**
85+
* Initializes the repository.
86+
*
87+
* This is equivalent to `git init`.
88+
*
89+
* @throws IOException If an I/O error occurs.
90+
*/
91+
@Throws(IOException::class)
92+
fun init()
93+
94+
/**
95+
* Stages all changes in the repository.
96+
*
97+
* This is equivalent to `git add -A`.
98+
*/
99+
@Throws(IOException::class)
100+
fun addAll()
101+
102+
/**
103+
* Commits all staged changes with the given message.
104+
*
105+
* This is equivalent to `git commit -m <message>`.
106+
*
107+
* @param message The commit message.
108+
* @param allowEmpty Whether to allow an empty commit.
109+
*/
110+
@Throws(IOException::class)
111+
fun commit(message: String, allowEmpty: Boolean = false)
112+
113+
/**
114+
* Detach HEAD at the tip of the current branch.
115+
*
116+
* This is equivalent to `git checkout --detach`.
117+
*/
118+
@Throws(IOException::class)
119+
fun detach()
120+
121+
/**
122+
* Adds a non-annotated (light-weight) tag to the current commit.
123+
*
124+
* This is equivalent to `git tag <tagName>`.
125+
*
126+
* @param tagName The name of the tag.
127+
*/
128+
@Throws(IOException::class)
129+
fun tag(tagName: String)
130+
131+
/**
132+
* Creates a new branch with the specified name.
133+
*
134+
* This is equivalent to `git switch --create <branchName>`.
135+
*
136+
* @param branchName The name of the branch.
137+
*/
138+
@Throws(IOException::class)
139+
fun createBranch(branchName: String)
140+
141+
}

0 commit comments

Comments
 (0)