GithubGraphQLService.java

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
Location : getDefaultBranchName
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:testGetDefaultBranchName()]
replaced return value with "" for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::getDefaultBranchName → KILLED

186

1.1
Location : getCommits
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:testGetCommits()]
replaced return value with "" for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::getCommits → KILLED

211

1.1
Location : loadCommitHistory
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:short_circuit_exit_behaves()]
negated conditional → KILLED

215

1.1
Location : loadCommitHistory
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:short_circuit_exit_behaves()]
negated conditional → KILLED

217

1.1
Location : loadCommitHistory
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:short_circuit_exit_behaves()]
removed call to edu/ucsb/cs156/frontiers/entities/Branch::setRetrievedTime → KILLED

219

1.1
Location : loadCommitHistory
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:short_circuit_exit_behaves()]
replaced return value with null for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::loadCommitHistory → KILLED

224

1.1
Location : loadCommitHistory
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:loadCommitHistory_skips_existing_commits()]
removed call to java/util/stream/Stream::forEach → KILLED

230

1.1
Location : loadCommitHistory
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:can_parse_two_pages()]
removed call to edu/ucsb/cs156/frontiers/entities/Branch::setRetrievedTime → KILLED

261

1.1
Location : loadCommitHistory
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:can_parse_two_pages()]
negated conditional → KILLED

265

1.1
Location : loadCommitHistory
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:can_parse_two_pages()]
removed call to edu/ucsb/cs156/frontiers/entities/Commit::setBranch → KILLED

270

1.1
Location : loadCommitHistory
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:can_parse_two_pages()]
negated conditional → KILLED

272

1.1
Location : loadCommitHistory
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:can_parse_two_pages()]
replaced return value with null for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::loadCommitHistory → KILLED

288

1.1
Location : getMostRecentCommitSha
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:short_circuit_exit_behaves()]
replaced return value with "" for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::getMostRecentCommitSha → KILLED

320

1.1
Location : assertBranchesExist
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:assertBranchesExist_matches_returns_correctly()]
changed conditional boundary → KILLED

2.2
Location : assertBranchesExist
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:assertBranchesExist_matches_returns_correctly()]
negated conditional → KILLED

340

1.1
Location : assertBranchesExist
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:assertBranchesExist_matches_returns_correctly()]
negated conditional → KILLED

2.2
Location : assertBranchesExist
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:assertBranchesExist_matches_returns_correctly()]
changed conditional boundary → KILLED

342

1.1
Location : assertBranchesExist
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:assertBranchesExist_matches_returns_correctly()]
negated conditional → KILLED

346

1.1
Location : assertBranchesExist
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:assertBranchesExist_matches_returns_correctly()]
negated conditional → KILLED

352

1.1
Location : assertBranchesExist
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:assertBranchesExist_matches_returns_correctly()]
replaced return value with Collections.emptyMap for edu/ucsb/cs156/frontiers/services/GithubGraphQLService::assertBranchesExist → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0