| 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 getCommits( | |
| 96 | Course course, String owner, String repo, String branch, int first, String after) | |
| 97 | throws JsonProcessingException, | |
| 98 | NoSuchAlgorithmException, | |
| 99 | InvalidKeySpecException, | |
| 100 | NoLinkedOrganizationException { | |
| 101 |
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); |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * Retrieves the commit history for a specified branch of a GitHub repository within a given time | |
| 106 | * range. | |
| 107 | * | |
| 108 | * @param course The course entity, used to fetch the associated GitHub installation token. | |
| 109 | * @param owner The owner of the GitHub repository. | |
| 110 | * @param repo The name of the GitHub repository. | |
| 111 | * @param branch The branch of the repository for which the commit history is retrieved. | |
| 112 | * @param since The start time for fetching commits (inclusive). Optional. Can be null. | |
| 113 | * @param until The end time for fetching commits (exclusive). Optional. Can be null. | |
| 114 | * @param size The maximum number of commits to retrieve in one request. | |
| 115 | * @param cursor The pagination cursor pointing to the start of the commit history to fetch. | |
| 116 | * Optional. Can be null. | |
| 117 | * @return A JSON string representing the commit history and associated metadata. | |
| 118 | * @throws NoLinkedOrganizationException If no linked organization exists for the specified | |
| 119 | * course. | |
| 120 | */ | |
| 121 | public String getCommits( | |
| 122 | Course course, | |
| 123 | String owner, | |
| 124 | String repo, | |
| 125 | String branch, | |
| 126 | Instant since, | |
| 127 | Instant until, | |
| 128 | int size, | |
| 129 | String cursor) | |
| 130 | throws JsonProcessingException, | |
| 131 | NoSuchAlgorithmException, | |
| 132 | InvalidKeySpecException, | |
| 133 | NoLinkedOrganizationException { | |
| 134 | String githubToken = jwtService.getInstallationToken(course); | |
| 135 | // language=GraphQL | |
| 136 | String query = | |
| 137 | """ | |
| 138 | query GetBranchCommits($owner: String!, $repo: String!, $branch: String!, $first: Int!, $after: String, $since: GitTimestamp, $until: GitTimestamp) { | |
| 139 | repository(owner: $owner, name: $repo) { | |
| 140 | ref(qualifiedName: $branch) { | |
| 141 | target { | |
| 142 | ... on Commit { | |
| 143 | history(first: $first, after: $after, since: $since, until: $until) { | |
| 144 | pageInfo { | |
| 145 | hasNextPage | |
| 146 | endCursor | |
| 147 | } | |
| 148 | edges { | |
| 149 | node { | |
| 150 | oid | |
| 151 | url | |
| 152 | messageHeadline | |
| 153 | committedDate | |
| 154 | author { | |
| 155 | name | |
| 156 | email | |
| 157 | user { | |
| 158 | login | |
| 159 | } | |
| 160 | } | |
| 161 | committer { | |
| 162 | name | |
| 163 | email | |
| 164 | user { | |
| 165 | login | |
| 166 | } | |
| 167 | } | |
| 168 | } | |
| 169 | } | |
| 170 | } | |
| 171 | } | |
| 172 | } | |
| 173 | } | |
| 174 | } | |
| 175 | } | |
| 176 | """; | |
| 177 | ||
| 178 | GraphQlResponse response = | |
| 179 | graphQlClient | |
| 180 | .mutate() | |
| 181 | .header("Authorization", "Bearer " + githubToken) | |
| 182 | .build() | |
| 183 | .document(query) | |
| 184 | .variable("owner", owner) | |
| 185 | .variable("repo", repo) | |
| 186 | .variable("branch", branch) | |
| 187 | .variable("first", size) | |
| 188 | .variable("after", cursor) | |
| 189 | .variable("since", since) | |
| 190 | .variable("until", until) | |
| 191 | .executeSync(); | |
| 192 | ||
| 193 | Map<String, Object> data = response.getData(); | |
| 194 | String jsonData = jacksonObjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data); | |
| 195 |
1
1. getCommits : replaced return value with "" for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::getCommits → KILLED |
return jsonData; |
| 196 | } | |
| 197 | ||
| 198 | public void downloadCommitHistory(DownloadRequest downloadRequest) | |
| 199 | throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException { | |
| 200 | String pointer = null; | |
| 201 | boolean hasNextPage; | |
| 202 | List<DownloadedCommit> downloadedCommits = new ArrayList<>(4000); | |
| 203 | do { | |
| 204 | JsonNode currentPage = | |
| 205 | jacksonObjectMapper.readTree( | |
| 206 | getCommits( | |
| 207 | downloadRequest.getCourse(), | |
| 208 | downloadRequest.getOrg(), | |
| 209 | downloadRequest.getRepo(), | |
| 210 | downloadRequest.getBranch(), | |
| 211 | downloadRequest.getStartDate(), | |
| 212 | downloadRequest.getEndDate(), | |
| 213 | 100, | |
| 214 | pointer)); | |
| 215 | pointer = | |
| 216 | currentPage | |
| 217 | .path("repository") | |
| 218 | .path("ref") | |
| 219 | .path("target") | |
| 220 | .path("history") | |
| 221 | .path("pageInfo") | |
| 222 | .path("endCursor") | |
| 223 | .asText(); | |
| 224 | hasNextPage = | |
| 225 | currentPage | |
| 226 | .path("repository") | |
| 227 | .path("ref") | |
| 228 | .path("target") | |
| 229 | .path("history") | |
| 230 | .path("pageInfo") | |
| 231 | .path("hasNextPage") | |
| 232 | .asBoolean(); | |
| 233 | JsonNode commits = | |
| 234 | currentPage.path("repository").path("ref").path("target").path("history").path("edges"); | |
| 235 | for (JsonNode node : commits) { | |
| 236 | DownloadedCommit newCommit = | |
| 237 | jacksonObjectMapper.treeToValue(node.get("node"), DownloadedCommit.class); | |
| 238 |
1
1. downloadCommitHistory : removed call to edu/ucsb/cs156/frontiers/entities/DownloadedCommit::setRequest → KILLED |
newCommit.setRequest(downloadRequest); |
| 239 | downloadedCommits.add(newCommit); | |
| 240 | } | |
| 241 | ||
| 242 |
1
1. downloadCommitHistory : negated conditional → KILLED |
} while (hasNextPage); |
| 243 | downloadedCommitRepository.saveAll(downloadedCommits); | |
| 244 | } | |
| 245 | } | |
Mutations | ||
| 83 |
1.1 |
|
| 101 |
1.1 |
|
| 195 |
1.1 |
|
| 238 |
1.1 |
|
| 242 |
1.1 |