| 1 | package edu.ucsb.cs156.frontiers.jobs; | |
| 2 | ||
| 3 | import com.fasterxml.jackson.databind.JsonNode; | |
| 4 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 5 | import edu.ucsb.cs156.frontiers.entities.Course; | |
| 6 | import edu.ucsb.cs156.frontiers.services.JwtService; | |
| 7 | import edu.ucsb.cs156.frontiers.services.jobs.JobContext; | |
| 8 | import edu.ucsb.cs156.frontiers.services.jobs.JobContextConsumer; | |
| 9 | import java.util.ArrayList; | |
| 10 | import java.util.List; | |
| 11 | import lombok.Builder; | |
| 12 | import org.springframework.http.HttpEntity; | |
| 13 | import org.springframework.http.HttpHeaders; | |
| 14 | import org.springframework.http.HttpMethod; | |
| 15 | import org.springframework.http.HttpStatus; | |
| 16 | import org.springframework.http.ResponseEntity; | |
| 17 | import org.springframework.web.client.HttpClientErrorException; | |
| 18 | import org.springframework.web.client.RestTemplate; | |
| 19 | ||
| 20 | @Builder | |
| 21 | public class DeleteRepoJob implements JobContextConsumer { | |
| 22 | ||
| 23 | Course course; | |
| 24 | String prefix; | |
| 25 | JwtService jwtService; | |
| 26 | RestTemplate restTemplate; | |
| 27 | ObjectMapper mapper; | |
| 28 | ||
| 29 | @Override | |
| 30 | public Course getCourse() { | |
| 31 |
1
1. getCourse : replaced return value with null for edu/ucsb/cs156/frontiers/jobs/DeleteRepoJob::getCourse → KILLED |
return course; |
| 32 | } | |
| 33 | ||
| 34 | @Override | |
| 35 | public void accept(JobContext ctx) throws Exception { | |
| 36 | String orgName = course.getOrgName(); | |
| 37 | String token = jwtService.getInstallationToken(course); | |
| 38 | ||
| 39 | HttpHeaders headers = new HttpHeaders(); | |
| 40 |
1
1. accept : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("Authorization", "Bearer " + token); |
| 41 |
1
1. accept : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("Accept", "application/vnd.github+json"); |
| 42 |
1
1. accept : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("X-GitHub-Api-Version", "2022-11-28"); |
| 43 | HttpEntity<String> entity = new HttpEntity<>(headers); | |
| 44 | ||
| 45 | List<String> matchedRepos = new ArrayList<>(); | |
| 46 | ||
| 47 | // 1. Fetch all repositories for the organization (Handling Pagination) | |
| 48 | String reposUrl = "https://api.github.com/orgs/" + orgName + "/repos?per_page=100"; | |
| 49 | boolean hasNext = true; | |
| 50 | ||
| 51 |
1
1. accept : negated conditional → KILLED |
while (hasNext) { |
| 52 | ResponseEntity<String> response = | |
| 53 | restTemplate.exchange(reposUrl, HttpMethod.GET, entity, String.class); | |
| 54 | JsonNode reposNode = mapper.readTree(response.getBody()); | |
| 55 | ||
| 56 | for (JsonNode repoNode : reposNode) { | |
| 57 | String repoName = repoNode.get("name").asText(); | |
| 58 |
1
1. accept : negated conditional → KILLED |
if (repoName.startsWith(prefix)) { |
| 59 | matchedRepos.add(repoName); | |
| 60 | } | |
| 61 | } | |
| 62 | ||
| 63 | // Check for pagination "Link" header to get the next page | |
| 64 | List<String> linkHeaders = response.getHeaders().get("Link"); | |
| 65 | hasNext = false; | |
| 66 |
2
1. accept : negated conditional → KILLED 2. accept : negated conditional → KILLED |
if (linkHeaders != null && !linkHeaders.isEmpty()) { |
| 67 | String linkHeader = linkHeaders.get(0); | |
| 68 | String[] parts = linkHeader.split(","); | |
| 69 | for (String part : parts) { | |
| 70 |
1
1. accept : negated conditional → KILLED |
if (part.contains("rel=\"next\"")) { |
| 71 |
1
1. accept : Replaced integer addition with subtraction → KILLED |
reposUrl = part.substring(part.indexOf('<') + 1, part.indexOf('>')); |
| 72 | hasNext = true; | |
| 73 | break; | |
| 74 | } | |
| 75 | } | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | ctx.log(String.format("%d repos found with prefix %s", matchedRepos.size(), prefix)); | |
| 80 | ||
| 81 | int reposDeleted = 0; | |
| 82 | int reposRetained = 0; | |
| 83 | int errors = 0; | |
| 84 | ||
| 85 | // 2. Iterate through matched repos to check for commits and delete | |
| 86 | for (String repoName : matchedRepos) { | |
| 87 | try { | |
| 88 | // Sleep delay to prevent hitting GitHub API rate limits | |
| 89 |
1
1. accept : removed call to java/lang/Thread::sleep → KILLED |
Thread.sleep(1000); |
| 90 | ||
| 91 | String commitsUrl = "https://api.github.com/repos/" + orgName + "/" + repoName + "/commits"; | |
| 92 | boolean hasCommits = true; | |
| 93 | ||
| 94 | try { | |
| 95 | ResponseEntity<String> commitsResponse = | |
| 96 | restTemplate.exchange(commitsUrl, HttpMethod.GET, entity, String.class); | |
| 97 | JsonNode commitsNode = mapper.readTree(commitsResponse.getBody()); | |
| 98 |
2
1. accept : negated conditional → KILLED 2. accept : negated conditional → KILLED |
hasCommits = commitsNode.isArray() && !commitsNode.isEmpty(); |
| 99 | } catch (HttpClientErrorException e) { | |
| 100 | // GitHub API returns 409 Conflict if a repository is completely empty (no commits) | |
| 101 |
1
1. accept : negated conditional → KILLED |
if (e.getStatusCode().equals(HttpStatus.CONFLICT)) { |
| 102 | hasCommits = false; | |
| 103 | } else { | |
| 104 | throw e; // Rethrow actual errors to be caught below | |
| 105 | } | |
| 106 | } | |
| 107 | ||
| 108 |
1
1. accept : negated conditional → KILLED |
if (hasCommits) { |
| 109 |
1
1. accept : Changed increment from 1 to -1 → KILLED |
reposRetained++; |
| 110 | ctx.log(String.format("Repo %s not delete; commits exist.", repoName)); | |
| 111 | } else { | |
| 112 | String deleteUrl = "https://api.github.com/repos/" + orgName + "/" + repoName; | |
| 113 | restTemplate.exchange(deleteUrl, HttpMethod.DELETE, entity, String.class); | |
| 114 |
1
1. accept : Changed increment from 1 to -1 → KILLED |
reposDeleted++; |
| 115 | } | |
| 116 | } catch (Exception e) { | |
| 117 |
1
1. accept : Changed increment from 1 to -1 → KILLED |
errors++; |
| 118 | ctx.log(String.format("Error processing repo %s: %s", repoName, e.getMessage())); | |
| 119 | } | |
| 120 | } | |
| 121 | ||
| 122 | // 3. Final Logging | |
| 123 | ctx.log(String.format("%d repos deleted", reposDeleted)); | |
| 124 | ctx.log(String.format("%d repos retained", reposRetained)); | |
| 125 | ctx.log(String.format("%d errors", errors)); | |
| 126 | } | |
| 127 | } | |
Mutations | ||
| 31 |
1.1 |
|
| 40 |
1.1 |
|
| 41 |
1.1 |
|
| 42 |
1.1 |
|
| 51 |
1.1 |
|
| 58 |
1.1 |
|
| 66 |
1.1 2.2 |
|
| 70 |
1.1 |
|
| 71 |
1.1 |
|
| 89 |
1.1 |
|
| 98 |
1.1 2.2 |
|
| 101 |
1.1 |
|
| 108 |
1.1 |
|
| 109 |
1.1 |
|
| 114 |
1.1 |
|
| 117 |
1.1 |