gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Commits

✨ (api): Add Gitea-compatible branch endpoint

f8ef052c7f804956defc0e579db5e0a3b589b97f · Michael Hainz · 2026-07-21T11:25:02Z

Changes

5 files changed, +177 -1

MODIFY README.md +1 -0
diff --git a/README.md b/README.md
index ef44ccc..7b4b93e 100644
--- a/README.md
+++ b/README.md
@@ -145,6 +145,7 @@
145 145 | GET | `/api/v1/repos` | Repositories visible to the caller (Gitea repository objects) |
146 146 | POST | `/api/v1/repos` | Create a repository (`400` invalid name, `409` duplicate) |
147 147 | GET | `/api/v1/repos/{owner}/{name}` | Repository detail in Gitea shape (`full_name`, `default_branch`, `clone_url`, `permissions`, …) |
148 +| GET | `/api/v1/repos/{owner}/{name}/branches/{branch}` | Branch tip in Gitea shape (`name`, `commit.id`, `protected`); slash-bearing names supported |
148 149 | DELETE | `/api/v1/repos/{owner}/{name}` | Delete a repository (owner only) |
149 150 | GET, POST | `/api/v1/repos/{owner}/{name}/issues` | List / create issues |
150 151 | GET, PATCH, DELETE | `/api/v1/repos/{owner}/{name}/issues/{number}` | Get / update status / delete an issue |
MODIFY docs/maintainers/gitea-api.md +3 -1
diff --git a/docs/maintainers/gitea-api.md b/docs/maintainers/gitea-api.md
index 1d7c546..2cb873f 100644
--- a/docs/maintainers/gitea-api.md
+++ b/docs/maintainers/gitea-api.md
@@ -63,10 +63,12 @@
63 63 - `GET /api/v1/repos` and `GET /api/v1/repos/{owner}/{name}` — Gitea repository
64 64 objects, including `default_branch` (live git read), `clone_url`/`html_url`
65 65 (from the request base URL), `permissions`, `fork`/`parent`, and merge flags.
66 +- `GET /api/v1/repos/{owner}/{name}/branches/{branch}` — branch object
67 + (`name`, `commit.id`, `protected`); the branch segment is matched greedily so
68 + slash-bearing names resolve, and only real branch refs count (tag/SHA → 404).
66 69
67 70 ## What still needs to be implemented
68 71
69 -- `GET /api/v1/repos/{owner}/{name}/branches/{branch}` — branch object.
70 72 - `pulls` resource: rename `merge-requests` → `pulls`, reshape to Gitea pull
71 73 requests, add find-by-branch (`GET pulls/{base}/{head}`), `PATCH pulls/{index}`
72 74 (title/body/state), keep create/get/list/merge.
MODIFY src/main/java/de/workaround/api/ApiModels.java +11 -0
diff --git a/src/main/java/de/workaround/api/ApiModels.java b/src/main/java/de/workaround/api/ApiModels.java
index 85fb9c9..ce23284 100644
--- a/src/main/java/de/workaround/api/ApiModels.java
+++ b/src/main/java/de/workaround/api/ApiModels.java
@@ -142,6 +142,17 @@
142 142 }
143 143
144 144 /**
145 + * A branch in the Gitea contract. Clients (Renovate) read {@code commit.id} as the branch tip SHA.
146 + * git-shark has no branch protection, so {@code protected} is always false.
147 + */
148 + public record BranchView(String name, CommitRef commit, @JsonProperty("protected") boolean isProtected)
149 + {
150 + public record CommitRef(String id)
151 + {
152 + }
153 + }
154 +
155 + /**
145 156 * A person in a listing where the caller's read access is unknown (search is open to anonymous callers).
146 157 * Deliberately omits {@code email}: unlike the self-scoped {@code /api/v1/user}, a search response would
147 158 * otherwise disclose every matched user's address to unauthenticated callers.
ADD src/main/java/de/workaround/api/BranchApiResource.java +57 -0
diff --git a/src/main/java/de/workaround/api/BranchApiResource.java b/src/main/java/de/workaround/api/BranchApiResource.java
new file mode 100644
index 0000000..63d53af
--- /dev/null
+++ b/src/main/java/de/workaround/api/BranchApiResource.java
@@ -0,0 +1,57 @@
1 +package de.workaround.api;
2 +
3 +import de.workaround.git.AccessPolicy;
4 +import de.workaround.git.GitBrowseService;
5 +import de.workaround.git.GitRepositoryService;
6 +import de.workaround.model.Repository;
7 +import jakarta.inject.Inject;
8 +import jakarta.ws.rs.GET;
9 +import jakarta.ws.rs.NotFoundException;
10 +import jakarta.ws.rs.Path;
11 +import jakarta.ws.rs.PathParam;
12 +import jakarta.ws.rs.Produces;
13 +import jakarta.ws.rs.core.MediaType;
14 +
15 +/**
16 + * The Gitea {@code GET /api/v1/repos/{owner}/{name}/branches/{branch}} endpoint. Renovate reads the returned
17 + * {@code commit.id} as the branch tip SHA. The branch segment is matched greedily so slash-bearing names
18 + * (e.g. {@code renovate/foo}) resolve; only real branch refs count, so a tag or raw SHA yields 404. Reads
19 + * follow repository visibility (private repos hidden as 404).
20 + */
21 +@Path("/api/v1/repos/{owner}/{name}/branches")
22 +@Produces(MediaType.APPLICATION_JSON)
23 +public class BranchApiResource
24 +{
25 + @Inject
26 + GitRepositoryService repositories;
27 +
28 + @Inject
29 + GitBrowseService browse;
30 +
31 + @Inject
32 + AccessPolicy accessPolicy;
33 +
34 + @Inject
35 + ApiPrincipal principal;
36 +
37 + @GET
38 + @Path("{branch:.+}")
39 + public ApiModels.BranchView get(@PathParam("owner") String owner, @PathParam("name") String name,
40 + @PathParam("branch") String branch)
41 + {
42 + Repository repo = requireReadable(owner, name);
43 + GitBrowseService.CommitInfo tip = browse.commit(repositories.repositoryPath(repo), "refs/heads/" + branch)
44 + .orElseThrow(NotFoundException::new);
45 + return new ApiModels.BranchView(branch, new ApiModels.BranchView.CommitRef(tip.id()), false);
46 + }
47 +
48 + private Repository requireReadable(String owner, String name)
49 + {
50 + Repository repo = repositories.find(owner, name).orElseThrow(NotFoundException::new);
51 + if (!accessPolicy.canRead(principal.orNull(), repo))
52 + {
53 + throw new NotFoundException();
54 + }
55 + return repo;
56 + }
57 +}
ADD src/test/java/de/workaround/api/BranchApiTest.java +105 -0
diff --git a/src/test/java/de/workaround/api/BranchApiTest.java b/src/test/java/de/workaround/api/BranchApiTest.java
new file mode 100644
index 0000000..0f01953
--- /dev/null
+++ b/src/test/java/de/workaround/api/BranchApiTest.java
@@ -0,0 +1,105 @@
1 +package de.workaround.api;
2 +
3 +import java.nio.charset.StandardCharsets;
4 +import java.util.Map;
5 +import java.util.UUID;
6 +
7 +import org.junit.jupiter.api.Test;
8 +
9 +import de.workaround.git.GitRepositoryService;
10 +import de.workaround.git.GitTestSeeder;
11 +import de.workaround.model.Repository;
12 +import de.workaround.model.User;
13 +import io.quarkus.test.junit.QuarkusTest;
14 +import jakarta.inject.Inject;
15 +import jakarta.transaction.Transactional;
16 +
17 +import static io.restassured.RestAssured.given;
18 +import static org.hamcrest.CoreMatchers.equalTo;
19 +import static org.hamcrest.CoreMatchers.is;
20 +import static org.hamcrest.text.MatchesPattern.matchesPattern;
21 +
22 +@QuarkusTest
23 +class BranchApiTest
24 +{
25 + @Inject
26 + GitRepositoryService service;
27 +
28 + @Inject
29 + User.Repo userRepo;
30 +
31 + @Test
32 + void getBranchReturnsGiteaShapeWithCommitId() throws Exception
33 + {
34 + User owner = persistUser("api-branch-" + shortId());
35 + Repository repo = service.create(owner, "brepo", Repository.Visibility.PUBLIC, null);
36 + GitTestSeeder.seed(service.repositoryPath(repo),
37 + Map.of("f.txt", "x\n".getBytes(StandardCharsets.UTF_8)));
38 +
39 + given().when().get("/api/v1/repos/" + owner.username + "/brepo/branches/main")
40 + .then().statusCode(200)
41 + .body("name", equalTo("main"))
42 + .body("commit.id", matchesPattern("[0-9a-f]{40}"))
43 + .body("protected", is(false));
44 + }
45 +
46 + @Test
47 + void branchNameWithSlashResolves() throws Exception
48 + {
49 + User owner = persistUser("api-branch-slash-" + shortId());
50 + Repository repo = service.create(owner, "brepo", Repository.Visibility.PUBLIC, null);
51 + GitTestSeeder.seed(service.repositoryPath(repo),
52 + Map.of("f.txt", "x\n".getBytes(StandardCharsets.UTF_8)));
53 + GitTestSeeder.seedBranch(service.repositoryPath(repo), "feat/x",
54 + Map.of("g.txt", "y\n".getBytes(StandardCharsets.UTF_8)));
55 +
56 + given().when().get("/api/v1/repos/" + owner.username + "/brepo/branches/feat/x")
57 + .then().statusCode(200)
58 + .body("name", equalTo("feat/x"))
59 + .body("commit.id", matchesPattern("[0-9a-f]{40}"));
60 + }
61 +
62 + @Test
63 + void unknownBranchIs404() throws Exception
64 + {
65 + User owner = persistUser("api-branch-missing-" + shortId());
66 + Repository repo = service.create(owner, "brepo", Repository.Visibility.PUBLIC, null);
67 + GitTestSeeder.seed(service.repositoryPath(repo),
68 + Map.of("f.txt", "x\n".getBytes(StandardCharsets.UTF_8)));
69 +
70 + given().when().get("/api/v1/repos/" + owner.username + "/brepo/branches/nope")
71 + .then().statusCode(404);
72 + }
73 +
74 + @Test
75 + void branchOfPrivateRepoIsHiddenFromStrangers() throws Exception
76 + {
77 + User owner = persistUser("api-branch-priv-" + shortId());
78 + Repository repo = service.create(owner, "secret", Repository.Visibility.PRIVATE, null);
79 + GitTestSeeder.seed(service.repositoryPath(repo),
80 + Map.of("f.txt", "x\n".getBytes(StandardCharsets.UTF_8)));
81 +
82 + given().when().get("/api/v1/repos/" + owner.username + "/secret/branches/main")
83 + .then().statusCode(404);
84 + }
85 +
86 + private static String shortId()
87 + {
88 + return UUID.randomUUID().toString().substring(0, 8);
89 + }
90 +
91 + @Transactional
92 + User persistUser(String name)
93 + {
94 + User existing = userRepo.findByOidcSubOptional(name).orElse(null);
95 + if (existing != null)
96 + {
97 + return existing;
98 + }
99 + User user = new User();
100 + user.oidcSub = name;
101 + user.username = name;
102 + user.persist();
103 + return user;
104 + }
105 +}

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog