gitshark

Clone repository

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

← Commits

✨ (api): Reshape repository responses to the Gitea repository object

956ca2a519320510152fda9e6dec12e0e4412396 · Michael Hainz · 2026-07-21T11:20:00Z

Changes

9 files changed, +203 -35

MODIFY README.md +2 -2
diff --git a/README.md b/README.md
index b98b446..ef44ccc 100644
--- a/README.md
+++ b/README.md
@@ -142,9 +142,9 @@
142 142 |---|---|---|
143 143 | GET | `/api/v1/version` | Emulated Gitea version (anonymous; gates Gitea-client feature use) |
144 144 | GET | `/api/v1/user` | The token owner in Gitea user shape (`401` without a valid token) |
145 -| GET | `/api/v1/repos` | Repositories visible to the caller |
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 -| GET | `/api/v1/repos/{owner}/{name}` | Repository detail |
147 +| GET | `/api/v1/repos/{owner}/{name}` | Repository detail in Gitea shape (`full_name`, `default_branch`, `clone_url`, `permissions`, …) |
148 148 | DELETE | `/api/v1/repos/{owner}/{name}` | Delete a repository (owner only) |
149 149 | GET, POST | `/api/v1/repos/{owner}/{name}/issues` | List / create issues |
150 150 | GET, PATCH, DELETE | `/api/v1/repos/{owner}/{name}/issues/{number}` | Get / update status / delete an issue |
MODIFY docs/README.md +3 -0
diff --git a/docs/README.md b/docs/README.md
index 8e5f84d..53f72cd 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -89,6 +89,9 @@
89 89 - **[CI/CD runner protocol](maintainers/ci-runners.md)** — how the Forgejo/Gitea
90 90 `runner.v1` server side is built (Connect-over-JAX-RS, protobuf codegen), the
91 91 decisions behind it, and the works/gaps list toward full workflow execution.
92 +- **[Gitea-compatible REST API](maintainers/gitea-api.md)** — the migration of
93 + `/api/v1` to the Gitea contract (so Renovate/`tea` can drive git-shark), the
94 + component map, key decisions, and the works/gaps list.
92 95
93 96 ## Where else to look
94 97
ADD docs/maintainers/gitea-api.md +80 -0
diff --git a/docs/maintainers/gitea-api.md b/docs/maintainers/gitea-api.md
new file mode 100644
index 0000000..1d7c546
--- /dev/null
+++ b/docs/maintainers/gitea-api.md
@@ -0,0 +1,80 @@
1 +# Gitea-compatible REST API
2 +
3 +git-shark's `/api/v1` is being migrated from a bespoke JSON shape to the
4 +**Gitea REST contract**, so Gitea-ecosystem tooling — [Renovate](https://docs.renovatebot.com/)
5 +first, then `tea`, `act_runner`, and assorted bots — can drive a git-shark
6 +instance with its stock `gitea`/`forgejo` platform driver and no git-shark-specific
7 +code. Tracked in issue #20.
8 +
9 +## Why migrate rather than run a shim
10 +
11 +The REST API has no external consumers yet, so reshaping it in place is a rename,
12 +not a breaking change — the cheapest possible moment to do it. A parallel
13 +Gitea shim next to the bespoke API would fight the existing routes for the same
14 +paths (`GET /api/v1/repos/{owner}/{name}` wants one JSON body, not two) and leave
15 +two schemas to maintain forever. One Gitea-native schema is less code and broader
16 +compatibility.
17 +
18 +The domain layer keeps its own vocabulary: internally these are **merge requests**
19 +(`MergeRequest*`), and only the wire speaks Gitea's "pull request". The mapping is
20 +confined to the DTO/resource layer in `de.workaround.api`.
21 +
22 +## Component map
23 +
24 +| Concern | Type | Notes |
25 +|---|---|---|
26 +| Auth | `ApiTokenAuthFilter` | Accepts both `Authorization: Bearer <PAT>` and the Gitea-style `Authorization: token <PAT>`; same personal access tokens as git-over-HTTP |
27 +| DTOs | `ApiModels` | All response records are Gitea-shaped; snake_case fields via `@JsonProperty`. **Shared with the MCP tools** — reshaping the REST body reshapes MCP tool output too (accepted; see decisions) |
28 +| Surrogate ids | `GiteaIds` | Folds a `UUID` PK into a stable non-negative `long` for Gitea's int64 `id` |
29 +| Version probe | `VersionApiResource` | `GET /api/v1/version`; string from `gitshark.gitea-api.version` |
30 +| Repositories | `RepositoryApiResource` | Gitea repository object incl. `owner`, `full_name`, `default_branch`, `clone_url`, `html_url`, `permissions`, merge flags |
31 +| User | `UserApiResource` | Self identity in Gitea user shape |
32 +| Search | `SearchApiResource` | git-shark-specific (not a Gitea endpoint); returns the email-free `PersonView` and a shallow repository projection |
33 +
34 +## Key decisions
35 +
36 +- **Reported version is deliberately low** (`gitshark.gitea-api.version`, default
37 + `1.13.0`). Gitea clients gate feature calls on it: `>= 1.14.0` makes Renovate
38 + call `requested_reviewers`, `>= 1.24.0` unlocks `delete_branch_after_merge` —
39 + neither is implemented, so the version is kept below them until they are. Raise
40 + it as capabilities land.
41 +- **Surrogate `id` is one-way.** `GiteaIds.of(uuid)` is a display value only;
42 + git-shark never looks an entity up by it — owner/name and per-repo `number`
43 + (Gitea's `index`) remain the real keys. The fold is lossy; do not reverse it.
44 +- **MCP shares the DTOs.** MCP tools return `ApiModels` records directly, so they
45 + now emit Gitea-shaped JSON. Accepted (option (a) in the spike): the shape is
46 + richer, not worse, for AI-agent consumers. MCP has no request base URL, so the
47 + repository `clone_url`/`html_url` are null there while the REST resource fills
48 + them from the request's external base URI.
49 +- **Fields git-shark has no feature for are hard-coded:** `archived` and `mirror`
50 + are always false (no archive feature; push-mirrors are outbound, not incoming
51 + mirrors), and the `allow_*` merge flags advertise merge commits only, matching
52 + the one merge strategy the merge service implements.
53 +- **PII:** the self-scoped `UserView` carries `email`; the search `PersonView`
54 + omits it, because search is anonymous and would otherwise disclose every
55 + matched user's address.
56 +
57 +## What works today
58 +
59 +- `Authorization: token <PAT>` scheme alongside `Bearer`.
60 +- `GET /api/v1/version` — Gitea version probe.
61 +- `GET /api/v1/user` — self identity in Gitea user shape (`id`, `login`,
62 + `username`, `full_name`, `email`).
63 +- `GET /api/v1/repos` and `GET /api/v1/repos/{owner}/{name}` — Gitea repository
64 + objects, including `default_branch` (live git read), `clone_url`/`html_url`
65 + (from the request base URL), `permissions`, `fork`/`parent`, and merge flags.
66 +
67 +## What still needs to be implemented
68 +
69 +- `GET /api/v1/repos/{owner}/{name}/branches/{branch}` — branch object.
70 +- `pulls` resource: rename `merge-requests` → `pulls`, reshape to Gitea pull
71 + requests, add find-by-branch (`GET pulls/{base}/{head}`), `PATCH pulls/{index}`
72 + (title/body/state), keep create/get/list/merge.
73 +- `GET labels` → `[]` stub and commit-status stubs (`POST /statuses/{sha}`,
74 + `GET /commits/{ref}/statuses`) so Renovate proceeds.
75 +- `GET /repos/{owner}/{name}/contents/{path}` (Renovate mostly clones, so low
76 + priority).
77 +- Issue open/closed mapping + issue-comment REST endpoints (dependency dashboard);
78 + deferred — run Renovate with `dependencyDashboard: false`.
79 +- A real Renovate `LOG_LEVEL=debug` end-to-end run to validate JSON fidelity
80 + (field-name mismatches fail silently).
MODIFY src/main/java/de/workaround/api/ApiModels.java +46 -12
diff --git a/src/main/java/de/workaround/api/ApiModels.java b/src/main/java/de/workaround/api/ApiModels.java
index 9ebb054..85fb9c9 100644
--- a/src/main/java/de/workaround/api/ApiModels.java
+++ b/src/main/java/de/workaround/api/ApiModels.java
@@ -25,25 +25,59 @@
25 25
26 26 // -- responses --
27 27
28 - public record RepositoryView(String owner, String name, Repository.Visibility visibility, String description,
29 - Instant createdAt, String parentOwner, String parentName)
28 + /** A repository owner (user or organisation) as Gitea's nested owner object; clients read {@code login}. */
29 + public record OwnerView(long id, String login, String username)
30 30 {
31 - /** Projection with the parent hidden — safe default for listings where the viewer's read access is unknown. */
32 - public static RepositoryView of(Repository repo)
31 + public static OwnerView of(Repository repo)
33 32 {
34 - return of(repo, false);
33 + return new OwnerView(GiteaIds.of(repo.ownerId()), repo.ownerHandle(), repo.ownerHandle());
34 + }
35 + }
36 +
37 + /** The caller's effective rights on a repository; Gitea clients gate writes (e.g. automerge) on {@code push}. */
38 + public record PermissionsView(boolean admin, boolean push, boolean pull)
39 + {
40 + }
41 +
42 + /**
43 + * A repository in the Gitea contract. git-shark has no archive feature and its push-mirrors are outbound,
44 + * so {@code archived}/{@code mirror} are always false; only merge-commit merges are implemented, so the
45 + * {@code allow_*} flags advertise merge commits only. {@code cloneUrl}/{@code htmlUrl}/{@code defaultBranch}
46 + * are supplied by the caller because they need the request's external base URL and a live git read.
47 + */
48 + public record RepositoryView(long id, String name, @JsonProperty("full_name") String fullName, OwnerView owner,
49 + String description, @JsonProperty("private") boolean isPrivate, boolean fork, boolean mirror,
50 + boolean archived, boolean empty, @JsonProperty("default_branch") String defaultBranch,
51 + @JsonProperty("clone_url") String cloneUrl, @JsonProperty("html_url") String htmlUrl, RepositoryView parent,
52 + PermissionsView permissions, @JsonProperty("allow_merge_commits") boolean allowMergeCommits,
53 + @JsonProperty("allow_rebase") boolean allowRebase, @JsonProperty("allow_squash_merge") boolean allowSquashMerge,
54 + Instant createdAt)
55 + {
56 + /**
57 + * A shallow projection used only as a fork's {@code parent}: DB-derived fields only, no git read or URLs.
58 + * Its own parent is always null so the shape never recurses.
59 + */
60 + public static RepositoryView shallow(Repository repo)
61 + {
62 + return new RepositoryView(GiteaIds.of(repo.id), repo.name, repo.ownerHandle() + "/" + repo.name,
63 + OwnerView.of(repo), repo.description, repo.visibility == Repository.Visibility.PRIVATE,
64 + repo.parent != null, false, false, false, null, null, null, null, null, true, false, false,
65 + repo.createdAt);
35 66 }
36 67
37 68 /**
38 - * @param showParent whether the caller may see the fork's parent; when false (or the repo is not a
39 - * fork) {@code parentOwner}/{@code parentName} are null, so a source turned private is never
40 - * disclosed through its forks.
69 + * @param showParent whether the caller may see this fork's parent; when false (or the repo is not a
70 + * fork) {@code parent} is null, so a source turned private is never disclosed through its forks.
41 71 */
42 - public static RepositoryView of(Repository repo, boolean showParent)
72 + public static RepositoryView of(Repository repo, boolean showParent, boolean empty, String defaultBranch,
73 + String cloneUrl, String htmlUrl, PermissionsView permissions)
43 74 {
44 - boolean parent = showParent && repo.parent != null;
45 - return new RepositoryView(repo.ownerHandle(), repo.name, repo.visibility, repo.description,
46 - repo.createdAt, parent ? repo.parent.ownerHandle() : null, parent ? repo.parent.name : null);
75 + boolean fork = repo.parent != null;
76 + RepositoryView parent = showParent && fork ? shallow(repo.parent) : null;
77 + return new RepositoryView(GiteaIds.of(repo.id), repo.name, repo.ownerHandle() + "/" + repo.name,
78 + OwnerView.of(repo), repo.description, repo.visibility == Repository.Visibility.PRIVATE, fork, false,
79 + false, empty, defaultBranch, cloneUrl, htmlUrl, parent, permissions, true, false, false,
80 + repo.createdAt);
47 81 }
48 82 }
49 83
MODIFY src/main/java/de/workaround/api/RepositoryApiResource.java +31 -4
diff --git a/src/main/java/de/workaround/api/RepositoryApiResource.java b/src/main/java/de/workaround/api/RepositoryApiResource.java
index 9384f22..eeefdbf 100644
--- a/src/main/java/de/workaround/api/RepositoryApiResource.java
+++ b/src/main/java/de/workaround/api/RepositoryApiResource.java
@@ -4,6 +4,7 @@
4 4 import java.util.List;
5 5
6 6 import de.workaround.git.AccessPolicy;
7 +import de.workaround.git.GitBrowseService;
7 8 import de.workaround.git.GitRepositoryService;
8 9 import de.workaround.git.InvalidRepositoryNameException;
9 10 import de.workaround.git.RepositoryAlreadyExistsException;
@@ -18,8 +19,10 @@
18 19 import jakarta.ws.rs.Path;
19 20 import jakarta.ws.rs.PathParam;
20 21 import jakarta.ws.rs.Produces;
22 +import jakarta.ws.rs.core.Context;
21 23 import jakarta.ws.rs.core.MediaType;
22 24 import jakarta.ws.rs.core.Response;
25 +import jakarta.ws.rs.core.UriInfo;
23 26
24 27 /**
25 28 * JSON REST access to repositories under {@code /api/v1/repos}. Reads follow the same visibility rule as the
@@ -34,15 +37,21 @@
34 37 GitRepositoryService service;
35 38
36 39 @Inject
40 + GitBrowseService browse;
41 +
42 + @Inject
37 43 AccessPolicy accessPolicy;
38 44
39 45 @Inject
40 46 ApiPrincipal principal;
41 47
48 + @Context
49 + UriInfo uriInfo;
50 +
42 51 @GET
43 52 public List<ApiModels.RepositoryView> list()
44 53 {
45 - return service.listVisibleTo(principal.orNull()).stream().map(ApiModels.RepositoryView::of).toList();
54 + return service.listVisibleTo(principal.orNull()).stream().map(this::view).toList();
46 55 }
47 56
48 57 @POST
@@ -58,7 +67,7 @@
58 67 {
59 68 Repository repo = service.create(user, request.name(), visibility, description);
60 69 return Response.created(URI.create("/api/v1/repos/" + user.username + "/" + repo.name))
61 - .entity(ApiModels.RepositoryView.of(repo)).build();
70 + .entity(view(repo)).build();
62 71 }
63 72 catch (InvalidRepositoryNameException e)
64 73 {
@@ -77,7 +86,7 @@
77 86 public ApiModels.RepositoryView get(@PathParam("owner") String owner, @PathParam("name") String name)
78 87 {
79 88 Repository repo = requireReadable(owner, name);
80 - return ApiModels.RepositoryView.of(repo, canSeeParent(repo));
89 + return view(repo);
81 90 }
82 91
83 92 @POST
@@ -90,7 +99,7 @@
90 99 {
91 100 Repository forked = service.fork(user, source);
92 101 return Response.created(URI.create("/api/v1/repos/" + user.username + "/" + forked.name))
93 - .entity(ApiModels.RepositoryView.of(forked, canSeeParent(forked))).build();
102 + .entity(view(forked)).build();
94 103 }
95 104 catch (RepositoryAlreadyExistsException e)
96 105 {
@@ -110,6 +119,24 @@
110 119 return Response.noContent().build();
111 120 }
112 121
122 + /** Build the Gitea repository view, filling the fields that need a git read (default branch, emptiness),
123 + * the request's external base URL (clone/html URLs) and the caller's effective permissions. */
124 + private ApiModels.RepositoryView view(Repository repo)
125 + {
126 + java.nio.file.Path path = service.repositoryPath(repo);
127 + boolean empty = browse.isEmpty(path);
128 + String defaultBranch = browse.defaultBranch(path);
129 + String base = uriInfo.getBaseUri().toString();
130 + String handle = repo.ownerHandle();
131 + String cloneUrl = base + "git/" + handle + "/" + repo.name + ".git";
132 + String htmlUrl = base + "repos/" + handle + "/" + repo.name;
133 + User caller = principal.orNull();
134 + ApiModels.PermissionsView permissions = new ApiModels.PermissionsView(accessPolicy.canAdmin(caller, repo),
135 + accessPolicy.canWrite(caller, repo), accessPolicy.canRead(caller, repo));
136 + return ApiModels.RepositoryView.of(repo, canSeeParent(repo), empty, defaultBranch, cloneUrl, htmlUrl,
137 + permissions);
138 + }
139 +
113 140 /** Whether the caller may see this repo's fork parent — true only when it is a fork of a repo they can read. */
114 141 private boolean canSeeParent(Repository repo)
115 142 {
MODIFY src/main/java/de/workaround/api/SearchApiResource.java +1 -1
diff --git a/src/main/java/de/workaround/api/SearchApiResource.java b/src/main/java/de/workaround/api/SearchApiResource.java
index 51373f0..99da63b 100644
--- a/src/main/java/de/workaround/api/SearchApiResource.java
+++ b/src/main/java/de/workaround/api/SearchApiResource.java
@@ -29,7 +29,7 @@
29 29 {
30 30 SearchResults results = search.search(principal.orNull(), q);
31 31 return new ApiModels.SearchView(
32 - results.repositories().stream().map(ApiModels.RepositoryView::of).toList(),
32 + results.repositories().stream().map(ApiModels.RepositoryView::shallow).toList(),
33 33 results.persons().stream().map(ApiModels.PersonView::of).toList());
34 34 }
35 35 }
MODIFY src/main/java/de/workaround/mcp/RepositoryTools.java +20 -4
diff --git a/src/main/java/de/workaround/mcp/RepositoryTools.java b/src/main/java/de/workaround/mcp/RepositoryTools.java
index 396c9fd..eb14660 100644
--- a/src/main/java/de/workaround/mcp/RepositoryTools.java
+++ b/src/main/java/de/workaround/mcp/RepositoryTools.java
@@ -3,6 +3,7 @@
3 3 import java.util.List;
4 4
5 5 import de.workaround.api.ApiModels;
6 +import de.workaround.git.GitBrowseService;
6 7 import de.workaround.git.GitRepositoryService;
7 8 import de.workaround.model.Repository;
8 9 import de.workaround.model.User;
@@ -23,6 +24,9 @@
23 24 GitRepositoryService service;
24 25
25 26 @Inject
27 + GitBrowseService browse;
28 +
29 + @Inject
26 30 McpRepoAccess access;
27 31
28 32 @Inject
@@ -34,7 +38,7 @@
34 38 @Tool(description = "List repositories visible to the caller: all public repositories, plus your own private ones when a valid token is supplied.")
35 39 public List<ApiModels.RepositoryView> listRepositories()
36 40 {
37 - return service.listVisibleTo(principal.orNull()).stream().map(ApiModels.RepositoryView::of).toList();
41 + return service.listVisibleTo(principal.orNull()).stream().map(this::view).toList();
38 42 }
39 43
40 44 @Tool(description = "Get a single repository by owner username and repository name.")
@@ -42,7 +46,7 @@
42 46 @ToolArg(description = "Repository name") String name)
43 47 {
44 48 Repository repo = access.requireReadable(principal.orNull(), owner, name);
45 - return ApiModels.RepositoryView.of(repo, canSeeParent(repo));
49 + return view(repo);
46 50 }
47 51
48 52 @Tool(description = "Create a new repository owned by the authenticated user. Requires a personal access token.")
@@ -57,7 +61,7 @@
57 61 ? Repository.Visibility.PRIVATE : Repository.Visibility.PUBLIC;
58 62 Repository repo = service.create(user, name, parsed,
59 63 description == null || description.isBlank() ? null : description);
60 - return ApiModels.RepositoryView.of(repo);
64 + return view(repo);
61 65 }
62 66
63 67 @Tool(description = "Fork a repository you can read into your own namespace, copying all branches and tags. Requires a personal access token.")
@@ -68,7 +72,7 @@
68 72 User user = principal.require();
69 73 Repository source = access.requireReadable(user, owner, name);
70 74 Repository forked = service.fork(user, source);
71 - return ApiModels.RepositoryView.of(forked, canSeeParent(forked));
75 + return view(forked);
72 76 }
73 77
74 78 private boolean canSeeParent(Repository repo)
@@ -76,6 +80,18 @@
76 80 return repo.parent != null && accessPolicy.canRead(principal.orNull(), repo.parent);
77 81 }
78 82
83 + /** MCP has no request base URL, so clone/html URLs are left null; the git-derived fields and permissions
84 + * are still populated so AI clients see the same shape the REST API serves. */
85 + private ApiModels.RepositoryView view(Repository repo)
86 + {
87 + java.nio.file.Path path = service.repositoryPath(repo);
88 + User caller = principal.orNull();
89 + ApiModels.PermissionsView permissions = new ApiModels.PermissionsView(accessPolicy.canAdmin(caller, repo),
90 + accessPolicy.canWrite(caller, repo), accessPolicy.canRead(caller, repo));
91 + return ApiModels.RepositoryView.of(repo, canSeeParent(repo), browse.isEmpty(path),
92 + browse.defaultBranch(path), null, null, permissions);
93 + }
94 +
79 95 @Tool(description = "Delete a repository you own. Requires a personal access token. This is irreversible.")
80 96 @Transactional
81 97 public String deleteRepository(@ToolArg(description = "Owner username") String owner,
MODIFY src/test/java/de/workaround/api/ApiSmokeIT.java +4 -2
diff --git a/src/test/java/de/workaround/api/ApiSmokeIT.java b/src/test/java/de/workaround/api/ApiSmokeIT.java
index 8f18fe2..904ffa7 100644
--- a/src/test/java/de/workaround/api/ApiSmokeIT.java
+++ b/src/test/java/de/workaround/api/ApiSmokeIT.java
@@ -38,9 +38,11 @@
38 38 .then()
39 39 .statusCode(200)
40 40 .contentType("application/json")
41 - .body("owner", equalTo("alice"))
41 + .body("owner.login", equalTo("alice"))
42 42 .body("name", equalTo("demo"))
43 - .body("visibility", equalTo("PUBLIC"));
43 + .body("full_name", equalTo("alice/demo"))
44 + .body("private", org.hamcrest.CoreMatchers.is(false))
45 + .body("clone_url", org.hamcrest.Matchers.endsWith("/git/alice/demo.git"));
44 46 }
45 47
46 48 @Test
MODIFY src/test/java/de/workaround/api/RepositoryApiTest.java +16 -10
diff --git a/src/test/java/de/workaround/api/RepositoryApiTest.java b/src/test/java/de/workaround/api/RepositoryApiTest.java
index 58e0ffd..6ef23a9 100644
--- a/src/test/java/de/workaround/api/RepositoryApiTest.java
+++ b/src/test/java/de/workaround/api/RepositoryApiTest.java
@@ -35,22 +35,27 @@
35 35 User owner = persistUser("api-repo-owner");
36 36 String token = mintToken(owner);
37 37
38 - // create
38 + // create — response is in the Gitea repository shape
39 39 String location = given().header("Authorization", "Bearer " + token)
40 40 .contentType("application/json")
41 41 .body(Map.of("name", "widgets", "visibility", "PUBLIC", "description", "gadgets"))
42 42 .when().post("/api/v1/repos")
43 43 .then().statusCode(201)
44 44 .body("name", equalTo("widgets"))
45 - .body("owner", equalTo(owner.username))
46 - .body("visibility", equalTo("PUBLIC"))
45 + .body("full_name", equalTo(owner.username + "/widgets"))
46 + .body("owner.login", equalTo(owner.username))
47 + .body("private", is(false))
47 48 .body("description", equalTo("gadgets"))
49 + .body("default_branch", equalTo("main"))
50 + .body("clone_url", org.hamcrest.Matchers.endsWith("/git/" + owner.username + "/widgets.git"))
51 + .body("permissions.admin", is(true))
48 52 .extract().header("Location");
49 53
50 54 // get via Location
51 55 given().when().get(location)
52 56 .then().statusCode(200)
53 - .body("name", equalTo("widgets"));
57 + .body("name", equalTo("widgets"))
58 + .body("full_name", equalTo(owner.username + "/widgets"));
54 59
55 60 // list includes it (public, visible even anonymously)
56 61 given().when().get("/api/v1/repos")
@@ -144,10 +149,11 @@
144 149 given().header("Authorization", "Bearer " + token)
145 150 .when().post("/api/v1/repos/" + owner.username + "/tocopy/fork")
146 151 .then().statusCode(201)
147 - .body("owner", equalTo(forker.username))
152 + .body("owner.login", equalTo(forker.username))
148 153 .body("name", equalTo("tocopy"))
149 - .body("parentOwner", equalTo(owner.username))
150 - .body("parentName", equalTo("tocopy"));
154 + .body("fork", is(true))
155 + .body("parent.owner.login", equalTo(owner.username))
156 + .body("parent.name", equalTo("tocopy"));
151 157 }
152 158
153 159 @Test
@@ -161,7 +167,7 @@
161 167 given().header("Authorization", "Bearer " + token)
162 168 .when().post("/api/v1/repos/" + owner.username + "/flip/fork")
163 169 .then().statusCode(201)
164 - .body("parentOwner", equalTo(owner.username));
170 + .body("parent.owner.login", equalTo(owner.username));
165 171
166 172 // owner makes the source private; the forker can no longer read it
167 173 service.changeVisibility(owner, source, Repository.Visibility.PRIVATE);
@@ -169,8 +175,8 @@
169 175 given().header("Authorization", "Bearer " + token)
170 176 .when().get("/api/v1/repos/" + forker.username + "/flip")
171 177 .then().statusCode(200)
172 - .body("parentOwner", org.hamcrest.Matchers.nullValue())
173 - .body("parentName", org.hamcrest.Matchers.nullValue());
178 + .body("fork", is(true))
179 + .body("parent", org.hamcrest.Matchers.nullValue());
174 180 }
175 181
176 182 @Test

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog