Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 798adb5299 | |||
| a2638b0c24 | |||
| e4a01db90d | |||
| 5b6185b236 | |||
| 3c7444675d | |||
| 7610f5d97f | |||
| 53cb54c662 | |||
| 82ed9fb77a | |||
| b9c637f0ba | |||
| fac9062f83 | |||
| a314b88eb3 | |||
| b3974c15f2 | |||
| 768587ce57 | |||
| 7c595ef022 | |||
| 270223dd9f | |||
| 0e8ab5f4ef | |||
| 4077c1b28e | |||
| e6f6e2466b | |||
| 5be1b1cc29 | |||
| 36ecd019a8 |
@@ -1,22 +1,22 @@
|
|||||||
name: Build
|
name: Build and Test
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- '*'
|
- master
|
||||||
- '!master'
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '25'
|
java-version: '25'
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
- name: Build
|
- name: Build and Test
|
||||||
run: ./gradlew build --no-daemon
|
run: ./gradlew build --no-daemon
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '25'
|
java-version: '25'
|
||||||
@@ -20,6 +20,9 @@ jobs:
|
|||||||
- name: Build Jar
|
- name: Build Jar
|
||||||
run: ./gradlew bootJar
|
run: ./gradlew bootJar
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: ./gradlew test
|
||||||
|
|
||||||
- name: Build Container
|
- name: Build Container
|
||||||
run: docker build --tag gitea.seeseepuff.be/seeseemelk/webgit:latest .
|
run: docker build --tag gitea.seeseepuff.be/seeseemelk/webgit:latest .
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Copilot Instructions
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
This project targets **retro systems** (Windows 3.11, Windows 98, FreeDOS).
|
||||||
|
The UI must work in period-appropriate browsers:
|
||||||
|
|
||||||
|
- **No JavaScript** — all interactivity must be server-side.
|
||||||
|
- **No CSS** — use HTML attributes (`border`, `cellpadding`, `cellspacing`) for layout.
|
||||||
|
- **Use table-based layouts** to arrange elements side by side.
|
||||||
|
- Keep HTML simple: plain `<form>`, `<table>`, `<input>`, `<a>` elements only.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- **All new functionality must have unit tests.**
|
||||||
|
- Service-layer tests go in `src/test/java/.../service/GitServiceTest.java` and use
|
||||||
|
real JGit repositories created in `@TempDir` directories.
|
||||||
|
- Controller tests go in `src/test/java/.../controller/RepoControllerTest.java` and
|
||||||
|
use `@WebMvcTest` with `@MockitoBean` for the service layer.
|
||||||
|
- Run `./gradlew test` to verify all tests pass before considering a change complete.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- Java 25, Spring Boot 4.0, Thymeleaf, Gradle
|
||||||
|
- JGit for all Git operations (no shelling out to `git`)
|
||||||
|
- Lombok for boilerplate reduction (`@Getter`, `@Setter`, `@RequiredArgsConstructor`)
|
||||||
+7
-2
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
jacoco
|
jacoco
|
||||||
id("org.springframework.boot") version "4.0.3"
|
id("org.springframework.boot") version "4.0.6"
|
||||||
id("io.spring.dependency-management") version "1.1.7"
|
id("io.spring.dependency-management") version "1.1.7"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ dependencies {
|
|||||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
|
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-webmvc")
|
implementation("org.springframework.boot:spring-boot-starter-webmvc")
|
||||||
implementation("org.eclipse.jgit:org.eclipse.jgit:7.2.0.202503040940-r")
|
implementation("org.eclipse.jgit:org.eclipse.jgit:7.6.0.202603022253-r")
|
||||||
compileOnly("org.projectlombok:lombok")
|
compileOnly("org.projectlombok:lombok")
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||||
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
|
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
|
||||||
@@ -43,6 +43,11 @@ dependencies {
|
|||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
finalizedBy(tasks.jacocoTestReport)
|
finalizedBy(tasks.jacocoTestReport)
|
||||||
|
afterSuite(KotlinClosure2<TestDescriptor, TestResult, Unit>({ desc, result ->
|
||||||
|
if (desc.parent == null) {
|
||||||
|
println("Test results: ${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped")
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.jacocoTestReport {
|
tasks.jacocoTestReport {
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
|
retries=0
|
||||||
|
retryBackOffMs=500
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|||||||
Vendored
+10
-21
@@ -23,8 +23,8 @@
|
|||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
setlocal EnableExtensions
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
@@ -51,7 +51,7 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
@@ -65,7 +65,7 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
@@ -73,21 +73,10 @@ goto fail
|
|||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||||
|
@rem which allows us to clear the local environment before executing the java command
|
||||||
|
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||||
|
|
||||||
:end
|
:exitWithErrorLevel
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -619,7 +619,20 @@ public class GitService
|
|||||||
if (properties.getUsername() != null)
|
if (properties.getUsername() != null)
|
||||||
cmd.setCredentialsProvider(new org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider(
|
cmd.setCredentialsProvider(new org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider(
|
||||||
properties.getUsername(), properties.getPassword()));
|
properties.getUsername(), properties.getPassword()));
|
||||||
cmd.call();
|
Iterable<org.eclipse.jgit.transport.PushResult> results = cmd.call();
|
||||||
|
for (var result : results)
|
||||||
|
{
|
||||||
|
for (var update : result.getRemoteUpdates())
|
||||||
|
{
|
||||||
|
var status = update.getStatus();
|
||||||
|
if (status != org.eclipse.jgit.transport.RemoteRefUpdate.Status.OK
|
||||||
|
&& status != org.eclipse.jgit.transport.RemoteRefUpdate.Status.UP_TO_DATE)
|
||||||
|
{
|
||||||
|
throw new IOException("Push failed: " + update.getRemoteName() + " " + status
|
||||||
|
+ (update.getMessage() != null ? " - " + update.getMessage() : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,7 +644,11 @@ public class GitService
|
|||||||
if (properties.getUsername() != null)
|
if (properties.getUsername() != null)
|
||||||
cmd.setCredentialsProvider(new org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider(
|
cmd.setCredentialsProvider(new org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider(
|
||||||
properties.getUsername(), properties.getPassword()));
|
properties.getUsername(), properties.getPassword()));
|
||||||
cmd.call();
|
var result = cmd.call();
|
||||||
|
if (!result.isSuccessful())
|
||||||
|
{
|
||||||
|
throw new IOException("Pull failed: " + result.getMergeResult().getMergeStatus());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -351,4 +351,78 @@ class RepoControllerTest
|
|||||||
.andExpect(model().attribute("filePath", "src/main.txt"))
|
.andExpect(model().attribute("filePath", "src/main.txt"))
|
||||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("+hello")));
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("+hello")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void changesPageShowsAheadBehind() throws Exception
|
||||||
|
{
|
||||||
|
when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of());
|
||||||
|
when(gitService.getStagedFiles("myrepo")).thenReturn(List.of());
|
||||||
|
when(gitService.getAheadBehind("myrepo")).thenReturn(new int[]{3, 1});
|
||||||
|
|
||||||
|
mockMvc.perform(get("/repo/myrepo/changes"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(model().attribute("commitsAhead", 3))
|
||||||
|
.andExpect(model().attribute("commitsBehind", 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void changesPageHidesAheadBehindWhenNoTracking() throws Exception
|
||||||
|
{
|
||||||
|
when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of());
|
||||||
|
when(gitService.getStagedFiles("myrepo")).thenReturn(List.of());
|
||||||
|
when(gitService.getAheadBehind("myrepo")).thenReturn(null);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/repo/myrepo/changes"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(model().attributeDoesNotExist("commitsAhead"))
|
||||||
|
.andExpect(model().attributeDoesNotExist("commitsBehind"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pushRedirectsToChangesWhenRequested() throws Exception
|
||||||
|
{
|
||||||
|
mockMvc.perform(post("/repo/myrepo/push")
|
||||||
|
.param("redirectTo", "changes"))
|
||||||
|
.andExpect(status().is3xxRedirection())
|
||||||
|
.andExpect(redirectedUrl("/repo/myrepo/changes"));
|
||||||
|
|
||||||
|
verify(gitService).push("myrepo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pullRedirectsToChangesWhenRequested() throws Exception
|
||||||
|
{
|
||||||
|
mockMvc.perform(post("/repo/myrepo/pull")
|
||||||
|
.param("redirectTo", "changes"))
|
||||||
|
.andExpect(status().is3xxRedirection())
|
||||||
|
.andExpect(redirectedUrl("/repo/myrepo/changes"));
|
||||||
|
|
||||||
|
verify(gitService).pull("myrepo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stageWithSelectAllStagesAllModifiedFiles() throws Exception
|
||||||
|
{
|
||||||
|
when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of("a.txt", "b.txt", "c.txt"));
|
||||||
|
|
||||||
|
mockMvc.perform(post("/repo/myrepo/stage")
|
||||||
|
.param("selectAll", "on"))
|
||||||
|
.andExpect(status().is3xxRedirection())
|
||||||
|
.andExpect(redirectedUrl("/repo/myrepo/changes"));
|
||||||
|
|
||||||
|
verify(gitService).stageFiles("myrepo", List.of("a.txt", "b.txt", "c.txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unstageWithSelectAllUnstagesAllStagedFiles() throws Exception
|
||||||
|
{
|
||||||
|
when(gitService.getStagedFiles("myrepo")).thenReturn(List.of("a.txt", "b.txt"));
|
||||||
|
|
||||||
|
mockMvc.perform(post("/repo/myrepo/unstage")
|
||||||
|
.param("selectAll", "on"))
|
||||||
|
.andExpect(status().is3xxRedirection())
|
||||||
|
.andExpect(redirectedUrl("/repo/myrepo/changes"));
|
||||||
|
|
||||||
|
verify(gitService).unstageFiles("myrepo", List.of("a.txt", "b.txt"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -561,4 +561,72 @@ class GitServiceTest
|
|||||||
|
|
||||||
assertEquals("# Test", Files.readString(readme));
|
assertEquals("# Test", Files.readString(readme));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAheadBehindReturnsZeroWhenUpToDate() throws GitAPIException, IOException
|
||||||
|
{
|
||||||
|
gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo");
|
||||||
|
|
||||||
|
int[] result = gitService.getAheadBehind("myrepo");
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(0, result[0]);
|
||||||
|
assertEquals(0, result[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAheadBehindReturnsAheadCount() throws GitAPIException, IOException
|
||||||
|
{
|
||||||
|
gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo");
|
||||||
|
Files.writeString(worktreePath.resolve("myrepo/newfile.txt"), "hello");
|
||||||
|
gitService.stageFiles("myrepo", List.of("newfile.txt"));
|
||||||
|
gitService.commit("myrepo", "Local commit");
|
||||||
|
|
||||||
|
int[] result = gitService.getAheadBehind("myrepo");
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(1, result[0]);
|
||||||
|
assertEquals(0, result[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAheadBehindReturnsBehindCount() throws GitAPIException, IOException
|
||||||
|
{
|
||||||
|
gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo");
|
||||||
|
|
||||||
|
// Push a commit to the remote via a second clone
|
||||||
|
Path tmpWork = tempDir.resolve("tmp-work2");
|
||||||
|
try (Git tmp = Git.cloneRepository()
|
||||||
|
.setURI(bareRemote.toUri().toString())
|
||||||
|
.setDirectory(tmpWork.toFile())
|
||||||
|
.call())
|
||||||
|
{
|
||||||
|
Files.writeString(tmpWork.resolve("remote-file.txt"), "remote");
|
||||||
|
tmp.add().addFilepattern("remote-file.txt").call();
|
||||||
|
tmp.commit().setMessage("Remote commit").call();
|
||||||
|
tmp.push().call();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch so our repo knows about the remote commit
|
||||||
|
try (Git git = Git.open(worktreePath.resolve("myrepo").toFile()))
|
||||||
|
{
|
||||||
|
git.fetch().call();
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] result = gitService.getAheadBehind("myrepo");
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(0, result[0]);
|
||||||
|
assertEquals(1, result[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pushFailsWithBadRemote() throws GitAPIException, IOException
|
||||||
|
{
|
||||||
|
gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo");
|
||||||
|
gitService.updateRemoteUrl("myrepo", "origin", "https://invalid.example.com/repo.git");
|
||||||
|
|
||||||
|
Files.writeString(worktreePath.resolve("myrepo/newfile.txt"), "hello");
|
||||||
|
gitService.stageFiles("myrepo", List.of("newfile.txt"));
|
||||||
|
gitService.commit("myrepo", "Local commit");
|
||||||
|
|
||||||
|
assertThrows(Exception.class, () -> gitService.push("myrepo"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user