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

Mutations

83

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

125

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

142

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

236

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

279

1.1
Location : downloadCommitHistory
Killed by : edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.GithubGraphQLServiceTests]/[method:handles_two_pages()]
removed call to edu/ucsb/cs156/frontiers/entities/DownloadedCommit::setRequest → KILLED

283

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

Active mutators

Tests examined


Report generated by PIT 1.17.0