| 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.Branch; | |
| 7 | import edu.ucsb.cs156.frontiers.entities.BranchId; | |
| 8 | import edu.ucsb.cs156.frontiers.entities.Commit; | |
| 9 | import edu.ucsb.cs156.frontiers.entities.Course; | |
| 10 | import edu.ucsb.cs156.frontiers.errors.NoLinkedOrganizationException; | |
| 11 | import edu.ucsb.cs156.frontiers.repositories.BranchRepository; | |
| 12 | import edu.ucsb.cs156.frontiers.repositories.CommitRepository; | |
| 13 | import java.security.NoSuchAlgorithmException; | |
| 14 | import java.security.spec.InvalidKeySpecException; | |
| 15 | import java.time.Instant; | |
| 16 | import java.util.ArrayList; | |
| 17 | import java.util.HashMap; | |
| 18 | import java.util.List; | |
| 19 | import java.util.Map; | |
| 20 | import java.util.Optional; | |
| 21 | import lombok.extern.slf4j.Slf4j; | |
| 22 | import org.springframework.data.auditing.DateTimeProvider; | |
| 23 | import org.springframework.graphql.GraphQlResponse; | |
| 24 | import org.springframework.graphql.client.HttpSyncGraphQlClient; | |
| 25 | import org.springframework.http.HttpHeaders; | |
| 26 | import org.springframework.http.MediaType; | |
| 27 | import org.springframework.http.ResponseEntity; | |
| 28 | import org.springframework.stereotype.Service; | |
| 29 | import org.springframework.transaction.annotation.Propagation; | |
| 30 | import org.springframework.transaction.annotation.Transactional; | |
| 31 | import org.springframework.web.client.RestClient; | |
| 32 | ||
| 33 | @Service | |
| 34 | @Slf4j | |
| 35 | public class GithubGraphQLService { | |
| 36 | ||
| 37 | private final HttpSyncGraphQlClient graphQlClient; | |
| 38 | ||
| 39 | private final JwtService jwtService; | |
| 40 | ||
| 41 | private final String githubBaseUrl = "https://api.github.com/graphql"; | |
| 42 | ||
| 43 | private final DateTimeProvider dateTimeProvider; | |
| 44 | private final ObjectMapper jacksonObjectMapper; | |
| 45 | private final CommitRepository commitRepository; | |
| 46 | private final BranchRepository branchRepository; | |
| 47 | private final RestClient client; | |
| 48 | ||
| 49 | public GithubGraphQLService( | |
| 50 | RestClient.Builder builder, | |
| 51 | JwtService jwtService, | |
| 52 | DateTimeProvider dateTimeProvider, | |
| 53 | ObjectMapper jacksonObjectMapper, | |
| 54 | CommitRepository commitRepository, | |
| 55 | BranchRepository branchRepository) { | |
| 56 | this.jwtService = jwtService; | |
| 57 | this.graphQlClient = | |
| 58 | HttpSyncGraphQlClient.builder(builder.baseUrl(githubBaseUrl).build()) | |
| 59 | .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) | |
| 60 | .build(); | |
| 61 | this.dateTimeProvider = dateTimeProvider; | |
| 62 | this.jacksonObjectMapper = jacksonObjectMapper; | |
| 63 | this.commitRepository = commitRepository; | |
| 64 | this.branchRepository = branchRepository; | |
| 65 | this.client = builder.baseUrl("https://api.github.com/").build(); | |
| 66 | } | |
| 67 | ||
| 68 | /** | |
| 69 | * Retrieves the name of the default branch for a given GitHub repository. | |
| 70 | * | |
| 71 | * @param owner The owner (username or organization) of the repository. | |
| 72 | * @param repo The name of the repository. | |
| 73 | * @return A Mono emitting the default branch name, or an empty Mono if not found. | |
| 74 | */ | |
| 75 | public String getDefaultBranchName(Course course, String owner, String repo) | |
| 76 | throws JsonProcessingException, | |
| 77 | NoSuchAlgorithmException, | |
| 78 | InvalidKeySpecException, | |
| 79 | NoLinkedOrganizationException { | |
| 80 | log.info( | |
| 81 | "getDefaultBranchName called with course.getId(): {} owner: {}, repo: {}", | |
| 82 | course.getId(), | |
| 83 | owner, | |
| 84 | repo); | |
| 85 | String githubToken = jwtService.getInstallationToken(course); | |
| 86 | ||
| 87 | String query = | |
| 88 | """ | |
| 89 | query getDefaultBranch($owner: String!, $repo: String!) { | |
| 90 | repository(owner: $owner, name: $repo) { | |
| 91 | defaultBranchRef { | |
| 92 | name | |
| 93 | } | |
| 94 | } | |
| 95 | } | |
| 96 | """; | |
| 97 | ||
| 98 |
1
1. getDefaultBranchName : replaced return value with "" for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::getDefaultBranchName → KILLED |
return graphQlClient |
| 99 | .mutate() | |
| 100 | .header("Authorization", "Bearer " + githubToken) | |
| 101 | .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) | |
| 102 | .build() | |
| 103 | .document(query) | |
| 104 | .variable("owner", owner) | |
| 105 | .variable("repo", repo) | |
| 106 | .retrieveSync("repository.defaultBranchRef.name") | |
| 107 | .toEntity(String.class); | |
| 108 | } | |
| 109 | ||
| 110 | public String getCommits( | |
| 111 | Course course, String owner, String repo, String branch, int first, String after) | |
| 112 | throws JsonProcessingException, | |
| 113 | NoSuchAlgorithmException, | |
| 114 | InvalidKeySpecException, | |
| 115 | NoLinkedOrganizationException { | |
| 116 | log.info( | |
| 117 | "getCommits called with course.getId(): {} owner: {}, repo: {}, branch: {}, first: {}, after: {}", | |
| 118 | course.getId(), | |
| 119 | owner, | |
| 120 | repo, | |
| 121 | branch, | |
| 122 | first, | |
| 123 | after); | |
| 124 | String githubToken = jwtService.getInstallationToken(course); | |
| 125 | ||
| 126 | String query = | |
| 127 | """ | |
| 128 | query GetBranchCommits($owner: String!, $repo: String!, $branch: String!, $first: Int!, $after: String) { | |
| 129 | repository(owner: $owner, name: $repo) { | |
| 130 | ref(qualifiedName: $branch) { | |
| 131 | target { | |
| 132 | ... on Commit { | |
| 133 | history(first: $first, after: $after) { | |
| 134 | pageInfo { | |
| 135 | hasNextPage | |
| 136 | endCursor | |
| 137 | } | |
| 138 | edges { | |
| 139 | node { | |
| 140 | oid | |
| 141 | url | |
| 142 | messageHeadline | |
| 143 | committedDate | |
| 144 | author { | |
| 145 | name | |
| 146 | email | |
| 147 | user { | |
| 148 | login | |
| 149 | } | |
| 150 | } | |
| 151 | parents { | |
| 152 | totalCount | |
| 153 | } | |
| 154 | committer { | |
| 155 | name | |
| 156 | email | |
| 157 | user { | |
| 158 | login | |
| 159 | } | |
| 160 | } | |
| 161 | } | |
| 162 | } | |
| 163 | } | |
| 164 | } | |
| 165 | } | |
| 166 | } | |
| 167 | } | |
| 168 | } | |
| 169 | """; | |
| 170 | ||
| 171 | GraphQlResponse response = | |
| 172 | graphQlClient | |
| 173 | .mutate() | |
| 174 | .header("Authorization", "Bearer " + githubToken) | |
| 175 | .build() | |
| 176 | .document(query) | |
| 177 | .variable("owner", owner) | |
| 178 | .variable("repo", repo) | |
| 179 | .variable("branch", branch) | |
| 180 | .variable("first", first) | |
| 181 | .variable("after", after) | |
| 182 | .executeSync(); | |
| 183 | ||
| 184 | Map<String, Object> data = response.getData(); | |
| 185 | String jsonData = jacksonObjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data); | |
| 186 |
1
1. getCommits : replaced return value with "" for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::getCommits → KILLED |
return jsonData; |
| 187 | } | |
| 188 | ||
| 189 | /** | |
| 190 | * Loads the commit history for a specified repository branch. The method retrieves information | |
| 191 | * about the branch from the database, determines if the branch information is up to date, and | |
| 192 | * updates the commit history if necessary by fetching data from GitHub. If the branch does not | |
| 193 | * exist in the database, it is created. | |
| 194 | * | |
| 195 | * @param course the course to be authenticated against | |
| 196 | * @param branch the identifier of the branch to load commit history for | |
| 197 | * @return the {@code Branch} object representing the branch with its latest commit history | |
| 198 | * information | |
| 199 | * @throws Exception if an error occurs while loading or updating the commit history | |
| 200 | */ | |
| 201 | @Transactional(propagation = Propagation.NESTED) | |
| 202 | public Branch loadCommitHistory(Course course, BranchId branch) throws Exception { | |
| 203 | Instant retrievedTime = Instant.from(dateTimeProvider.getNow().get()); | |
| 204 | ||
| 205 | HashMap<String, Commit> existingCommits = new HashMap<>(4000); | |
| 206 | ||
| 207 | Branch selectedBranch; | |
| 208 | ||
| 209 | Optional<Branch> existingBranch = branchRepository.findById(branch); | |
| 210 | ||
| 211 |
1
1. loadCommitHistory : negated conditional → KILLED |
if (existingBranch.isPresent()) { |
| 212 | selectedBranch = existingBranch.get(); | |
| 213 | String currentHead = getMostRecentCommitSha(course, branch); | |
| 214 | log.info("Branch {} already exists in database", branch); | |
| 215 |
1
1. loadCommitHistory : negated conditional → KILLED |
if (commitRepository.existsByBranchAndSha(selectedBranch, currentHead)) { |
| 216 | log.info("Branch {} already exists in database and is up to date", branch); | |
| 217 |
1
1. loadCommitHistory : removed call to edu/ucsb/cs156/frontiers/entities/Branch::setRetrievedTime → KILLED |
selectedBranch.setRetrievedTime(retrievedTime); |
| 218 | branchRepository.save(selectedBranch); | |
| 219 |
1
1. loadCommitHistory : replaced return value with null for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::loadCommitHistory → KILLED |
return selectedBranch; |
| 220 | } else { | |
| 221 | log.info("Branch {} already exists in database but is out of date, updating", branch); | |
| 222 | commitRepository | |
| 223 | .streamByBranch(selectedBranch) | |
| 224 |
1
1. loadCommitHistory : removed call to java/util/stream/Stream::forEach → KILLED |
.forEach(commit -> existingCommits.put(commit.getSha(), commit)); |
| 225 | } | |
| 226 | } else { | |
| 227 | selectedBranch = Branch.builder().id(branch).build(); | |
| 228 | log.info("Branch {} does not exist in database, creating new branch", branch); | |
| 229 | } | |
| 230 |
1
1. loadCommitHistory : removed call to edu/ucsb/cs156/frontiers/entities/Branch::setRetrievedTime → KILLED |
selectedBranch.setRetrievedTime(retrievedTime); |
| 231 | branchRepository.save(selectedBranch); | |
| 232 | String pointer = null; | |
| 233 | ArrayList<Commit> commitsToBeSaved = new ArrayList<>(4000); | |
| 234 | boolean hasNextPage; | |
| 235 | do { | |
| 236 | JsonNode currentPage = | |
| 237 | jacksonObjectMapper.readTree( | |
| 238 | getCommits(course, branch.org(), branch.repo(), branch.branchName(), 100, pointer)); | |
| 239 | pointer = | |
| 240 | currentPage | |
| 241 | .path("repository") | |
| 242 | .path("ref") | |
| 243 | .path("target") | |
| 244 | .path("history") | |
| 245 | .path("pageInfo") | |
| 246 | .path("endCursor") | |
| 247 | .asText(); | |
| 248 | hasNextPage = | |
| 249 | currentPage | |
| 250 | .path("repository") | |
| 251 | .path("ref") | |
| 252 | .path("target") | |
| 253 | .path("history") | |
| 254 | .path("pageInfo") | |
| 255 | .path("hasNextPage") | |
| 256 | .asBoolean(); | |
| 257 | JsonNode commits = | |
| 258 | currentPage.path("repository").path("ref").path("target").path("history").path("edges"); | |
| 259 | for (JsonNode node : commits) { | |
| 260 | String sha = node.get("node").get("oid").asText(); | |
| 261 |
1
1. loadCommitHistory : negated conditional → KILLED |
if (existingCommits.containsKey(sha)) { |
| 262 | continue; | |
| 263 | } else { | |
| 264 | Commit commit = jacksonObjectMapper.treeToValue(node.get("node"), Commit.class); | |
| 265 |
1
1. loadCommitHistory : removed call to edu/ucsb/cs156/frontiers/entities/Commit::setBranch → KILLED |
commit.setBranch(selectedBranch); |
| 266 | commitsToBeSaved.add(commit); | |
| 267 | } | |
| 268 | } | |
| 269 | ||
| 270 |
1
1. loadCommitHistory : negated conditional → KILLED |
} while (hasNextPage); |
| 271 | commitRepository.saveAll(commitsToBeSaved); | |
| 272 |
1
1. loadCommitHistory : replaced return value with null for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::loadCommitHistory → KILLED |
return selectedBranch; |
| 273 | } | |
| 274 | ||
| 275 | public String getMostRecentCommitSha(Course course, BranchId branch) throws Exception { | |
| 276 | String token = jwtService.getInstallationToken(course); | |
| 277 | ResponseEntity<JsonNode> response = | |
| 278 | client | |
| 279 | .get() | |
| 280 | .uri( | |
| 281 | "/repos/" + branch.org() + "/" + branch.repo() + "/branches/" + branch.branchName()) | |
| 282 | .header("Authorization", "Bearer " + token) | |
| 283 | .header("Accept", "application/vnd.github+json") | |
| 284 | .header("X-GitHub-Api-Version", "2022-11-28") | |
| 285 | .retrieve() | |
| 286 | .toEntity(JsonNode.class); | |
| 287 | String commitSha = response.getBody().path("commit").path("sha").asText(); | |
| 288 |
1
1. getMostRecentCommitSha : replaced return value with "" for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::getMostRecentCommitSha → KILLED |
return commitSha; |
| 289 | } | |
| 290 | ||
| 291 | public enum ValidationStatus { | |
| 292 | EXISTS, | |
| 293 | BRANCH_DOES_NOT_EXIST, | |
| 294 | REPOSITORY_DOES_NOT_EXIST, | |
| 295 | } | |
| 296 | ||
| 297 | // language=GraphQL | |
| 298 | final String individualBranchQuery = | |
| 299 | """ | |
| 300 | repo_%d: repository(owner: "%s", name: "%s") { | |
| 301 | ref(qualifiedName: "refs/heads/%s") { | |
| 302 | target { | |
| 303 | ... on Commit { | |
| 304 | oid | |
| 305 | } | |
| 306 | } | |
| 307 | } | |
| 308 | } | |
| 309 | """; | |
| 310 | final String completeQueryWrap = """ | |
| 311 | query CompleteQuery { | |
| 312 | %s | |
| 313 | } | |
| 314 | """; | |
| 315 | ||
| 316 | public Map<BranchId, ValidationStatus> assertBranchesExist(Course course, List<BranchId> branches) | |
| 317 | throws Exception { | |
| 318 | StringBuilder query = new StringBuilder(); | |
| 319 | Map<BranchId, ValidationStatus> result = new HashMap<>(); | |
| 320 |
2
1. assertBranchesExist : changed conditional boundary → KILLED 2. assertBranchesExist : negated conditional → KILLED |
for (int i = 0; i < branches.size(); i++) { |
| 321 | query.append( | |
| 322 | String.format( | |
| 323 | individualBranchQuery, | |
| 324 | i, | |
| 325 | branches.get(i).org(), | |
| 326 | branches.get(i).repo(), | |
| 327 | branches.get(i).branchName())); | |
| 328 | } | |
| 329 | String finalizedQuery = String.format(completeQueryWrap, query); | |
| 330 | String githubToken = jwtService.getInstallationToken(course); | |
| 331 | JsonNode response = | |
| 332 | graphQlClient | |
| 333 | .mutate() | |
| 334 | .header("Authorization", "Bearer " + githubToken) | |
| 335 | .build() | |
| 336 | .document(finalizedQuery) | |
| 337 | .executeSync() | |
| 338 | .toEntity(JsonNode.class); | |
| 339 | ||
| 340 |
2
1. assertBranchesExist : negated conditional → KILLED 2. assertBranchesExist : changed conditional boundary → KILLED |
for (int i = 0; i < branches.size(); i++) { |
| 341 | JsonNode branchNode = response.path("repo_" + i); | |
| 342 |
1
1. assertBranchesExist : negated conditional → KILLED |
if (branchNode.isNull()) { |
| 343 | result.put(branches.get(i), ValidationStatus.REPOSITORY_DOES_NOT_EXIST); | |
| 344 | continue; | |
| 345 | } | |
| 346 |
1
1. assertBranchesExist : negated conditional → KILLED |
if (branchNode.path("ref").isNull()) { |
| 347 | result.put(branches.get(i), ValidationStatus.BRANCH_DOES_NOT_EXIST); | |
| 348 | continue; | |
| 349 | } | |
| 350 | result.put(branches.get(i), ValidationStatus.EXISTS); | |
| 351 | } | |
| 352 |
1
1. assertBranchesExist : replaced return value with Collections.emptyMap for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::assertBranchesExist → KILLED |
return result; |
| 353 | } | |
| 354 | } | |
Mutations | ||
| 98 |
1.1 |
|
| 186 |
1.1 |
|
| 211 |
1.1 |
|
| 215 |
1.1 |
|
| 217 |
1.1 |
|
| 219 |
1.1 |
|
| 224 |
1.1 |
|
| 230 |
1.1 |
|
| 261 |
1.1 |
|
| 265 |
1.1 |
|
| 270 |
1.1 |
|
| 272 |
1.1 |
|
| 288 |
1.1 |
|
| 320 |
1.1 2.2 |
|
| 340 |
1.1 2.2 |
|
| 342 |
1.1 |
|
| 346 |
1.1 |
|
| 352 |
1.1 |