Compare commits

..

20 Commits

Author SHA1 Message Date
seeseemelk 798adb5299 Merge branch 'master' into renovate/spring-boot
Build and Test / build (pull_request) Successful in 1m37s
2026-05-03 11:03:19 +02:00
seeseemelk a2638b0c24 Update .gitea/workflows/build.yml
Deploy / build (push) Successful in 2m4s
2026-05-03 11:03:14 +02:00
seeseemelk e4a01db90d Merge branch 'master' into renovate/spring-boot 2026-05-03 11:02:02 +02:00
seeseemelk 5b6185b236 Merge pull request 'Update dependency org.eclipse.jgit:org.eclipse.jgit to v7.6.0.202603022253-r' (#2) from renovate/org.eclipse.jgit-org.eclipse.jgit-7.x into master
Deploy / build (push) Successful in 2m42s
Reviewed-on: #2
2026-05-02 23:03:29 +02:00
seeseemelk 3c7444675d Merge branch 'master' into renovate/org.eclipse.jgit-org.eclipse.jgit-7.x 2026-05-02 23:03:22 +02:00
seeseemelk 7610f5d97f Merge pull request 'Update Gradle to v9.5.0' (#4) from renovate/gradle-9.x into master
Deploy / build (push) Has been cancelled
Reviewed-on: #4
2026-05-02 23:03:13 +02:00
seeseemelk 53cb54c662 Merge branch 'master' into renovate/gradle-9.x 2026-05-02 23:03:07 +02:00
seeseemelk 82ed9fb77a Merge pull request 'Update actions/checkout action to v6' (#5) from renovate/actions-checkout-6.x into master
Deploy / build (push) Has been cancelled
Reviewed-on: #5
2026-05-02 23:03:01 +02:00
seeseemelk b9c637f0ba Merge branch 'master' into renovate/actions-checkout-6.x 2026-05-02 23:02:55 +02:00
seeseemelk fac9062f83 Merge pull request 'Update actions/setup-java action to v5' (#6) from renovate/actions-setup-java-5.x into master
Deploy / build (push) Has been cancelled
Reviewed-on: #6
2026-05-02 23:02:49 +02:00
Renovate a314b88eb3 Update actions/setup-java action to v5 2026-05-02 21:02:05 +00:00
Renovate b3974c15f2 Update actions/checkout action to v6 2026-05-02 21:02:04 +00:00
Renovate 768587ce57 Update Gradle to v9.5.0 2026-05-02 21:02:02 +00:00
Renovate 7c595ef022 Update dependency org.eclipse.jgit:org.eclipse.jgit to v7.6.0.202603022253-r 2026-05-02 20:59:17 +00:00
Renovate 270223dd9f Update plugin org.springframework.boot to v4.0.6 2026-05-02 20:59:16 +00:00
seeseemelk 0e8ab5f4ef Add renovate.json
Deploy / build (push) Has been cancelled
2026-05-02 22:58:10 +02:00
seeseemelk 4077c1b28e Log test results
Deploy / build (push) Successful in 1m41s
2026-02-27 22:02:13 +01:00
seeseemelk e6f6e2466b Add test step
Deploy / build (push) Successful in 1m4s
2026-02-27 21:56:51 +01:00
seeseemelk 5be1b1cc29 Throw exception on authentication issues 2026-02-27 21:54:30 +01:00
seeseemelk 36ecd019a8 Add copilot instructions 2026-02-27 21:53:56 +01:00
12 changed files with 226 additions and 36 deletions
+7 -7
View File
@@ -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
+5 -2
View File
@@ -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 .
+26
View File
@@ -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
View File
@@ -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 {
Binary file not shown.
+3 -1
View File
@@ -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
Vendored
+1 -1
View File
@@ -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
View File
@@ -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
+6
View File
@@ -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"));
}
} }