Skip to content

Commit

Permalink
Merge pull request #7 from Leets-Official/6-fetch-from-Github
Browse files Browse the repository at this point in the history
  • Loading branch information
leejuae authored Jul 17, 2024
2 parents 99f4809 + d47c43b commit 5eca930
Show file tree
Hide file tree
Showing 13 changed files with 378 additions and 19 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ dependencies {
// AWS
implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.1.0")
implementation 'io.awspring.cloud:spring-cloud-aws-starter-parameter-store'

// GSON
implementation "com.google.code.gson:gson:2.8.9"
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
import com.leets.commitatobe.domain.user.domain.User;
import com.leets.commitatobe.global.shared.entity.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.Date;
import java.util.UUID;

@Entity(name = "commit")
Expand All @@ -25,13 +25,29 @@ public class Commit extends BaseTimeEntity {
private Integer cnt;

@Column
private Date commitDate;

@Column
private LocalDateTime date;
private LocalDateTime commitDate;

@ManyToOne
@JoinColumn(name="user_id")
@JsonBackReference
private User user;

public static Commit create(LocalDateTime commitDate, Integer cnt, User user) {
return Commit.builder()
.commitDate(commitDate)
.cnt(cnt)
.user(user)
.build();
}

@Builder
public Commit(LocalDateTime commitDate, Integer cnt, User user) {
this.commitDate = commitDate;
this.cnt = cnt;
this.user = user;
}

public void updateCnt(Integer cnt) {
this.cnt = this.cnt + cnt;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.leets.commitatobe.domain.commit.domain.repository;

import com.leets.commitatobe.domain.commit.domain.Commit;
import com.leets.commitatobe.domain.user.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Repository
public interface CommitRepository extends JpaRepository<Commit, UUID> {
List<Commit> findAllByUser(User user);

Optional<Commit> findByCommitDateAndUser(LocalDateTime commitDate, User user);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
package com.leets.commitatobe.domain.commit.presentation;
import com.leets.commitatobe.domain.commit.presentation.dto.response.CommitResponse;
import com.leets.commitatobe.domain.commit.usecase.FetchCommits;
import com.leets.commitatobe.global.response.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;


@RestController
@RequiredArgsConstructor
@RequestMapping("/commit")
public class CommitController {
private final FetchCommits fetchCommits;

@Operation(
summary = "์ปค๋ฐ‹ ๊ธฐ๋ก ๋ถˆ๋Ÿฌ์˜ค๊ธฐ",
description = "GitHub์—์„œ ์ปค๋ฐ‹ ๊ธฐ๋ก์„ ๊ฐ€์ ธ์™€ DB์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.")
@PostMapping("/fetch")
public ApiResponse<CommitResponse> fetchCommits(HttpServletRequest request) {
return ApiResponse.onSuccess(fetchCommits.execute(request));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package com.leets.commitatobe.domain.commit.presentation.dto.response;

public record CommitResponse() {
public record CommitResponse(
String message
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.leets.commitatobe.domain.commit.usecase;

import com.leets.commitatobe.domain.commit.domain.Commit;
import com.leets.commitatobe.domain.commit.domain.repository.CommitRepository;
import com.leets.commitatobe.domain.commit.presentation.dto.response.CommitResponse;
import com.leets.commitatobe.domain.login.usecase.LoginCommandServiceImpl;
import com.leets.commitatobe.domain.login.usecase.LoginQueryService;
import com.leets.commitatobe.domain.user.domain.User;
import com.leets.commitatobe.domain.user.domain.repository.UserRepository;
import com.leets.commitatobe.global.exception.ApiException;
import com.leets.commitatobe.global.response.code.status.ErrorStatus;
import com.leets.commitatobe.global.response.code.status.SuccessStatus;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Service
@RequiredArgsConstructor
public class FetchCommits {
private final CommitRepository commitRepository;
private final UserRepository userRepository;
private final GitHubService gitHubService; // GitHub API ํ†ต์‹ 
private final LoginCommandServiceImpl loginCommandService;
private final LoginQueryService loginQueryService;

public CommitResponse execute(HttpServletRequest request) {
String gitHubId = loginQueryService.getGitHubId(request);
User user = userRepository.findByGithubId(gitHubId)
.orElseThrow(() -> new UsernameNotFoundException("ํ•ด๋‹นํ•˜๋Š” ๊นƒํ—ˆ๋ธŒ ๋‹‰๋„ค์ž„๊ณผ ์ผ์น˜ํ•˜๋Š” ์œ ์ €๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ: " + gitHubId));

LocalDateTime dateTime;
try {
dateTime = commitRepository.findAllByUser(user).stream()
.max(Comparator.comparing(Commit::getUpdatedAt))
.orElseThrow(() -> new ApiException(ErrorStatus._COMMIT_NOT_FOUND))
.getUpdatedAt();
} catch (ApiException e) {
dateTime = LocalDateTime.now();
// dateTime = LocalDateTime.of(2024, 07, 01, 0, 0, 0); // test ์šฉ
}

try {
// Github API Access Token ์ €์žฅ
gitHubService.updateToken(loginCommandService.gitHubLogin(gitHubId));

List<String> repos = gitHubService.fetchRepos(gitHubId);
ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
List<CompletableFuture<Void>> futures = new ArrayList<>();
LocalDateTime finalDateTime = dateTime; // ์˜ค๋ฅ˜ ๋ฐฉ์ง€

for (String fullName : repos) {
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> {
try {
gitHubService.countCommits(fullName, gitHubId, finalDateTime);
} catch (IOException e) {
throw new ApiException(ErrorStatus._GIT_URL_INCORRECT);
}
}, executor);
futures.add(voidCompletableFuture);
}

CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
allFutures.join();

executor.shutdown();

saveCommits(user);

} catch (Exception e) {
throw new RuntimeException(e);
}

return new CommitResponse(SuccessStatus._OK.getMessage());
}

private void saveCommits(User user) {
// ๋‚ ์งœ๋ณ„ ์ปค๋ฐ‹ ์ˆ˜ DB์— ์ €์žฅ
for (Map.Entry<LocalDateTime, Integer> entry : gitHubService.getCommitsByDate().entrySet()) {
Commit commit = commitRepository.findByCommitDateAndUser(entry.getKey(), user)
.orElse(Commit.create(entry.getKey(), 0, user));
commit.updateCnt(entry.getValue());
commitRepository.save(commit);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package com.leets.commitatobe.domain.commit.usecase;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Getter
public class GitHubService {
private final String GITHUB_API_URL = "https://api.github.com";
private String AUTH_TOKEN;
private final Map<LocalDateTime, Integer> commitsByDate = new HashMap<>();

// GitHub repository ์ด๋ฆ„ ์ €์žฅ
public List<String> fetchRepos(String gitHubUsername) throws IOException {
URL url = new URL(GITHUB_API_URL + "/user/repos?type=all&sort=pushed&per_page=100");
HttpURLConnection connection = getConnection(url);
JsonArray repos = fetchJsonArray(connection);

if (repos == null) {
return new ArrayList<>();
}

List<String> repoFullNames = new ArrayList<>();
for (int i = 0; i < repos.size(); i++) {
String fullName = repos.get(i).getAsJsonObject().get("full_name").getAsString();
repoFullNames.add(fullName);
}

ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
return forkJoinPool.submit(() ->
repoFullNames.parallelStream()
.filter(fullName -> {
try {
return isContributor(fullName, gitHubUsername);
} catch (IOException e) {
return false;
}
})
.collect(Collectors.toList())
).join();
}

// ์ž์‹ ์ด ํ•ด๋‹น repository์˜ ๊ธฐ์—ฌ์ž ์ธ์ง€ ํ™•์ธ
private boolean isContributor(String fullName, String gitHubUsername) throws IOException {
if (fullName.contains(gitHubUsername)) {
return true;
}

URL url = new URL(GITHUB_API_URL + "/repos/" + fullName + "/contributors");
HttpURLConnection connection = getConnection(url);
JsonArray contributors = fetchJsonArray(connection);

if (contributors == null) {
return false;
}

for (int i = 0; i < contributors.size(); i++) {
JsonObject contributor = contributors.get(i).getAsJsonObject();
String contributorLogin = contributor.get("login").getAsString();

if (contributorLogin.equals(gitHubUsername)) {
return true;
}
}

return false;
}

// commit์„ ์ผ๋ณ„๋กœ ์ •๋ฆฌ
public void countCommits(String fullName, String gitHubUsername, LocalDateTime date) throws IOException {
int page = 1;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

while (true) {
URL url = new URL(GITHUB_API_URL + "/repos/" + fullName + "/commits?page=" + page + "&per_page=100");
HttpURLConnection connection = getConnection(url);
JsonArray commits = fetchJsonArray(connection);

if (commits == null || commits.isEmpty()) {
return;
}

for (int i = 0; i < commits.size(); i++) {
String commitDateTime = getCommitDateTime(commits.get(i).getAsJsonObject());

int comparisonResult = commitDateTime.compareTo(formatToISO8601(date));

if (comparisonResult < 0 || !commits.get(i).getAsJsonObject().get("author").getAsJsonObject().get("login").getAsString().equals(gitHubUsername)) {
continue;
}

LocalDateTime commitDate = LocalDate.parse(commitDateTime.substring(0, 10), formatter).atStartOfDay();

synchronized (commitsByDate) {
commitsByDate.put(commitDate, commitsByDate.getOrDefault(commitDate, 0) + 1);
}
}

page++;
}
}

// http ์—ฐ๊ฒฐ
private HttpURLConnection getConnection(URL url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Authorization", "token " + AUTH_TOKEN);
connection.setRequestProperty("Accept", "application/vnd.github.v3+json");
return connection;
}

// ์‘๋‹ต์„ jsonObject๋กœ ๋ฐ˜ํ™˜
private JsonObject fetchJsonObject(HttpURLConnection connection) throws IOException {
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
return JsonParser.parseReader(in).getAsJsonObject();
}
} else {
System.err.println(responseCode);
return null;
}
}

// ์‘๋‹ต์„ JsonArray๋กœ ๋ฐ˜ํ™˜
private JsonArray fetchJsonArray(HttpURLConnection connection) throws IOException {
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
return JsonParser.parseReader(in).getAsJsonArray();
}
} else {
System.err.println(responseCode);
return null;
}
}

// commit ์‹œ๊ฐ„ ์ถ”์ถœ
private String getCommitDateTime(JsonObject commit) {
String originCommitDateTime = commit.get("commit").getAsJsonObject() // UTC+0
.get("author").getAsJsonObject()
.get("date").getAsString();

// ์ž…๋ ฅ๋œ ์‹œ๊ฐ„ ๋ฌธ์ž์—ด์„ LocalDateTime์œผ๋กœ ๋ณ€ํ™˜
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
LocalDateTime dateTime = LocalDateTime.parse(originCommitDateTime, formatter);

// 9์‹œ๊ฐ„ ์ถ”๊ฐ€ -> UTC+9(ํ•œ๊ตญ ํ‘œ์ค€ ์‹œ)
return dateTime.plusHours(9).format(formatter);
}

// GitHub API์—์„œ ์ œ๊ณตํ•˜๋Š” ์‹œ๊ฐ„ ํ‘œํ˜„๋ฒ•์œผ๋กœ ๋ณ€ํ™˜
private String formatToISO8601(LocalDateTime dateTime) {
ZonedDateTime zonedDateTime = dateTime.atZone(ZoneOffset.UTC);
return DateTimeFormatter.ISO_INSTANT.format(zonedDateTime);
}

// GitHub Access Token ์ €์žฅ
public void updateToken(String accessToken) {
this.AUTH_TOKEN = accessToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public interface LoginQueryService {
// AUTH ํ—ค๋”์—์„œ ์—‘์„ธ์Šค ํ† ํฐ์„ ์ด์šฉํ•ด ์œ ์ € ์•„์ด๋””๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ํ•จ์ˆ˜
GitHubDto getGitHubUser(HttpServletRequest request);

String getGitHubId(HttpServletRequest request);
}
Loading

0 comments on commit 5eca930

Please sign in to comment.