| 1 | package edu.ucsb.cs156.frontiers.services; | |
| 2 | ||
| 3 | import com.fasterxml.jackson.core.JsonProcessingException; | |
| 4 | import com.fasterxml.jackson.databind.JsonNode; | |
| 5 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 6 | import edu.ucsb.cs156.frontiers.entities.Course; | |
| 7 | import edu.ucsb.cs156.frontiers.entities.DownloadRequest; | |
| 8 | import edu.ucsb.cs156.frontiers.entities.DownloadedCommit; | |
| 9 | import edu.ucsb.cs156.frontiers.errors.NoLinkedOrganizationException; | |
| 10 | import edu.ucsb.cs156.frontiers.repositories.DownloadedCommitRepository; | |
| 11 | import java.security.NoSuchAlgorithmException; | |
| 12 | import java.security.spec.InvalidKeySpecException; | |
| 13 | import java.time.Instant; | |
| 14 | import java.util.ArrayList; | |
| 15 | import java.util.List; | |
| 16 | import java.util.Map; | |
| 17 | import lombok.extern.slf4j.Slf4j; | |
| 18 | import org.springframework.graphql.GraphQlResponse; | |
| 19 | import org.springframework.graphql.client.HttpSyncGraphQlClient; | |
| 20 | import org.springframework.http.HttpHeaders; | |
| 21 | import org.springframework.http.MediaType; | |
| 22 | import org.springframework.stereotype.Service; | |
| 23 | import org.springframework.web.client.RestClient; | |
| 24 | ||
| 25 | @Service | |
| 26 | @Slf4j | |
| 27 | public class GithubGraphQLService { | |
| 28 | ||
| 29 | private final HttpSyncGraphQlClient graphQlClient; | |
| 30 | ||
| 31 | private final JwtService jwtService; | |
| 32 | ||
| 33 | private final String githubBaseUrl = "https://api.github.com/graphql"; | |
| 34 | ||
| 35 | private final ObjectMapper jacksonObjectMapper; | |
| 36 | private final DownloadedCommitRepository downloadedCommitRepository; | |
| 37 | ||
| 38 | public GithubGraphQLService( | |
| 39 | RestClient.Builder builder, | |
| 40 | JwtService jwtService, | |
| 41 | ObjectMapper jacksonObjectMapper, | |
| 42 | DownloadedCommitRepository downloadedCommitRepository) { | |
| 43 | this.jwtService = jwtService; | |
| 44 | this.graphQlClient = | |
| 45 | HttpSyncGraphQlClient.builder(builder.baseUrl(githubBaseUrl).build()) | |
| 46 | .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) | |
| 47 | .build(); | |
| 48 | this.jacksonObjectMapper = jacksonObjectMapper; | |
| 49 | this.downloadedCommitRepository = downloadedCommitRepository; | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Retrieves the name of the default branch for a given GitHub repository. | |
| 54 | * | |
| 55 | * @param owner The owner (username or organization) of the repository. | |
| 56 | * @param repo The name of the repository. | |
| 57 | * @return A Mono emitting the default branch name, or an empty Mono if not found. | |
| 58 | */ | |
| 59 | public String getDefaultBranchName(Course course, String owner, String repo) | |
| 60 | throws JsonProcessingException, | |
| 61 | NoSuchAlgorithmException, | |
| 62 | InvalidKeySpecException, | |
| 63 | NoLinkedOrganizationException { | |
| 64 | log.info( | |
| 65 | "getDefaultBranchName called with course.getId(): {} owner: {}, repo: {}", | |
| 66 | course.getId(), | |
| 67 | owner, | |
| 68 | repo); | |
| 69 | String githubToken = jwtService.getInstallationToken(course); | |
| 70 | ||
| 71 | // language=GraphQL | |
| 72 | String query = | |
| 73 | """ | |
| 74 | query getDefaultBranch($owner: String!, $repo: String!) { | |
| 75 | repository(owner: $owner, name: $repo) { | |
| 76 | defaultBranchRef { | |
| 77 | name | |
| 78 | } | |
| 79 | } | |
| 80 | } | |
| 81 | """; | |
| 82 | ||
| 83 |
1
1. getDefaultBranchName : replaced return value with "" for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::getDefaultBranchName → KILLED |
return graphQlClient |
| 84 | .mutate() | |
| 85 | .header("Authorization", "Bearer " + githubToken) | |
| 86 | .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) | |
| 87 | .build() | |
| 88 | .document(query) | |
| 89 | .variable("owner", owner) | |
| 90 | .variable("repo", repo) | |
| 91 | .retrieveSync("repository.defaultBranchRef.name") | |
| 92 | .toEntity(String.class); | |
| 93 | } | |
| 94 | ||
| 95 | public String getDefaultBasePermission(Course course) | |
| 96 | throws JsonProcessingException, | |
| 97 | NoSuchAlgorithmException, | |
| 98 | InvalidKeySpecException, | |
| 99 | NoLinkedOrganizationException { | |
| 100 | log.info("getDefaultBasePermission called with course.getId(): {}", course.getId()); | |
| 101 | String githubToken = jwtService.getInstallationToken(course); | |
| 102 | ||
| 103 | // language=GraphQL | |
| 104 | String query = | |
| 105 | """ | |
| 106 | query GetOrgDefaultPermission($login: String!) { | |
| 107 | organization(login: $login) { | |
| 108 | name | |
| 109 | defaultRepositoryPermission | |
| 110 | } | |
| 111 | } | |
| 112 | """; | |
| 113 | ||
| 114 |
1
1. getDefaultBasePermission : replaced return value with "" for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::getDefaultBasePermission → KILLED |
return graphQlClient |
| 115 | .mutate() | |
| 116 | .header("Authorization", "Bearer " + githubToken) | |
| 117 | .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) | |
| 118 | .build() | |
| 119 | .document(query) | |
| 120 | .variable("login", course.getOrgName()) | |
| 121 | .retrieveSync("organization.defaultRepositoryPermission") | |
| 122 | .toEntity(String.class); | |
| 123 | } | |
| 124 | ||
| 125 | public String getCommits( | |
| 126 | Course course, String owner, String repo, String branch, int first, String after) | |
| 127 | throws JsonProcessingException, | |
| 128 | NoSuchAlgorithmException, | |
| 129 | InvalidKeySpecException, | |
| 130 | NoLinkedOrganizationException { | |
| 131 |
1
1. getCommits : replaced return value with "" for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::getCommits → KILLED |
return getCommits(course, owner, repo, branch, null, null, first, after); |
| 132 | } | |
| 133 | ||
| 134 | /** | |
| 135 | * Retrieves the commit history for a specified branch of a GitHub repository within a given time | |
| 136 | * range. | |
| 137 | * | |
| 138 | * @param course The course entity, used to fetch the associated GitHub installation token. | |
| 139 | * @param owner The owner of the GitHub repository. | |
| 140 | * @param repo The name of the GitHub repository. | |
| 141 | * @param branch The branch of the repository for which the commit history is retrieved. | |
| 142 | * @param since The start time for fetching commits (inclusive). Optional. Can be null. | |
| 143 | * @param until The end time for fetching commits (exclusive). Optional. Can be null. | |
| 144 | * @param size The maximum number of commits to retrieve in one request. | |
| 145 | * @param cursor The pagination cursor pointing to the start of the commit history to fetch. | |
| 146 | * Optional. Can be null. | |
| 147 | * @return A JSON string representing the commit history and associated metadata. | |
| 148 | * @throws NoLinkedOrganizationException If no linked organization exists for the specified | |
| 149 | * course. | |
| 150 | */ | |
| 151 | public String getCommits( | |
| 152 | Course course, | |
| 153 | String owner, | |
| 154 | String repo, | |
| 155 | String branch, | |
| 156 | Instant since, | |
| 157 | Instant until, | |
| 158 | int size, | |
| 159 | String cursor) | |
| 160 | throws JsonProcessingException, | |
| 161 | NoSuchAlgorithmException, | |
| 162 | InvalidKeySpecException, | |
| 163 | NoLinkedOrganizationException { | |
| 164 | String githubToken = jwtService.getInstallationToken(course); | |
| 165 | // language=GraphQL | |
| 166 | String query = | |
| 167 | """ | |
| 168 | query GetBranchCommits($owner: String!, $repo: String!, $branch: String!, $first: Int!, $after: String, $since: GitTimestamp, $until: GitTimestamp) { | |
| 169 | repository(owner: $owner, name: $repo) { | |
| 170 | ref(qualifiedName: $branch) { | |
| 171 | target { | |
| 172 | ... on Commit { | |
| 173 | history(first: $first, after: $after, since: $since, until: $until) { | |
| 174 | pageInfo { | |
| 175 | hasNextPage | |
| 176 | endCursor | |
| 177 | } | |
| 178 | edges { | |
| 179 | node { | |
| 180 | oid | |
| 181 | url | |
| 182 | messageHeadline | |
| 183 | committedDate | |
| 184 | author { | |
| 185 | name | |
| 186 | email | |
| 187 | user { | |
| 188 | login | |
| 189 | } | |
| 190 | } | |
| 191 | committer { | |
| 192 | name | |
| 193 | email | |
| 194 | user { | |
| 195 | login | |
| 196 | } | |
| 197 | } | |
| 198 | } | |
| 199 | } | |
| 200 | } | |
| 201 | } | |
| 202 | } | |
| 203 | } | |
| 204 | } | |
| 205 | } | |
| 206 | """; | |
| 207 | ||
| 208 | GraphQlResponse response = | |
| 209 | graphQlClient | |
| 210 | .mutate() | |
| 211 | .header("Authorization", "Bearer " + githubToken) | |
| 212 | .build() | |
| 213 | .document(query) | |
| 214 | .variable("owner", owner) | |
| 215 | .variable("repo", repo) | |
| 216 | .variable("branch", branch) | |
| 217 | .variable("first", size) | |
| 218 | .variable("after", cursor) | |
| 219 | .variable("since", since) | |
| 220 | .variable("until", until) | |
| 221 | .executeSync(); | |
| 222 | ||
| 223 | Map<String, Object> data = response.getData(); | |
| 224 | String jsonData = jacksonObjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data); | |
| 225 |
1
1. getCommits : replaced return value with "" for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::getCommits → KILLED |
return jsonData; |
| 226 | } | |
| 227 | ||
| 228 | public void downloadCommitHistory(DownloadRequest downloadRequest) | |
| 229 | throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException { | |
| 230 | String pointer = null; | |
| 231 | boolean hasNextPage; | |
| 232 | List<DownloadedCommit> downloadedCommits = new ArrayList<>(4000); | |
| 233 | do { | |
| 234 | JsonNode currentPage = | |
| 235 | jacksonObjectMapper.readTree( | |
| 236 | getCommits( | |
| 237 | downloadRequest.getCourse(), | |
| 238 | downloadRequest.getOrg(), | |
| 239 | downloadRequest.getRepo(), | |
| 240 | downloadRequest.getBranch(), | |
| 241 | downloadRequest.getStartDate(), | |
| 242 | downloadRequest.getEndDate(), | |
| 243 | 100, | |
| 244 | pointer)); | |
| 245 | pointer = | |
| 246 | currentPage | |
| 247 | .path("repository") | |
| 248 | .path("ref") | |
| 249 | .path("target") | |
| 250 | .path("history") | |
| 251 | .path("pageInfo") | |
| 252 | .path("endCursor") | |
| 253 | .asText(); | |
| 254 | hasNextPage = | |
| 255 | currentPage | |
| 256 | .path("repository") | |
| 257 | .path("ref") | |
| 258 | .path("target") | |
| 259 | .path("history") | |
| 260 | .path("pageInfo") | |
| 261 | .path("hasNextPage") | |
| 262 | .asBoolean(); | |
| 263 | JsonNode commits = | |
| 264 | currentPage.path("repository").path("ref").path("target").path("history").path("edges"); | |
| 265 | for (JsonNode node : commits) { | |
| 266 | DownloadedCommit newCommit = | |
| 267 | jacksonObjectMapper.treeToValue(node.get("node"), DownloadedCommit.class); | |
| 268 |
1
1. downloadCommitHistory : removed call to edu/ucsb/cs156/frontiers/entities/DownloadedCommit::setRequest → KILLED |
newCommit.setRequest(downloadRequest); |
| 269 | downloadedCommits.add(newCommit); | |
| 270 | } | |
| 271 | ||
| 272 |
1
1. downloadCommitHistory : negated conditional → KILLED |
} while (hasNextPage); |
| 273 | downloadedCommitRepository.saveAll(downloadedCommits); | |
| 274 | } | |
| 275 | } | |
Mutations | ||
| 83 |
1.1 |
|
| 114 |
1.1 |
|
| 131 |
1.1 |
|
| 225 |
1.1 |
|
| 268 |
1.1 |
|
| 272 |
1.1 |