DeleteRepoJob.java
package edu.ucsb.cs156.frontiers.jobs;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.ucsb.cs156.frontiers.entities.Course;
import edu.ucsb.cs156.frontiers.services.JwtService;
import edu.ucsb.cs156.frontiers.services.jobs.JobContext;
import edu.ucsb.cs156.frontiers.services.jobs.JobContextConsumer;
import java.util.ArrayList;
import java.util.List;
import lombok.Builder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
@Builder
public class DeleteRepoJob implements JobContextConsumer {
Course course;
String prefix;
JwtService jwtService;
RestTemplate restTemplate;
ObjectMapper mapper;
@Override
public Course getCourse() {
return course;
}
@Override
public void accept(JobContext ctx) throws Exception {
String orgName = course.getOrgName();
String token = jwtService.getInstallationToken(course);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + token);
headers.add("Accept", "application/vnd.github+json");
headers.add("X-GitHub-Api-Version", "2022-11-28");
HttpEntity<String> entity = new HttpEntity<>(headers);
List<String> matchedRepos = new ArrayList<>();
// 1. Fetch all repositories for the organization (Handling Pagination)
String reposUrl = "https://api.github.com/orgs/" + orgName + "/repos?per_page=100";
boolean hasNext = true;
while (hasNext) {
ResponseEntity<String> response =
restTemplate.exchange(reposUrl, HttpMethod.GET, entity, String.class);
JsonNode reposNode = mapper.readTree(response.getBody());
for (JsonNode repoNode : reposNode) {
String repoName = repoNode.get("name").asText();
if (repoName.startsWith(prefix)) {
matchedRepos.add(repoName);
}
}
// Check for pagination "Link" header to get the next page
List<String> linkHeaders = response.getHeaders().get("Link");
hasNext = false;
if (linkHeaders != null && !linkHeaders.isEmpty()) {
String linkHeader = linkHeaders.get(0);
String[] parts = linkHeader.split(",");
for (String part : parts) {
if (part.contains("rel=\"next\"")) {
reposUrl = part.substring(part.indexOf('<') + 1, part.indexOf('>'));
hasNext = true;
break;
}
}
}
}
ctx.log(String.format("%d repos found with prefix %s", matchedRepos.size(), prefix));
int reposDeleted = 0;
int reposRetained = 0;
int errors = 0;
// 2. Iterate through matched repos to check for commits and delete
for (String repoName : matchedRepos) {
try {
// Sleep delay to prevent hitting GitHub API rate limits
Thread.sleep(1000);
String commitsUrl = "https://api.github.com/repos/" + orgName + "/" + repoName + "/commits";
boolean hasCommits = true;
try {
ResponseEntity<String> commitsResponse =
restTemplate.exchange(commitsUrl, HttpMethod.GET, entity, String.class);
JsonNode commitsNode = mapper.readTree(commitsResponse.getBody());
hasCommits = commitsNode.isArray() && !commitsNode.isEmpty();
} catch (HttpClientErrorException e) {
// GitHub API returns 409 Conflict if a repository is completely empty (no commits)
if (e.getStatusCode().equals(HttpStatus.CONFLICT)) {
hasCommits = false;
} else {
throw e; // Rethrow actual errors to be caught below
}
}
if (hasCommits) {
reposRetained++;
ctx.log(String.format("Repo %s not delete; commits exist.", repoName));
} else {
String deleteUrl = "https://api.github.com/repos/" + orgName + "/" + repoName;
restTemplate.exchange(deleteUrl, HttpMethod.DELETE, entity, String.class);
reposDeleted++;
}
} catch (Exception e) {
errors++;
ctx.log(String.format("Error processing repo %s: %s", repoName, e.getMessage()));
}
}
// 3. Final Logging
ctx.log(String.format("%d repos deleted", reposDeleted));
ctx.log(String.format("%d repos retained", reposRetained));
ctx.log(String.format("%d errors", errors));
}
}