From 2bf24730f0d104fb9dae3e284a6b93201addb74f Mon Sep 17 00:00:00 2001 From: Ege Kocabas Date: Sun, 17 Nov 2024 15:01:08 +0100 Subject: [PATCH 1/3] add github client config & githubsync service --- server/application-server/.env.example | 2 +- server/application-server/build.gradle | 2 + .../cit/aet/helios/config/GitHubConfig.java | 106 +++++++++ .../aet/helios/gitprovider/GitHubService.java | 99 +++++++++ .../common/github/BaseGitServiceEntity.java | 22 ++ .../github/BaseGitServiceEntityConverter.java | 42 ++++ .../gitprovider/common/github/DateUtil.java | 11 + .../aet/helios/gitprovider/issue/Issue.java | 83 +++++++ .../gitprovider/issue/IssueRepository.java | 7 + .../issue/github/GitHubIssueConverter.java | 44 ++++ .../gitprovider/pullrequest/PullRequest.java | 78 +++++++ .../pullrequest/PullRequestBaseInfoDTO.java | 44 ++++ .../pullrequest/PullRequestInfoDTO.java | 58 +++++ .../pullrequest/PullRequestRepository.java | 10 + .../github/GitHubPullRequestConverter.java | 95 ++++++++ .../GitHubPullRequestMessageHandler.java | 42 ++++ .../github/GitHubPullRequestSyncService.java | 207 ++++++++++++++++++ .../gitprovider/repository/Repository.java | 95 ++++++++ .../repository/RepositoryInfoDTO.java | 22 ++ .../repository/RepositoryRepository.java | 10 + .../github/GitHubRepositoryConverter.java | 58 +++++ .../github/GitHubRepositorySyncService.java | 128 +++++++++++ .../cit/aet/helios/gitprovider/user/User.java | 82 +++++++ .../helios/gitprovider/user/UserInfoDTO.java | 22 ++ .../gitprovider/user/UserRepository.java | 7 + .../user/github/GitHubUserConverter.java | 88 ++++++++ .../user/github/GitHubUserSyncService.java | 88 ++++++++ .../aet/helios/syncing/DataSyncStatus.java | 33 +++ .../syncing/DataSyncStatusRepository.java | 12 + .../syncing/GitHubDataSyncScheduler.java | 41 ++++ .../helios/syncing/GitHubDataSyncService.java | 76 +++++++ .../src/main/resources/application.yml | 20 +- 32 files changed, 1732 insertions(+), 2 deletions(-) create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/config/GitHubConfig.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/GitHubService.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/common/github/BaseGitServiceEntity.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/common/github/BaseGitServiceEntityConverter.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/common/github/DateUtil.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/issue/Issue.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/issue/IssueRepository.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/issue/github/GitHubIssueConverter.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequest.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequestBaseInfoDTO.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequestInfoDTO.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequestRepository.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/github/GitHubPullRequestConverter.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/github/GitHubPullRequestMessageHandler.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/github/GitHubPullRequestSyncService.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/Repository.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/RepositoryInfoDTO.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/RepositoryRepository.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/github/GitHubRepositoryConverter.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/github/GitHubRepositorySyncService.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/User.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/UserInfoDTO.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/UserRepository.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/github/GitHubUserConverter.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/github/GitHubUserSyncService.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/DataSyncStatus.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/DataSyncStatusRepository.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/GitHubDataSyncScheduler.java create mode 100644 server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/GitHubDataSyncService.java diff --git a/server/application-server/.env.example b/server/application-server/.env.example index e2356381..522344fd 100644 --- a/server/application-server/.env.example +++ b/server/application-server/.env.example @@ -4,4 +4,4 @@ DATASOURCE_PASSWORD=helios NATS_SERVER=localhost:4222 NATS_AUTH_TOKEN='5760e8ae09adfb2756f9f8cd5cb2caa704cd3f549eaa9298be843ceb165185d815b81f90c680fa7f626b7cd63abf6ac9' REPOSITORY_NAME= - +GITHUB_AUTH_TOKEN= diff --git a/server/application-server/build.gradle b/server/application-server/build.gradle index 900aafd6..8a5ee522 100644 --- a/server/application-server/build.gradle +++ b/server/application-server/build.gradle @@ -54,6 +54,8 @@ dependencies { implementation 'org.openapitools:jackson-databind-nullable:0.2.6' implementation 'io.nats:jnats:2.20.4' implementation 'org.kohsuke:github-api:1.326' + implementation 'com.squareup.okhttp3:okhttp:4.12.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' runtimeOnly 'org.postgresql:postgresql' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/config/GitHubConfig.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/config/GitHubConfig.java new file mode 100644 index 00000000..2d580409 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/config/GitHubConfig.java @@ -0,0 +1,106 @@ +package de.tum.cit.aet.helios.config; + +import lombok.Getter; +import okhttp3.Cache; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; +import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +@Configuration +public class GitHubConfig { + + private static final Logger logger = LoggerFactory.getLogger(GitHubConfig.class); + + @Getter + @Value("${github.organizationName}") + private String organizationName; + + @Value("${github.authToken}") + private String ghAuthToken; + + @Value("${github.cache.enabled}") + private boolean cacheEnabled; + + @Value("${github.cache.ttl}") + private int cacheTtl; + + @Value("${github.cache.size}") + private int cacheSize; + + private final Environment environment; + + @Autowired + public GitHubConfig(Environment environment) { + this.environment = environment; + } + + @Bean + public GitHub createGitHubClientWithCache() { + if (ghAuthToken == null || ghAuthToken.isEmpty()) { + logger.error("GitHub auth token is not provided! GitHub client will be disabled."); + return GitHub.offline(); + } + + // Set up a logging interceptor for debugging + HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); + if (environment.matchesProfiles("debug")) { + logger.warn("The requests to GitHub will be logged with the full body. This exposes sensitive data such as OAuth tokens. Use only for debugging!"); + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + } else { + logger.info("The requests to GitHub will be logged with the basic information."); + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC); + } + + OkHttpClient.Builder builder = new OkHttpClient().newBuilder(); + + if (cacheEnabled) { + File cacheDir = new File("./build/github-cache"); + Cache cache = new Cache(cacheDir, cacheSize * 1024L * 1024L); + builder.cache(cache); + logger.info("Cache is enabled with TTL {} seconds and size {} MB", cacheTtl, cacheSize); + } else { + logger.info("Cache is disabled"); + } + + // Configure OkHttpClient with the cache and logging + OkHttpClient okHttpClient = builder + .connectTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .addInterceptor(loggingInterceptor) + .build(); + + try { + // Initialize GitHub client with OAuth token and custom OkHttpClient + GitHub github = new GitHubBuilder() + .withConnector(new OkHttpGitHubConnector(okHttpClient, cacheTtl)) + .withOAuthToken(ghAuthToken) + .build(); + if (!github.isCredentialValid()) { + logger.error("Invalid GitHub credentials!"); + throw new IllegalStateException("Invalid GitHub credentials"); + } + logger.info("GitHub client initialized successfully"); + return github; + } catch (IOException e) { + logger.error("Failed to initialize GitHub client: {}", e.getMessage()); + throw new RuntimeException("GitHub client initialization failed", e); + } catch (Exception e) { + logger.error("An unexpected error occurred during GitHub client initialization: {}", e.getMessage()); + throw new RuntimeException("Unexpected error during GitHub client initialization", e); + } + } +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/GitHubService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/GitHubService.java new file mode 100644 index 00000000..5340cd3a --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/GitHubService.java @@ -0,0 +1,99 @@ +package de.tum.cit.aet.helios.gitprovider; + +import de.tum.cit.aet.helios.config.GitHubConfig; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHWorkflow; +import org.kohsuke.github.GitHub; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@Service +public class GitHubService { + + private static final Logger logger = LoggerFactory.getLogger(GitHubService.class); + + private final GitHub github; + + private final GitHubConfig gitHubConfig; + + private GHOrganization gitHubOrganization; + + @Autowired + public GitHubService(GitHub github, GitHubConfig gitHubConfig) { + this.github = github; + this.gitHubConfig = gitHubConfig; + } + + /** + * Retrieves the GitHub organization client. + * + * @return the GitHub organization client + * @throws IOException if an I/O error occurs + */ + public GHOrganization getOrganizationClient() throws IOException { + if (gitHubOrganization == null) { + final String organizationName = gitHubConfig.getOrganizationName(); + if (organizationName == null || organizationName.isEmpty()) { + logger.error("No organization name provided in the configuration. GitHub organization client will not be initialized."); + throw new RuntimeException("No organization name provided in the configuration."); + } + gitHubOrganization = github.getOrganization(organizationName); + } + return gitHubOrganization; + } + + /** + * Retrieves the GitHub repository. + * + * @param repoNameWithOwners the repository name with owners + * @return the GitHub repository + * @throws IOException if an I/O error occurs + */ + public GHRepository getRepository(String repoNameWithOwners) throws IOException { + return github.getRepository(repoNameWithOwners); + } + + /** + * Retrieves the list of workflows for a given repository. + * + * @param repoNameWithOwners the repository name with owners + * @return the list of workflows + * @throws IOException if an I/O error occurs + */ + public List getWorkflows(String repoNameWithOwners) throws IOException { + return getRepository(repoNameWithOwners).listWorkflows().toList(); + } + + /** + * Retrieves a specific workflow for a given repository. + * + * @param repoNameWithOwners the repository name with owners + * @param workflowFileNameOrId the workflow file name or ID + * @return the GitHub workflow + * @throws IOException if an I/O error occurs + */ + public GHWorkflow getWorkflow(String repoNameWithOwners, String workflowFileNameOrId) throws IOException { + return getRepository(repoNameWithOwners).getWorkflow(workflowFileNameOrId); + } + + /** + * Dispatches a workflow for a given repository. + * + * @param repoNameWithOwners the repository name with owners + * @param workflowFileNameOrId the workflow file name or ID + * @param ref the reference (branch or tag) to run the workflow on + * @param inputs the inputs for the workflow + * @throws IOException if an I/O error occurs + */ + public void dispatchWorkflow(String repoNameWithOwners, String workflowFileNameOrId, String ref, Map inputs) throws IOException { + getRepository(repoNameWithOwners).getWorkflow(workflowFileNameOrId).dispatch(ref, inputs); + } + +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/common/github/BaseGitServiceEntity.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/common/github/BaseGitServiceEntity.java new file mode 100644 index 00000000..b949b7e3 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/common/github/BaseGitServiceEntity.java @@ -0,0 +1,22 @@ +package de.tum.cit.aet.helios.gitprovider.common.github; + +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import lombok.*; + +import java.time.OffsetDateTime; + +@MappedSuperclass +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public abstract class BaseGitServiceEntity { + @Id + protected Long id; + + protected OffsetDateTime createdAt; + + protected OffsetDateTime updatedAt; +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/common/github/BaseGitServiceEntityConverter.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/common/github/BaseGitServiceEntityConverter.java new file mode 100644 index 00000000..e3828808 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/common/github/BaseGitServiceEntityConverter.java @@ -0,0 +1,42 @@ +package de.tum.cit.aet.helios.gitprovider.common.github; + +import org.kohsuke.github.GHObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.lang.NonNull; + +import java.io.IOException; + +@ReadingConverter +public abstract class BaseGitServiceEntityConverter + implements Converter { + + private static final Logger logger = LoggerFactory.getLogger(BaseGitServiceEntityConverter.class); + + abstract public T update(@NonNull S source, @NonNull T target); + + protected void convertBaseFields(S source, T target) { + if (source == null || target == null) { + throw new IllegalArgumentException("Source and target must not be null"); + } + + // Map common fields + target.setId(source.getId()); + + try { + target.setCreatedAt(DateUtil.convertToOffsetDateTime(source.getCreatedAt())); + } catch (IOException e) { + logger.error("Failed to convert createdAt field for source {}: {}", source.getId(), e.getMessage()); + target.setCreatedAt(null); + } + + try { + target.setUpdatedAt(DateUtil.convertToOffsetDateTime(source.getUpdatedAt())); + } catch (IOException e) { + logger.error("Failed to convert updatedAt field for source {}: {}", source.getId(), e.getMessage()); + target.setUpdatedAt(null); + } + } +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/common/github/DateUtil.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/common/github/DateUtil.java new file mode 100644 index 00000000..701177b5 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/common/github/DateUtil.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.helios.gitprovider.common.github; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Date; + +public class DateUtil { + public static OffsetDateTime convertToOffsetDateTime(Date date) { + return date != null ? date.toInstant().atOffset(ZoneOffset.UTC) : null; + } +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/issue/Issue.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/issue/Issue.java new file mode 100644 index 00000000..6b7a8f55 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/issue/Issue.java @@ -0,0 +1,83 @@ +package de.tum.cit.aet.helios.gitprovider.issue; + +import de.tum.cit.aet.helios.gitprovider.common.github.BaseGitServiceEntity; +import de.tum.cit.aet.helios.gitprovider.repository.Repository; +import de.tum.cit.aet.helios.gitprovider.user.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springframework.lang.NonNull; + +import java.time.OffsetDateTime; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "issue") +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "issue_type", discriminatorType = DiscriminatorType.STRING) +@DiscriminatorValue(value = "ISSUE") +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class Issue extends BaseGitServiceEntity { + + private int number; + + @NonNull + @Enumerated(EnumType.STRING) + private State state; + + @NonNull + private String title; + + @Lob + private String body; + + @NonNull + private String htmlUrl; + + private boolean isLocked; + + private OffsetDateTime closedAt; + + private int commentsCount; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id") + @ToString.Exclude + private User author; + + @ManyToMany + @JoinTable(name = "issue_assignee", joinColumns = @JoinColumn(name = "issue_id"), inverseJoinColumns = @JoinColumn(name = "user_id")) + @ToString.Exclude + private Set assignees = new HashSet<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "repository_id") + private Repository repository; + + public enum State { + OPEN, CLOSED + } + + public boolean isPullRequest() { + return false; + } + + // Missing properties + // - milestone + // - issue_comments + // - labels + + // Ignored GitHub properties: + // - closed_by seems not to be used by webhooks + // - author_association (not provided by our GitHub API client) + // - state_reason + // - reactions + // - active_lock_reason + // - [remaining urls] +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/issue/IssueRepository.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/issue/IssueRepository.java new file mode 100644 index 00000000..70ab8a30 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/issue/IssueRepository.java @@ -0,0 +1,7 @@ +package de.tum.cit.aet.helios.gitprovider.issue; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface IssueRepository extends JpaRepository { + +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/issue/github/GitHubIssueConverter.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/issue/github/GitHubIssueConverter.java new file mode 100644 index 00000000..325d606c --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/issue/github/GitHubIssueConverter.java @@ -0,0 +1,44 @@ +package de.tum.cit.aet.helios.gitprovider.issue.github; + + +import de.tum.cit.aet.helios.gitprovider.common.github.BaseGitServiceEntityConverter; +import de.tum.cit.aet.helios.gitprovider.common.github.DateUtil; +import de.tum.cit.aet.helios.gitprovider.issue.Issue; +import org.kohsuke.github.GHIssue; +import org.kohsuke.github.GHIssueState; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +@Component +public class GitHubIssueConverter extends BaseGitServiceEntityConverter { + + @Override + public Issue convert(@NonNull GHIssue source) { + return update(source, new Issue()); + } + + @Override + public Issue update(@NonNull GHIssue source, @NonNull Issue issue) { + convertBaseFields(source, issue); + issue.setNumber(source.getNumber()); + issue.setState(convertState(source.getState())); + issue.setTitle(source.getTitle()); + issue.setBody(source.getBody()); + issue.setHtmlUrl(source.getHtmlUrl().toString()); + issue.setLocked(source.isLocked()); + issue.setClosedAt(DateUtil.convertToOffsetDateTime(source.getClosedAt())); + issue.setCommentsCount(issue.getCommentsCount()); + return issue; + } + + private Issue.State convertState(GHIssueState state) { + switch (state) { + case OPEN: + return Issue.State.OPEN; + case CLOSED: + return Issue.State.CLOSED; + default: + return Issue.State.CLOSED; + } + } +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequest.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequest.java new file mode 100644 index 00000000..58c4c8d4 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequest.java @@ -0,0 +1,78 @@ +package de.tum.cit.aet.helios.gitprovider.pullrequest; + +import de.tum.cit.aet.helios.gitprovider.issue.Issue; +import de.tum.cit.aet.helios.gitprovider.user.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.OffsetDateTime; +import java.util.HashSet; +import java.util.Set; + +@Entity +@DiscriminatorValue(value = "PULL_REQUEST") +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class PullRequest extends Issue { + + private OffsetDateTime mergedAt; + + private String mergeCommitSha; + + private boolean isDraft; + + private boolean isMerged; + + private Boolean isMergeable; + + private String mergeableState; + + // Indicates whether maintainers can modify the pull request. + private boolean maintainerCanModify; + + private int commits; + + private int additions; + + private int deletions; + + private int changedFiles; + + @ManyToOne + @JoinColumn(name = "merged_by_id") + @ToString.Exclude + private User mergedBy; + + @ManyToMany + @JoinTable( + name = "pull_request_requested_reviewers", + joinColumns = @JoinColumn(name = "pull_request_id"), + inverseJoinColumns = @JoinColumn(name = "user_id") + ) + @ToString.Exclude + private Set requestedReviewers = new HashSet<>(); + + @Override + public boolean isPullRequest() { + return true; + } + + // Missing properties: + // - PullRequestReview + // - PullRequestReviewComment + + + // Ignored GitHub properties: + // - rebaseable (not provided by our GitHub API client) + // - head -> "label", "ref", "repo", "sha", "user" + // - base -> "label", "ref", "repo", "sha", "user" + // - auto_merge + // - requested_teams + // - comments (cached number) + // - review_comments (cached number) +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequestBaseInfoDTO.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequestBaseInfoDTO.java new file mode 100644 index 00000000..7c5c9697 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequestBaseInfoDTO.java @@ -0,0 +1,44 @@ +package de.tum.cit.aet.helios.gitprovider.pullrequest; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.helios.gitprovider.issue.Issue; +import de.tum.cit.aet.helios.gitprovider.issue.Issue.State; +import de.tum.cit.aet.helios.gitprovider.repository.RepositoryInfoDTO; +import org.springframework.lang.NonNull; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PullRequestBaseInfoDTO( + @NonNull Long id, + @NonNull Integer number, + @NonNull String title, + @NonNull State state, + @NonNull Boolean isDraft, + @NonNull Boolean isMerged, + RepositoryInfoDTO repository, + @NonNull String htmlUrl) { + + public static PullRequestBaseInfoDTO fromPullRequest(PullRequest pullRequest) { + return new PullRequestBaseInfoDTO( + pullRequest.getId(), + pullRequest.getNumber(), + pullRequest.getTitle(), + pullRequest.getState(), + pullRequest.isDraft(), + pullRequest.isMerged(), + RepositoryInfoDTO.fromRepository(pullRequest.getRepository()), + pullRequest.getHtmlUrl()); + } + + public static PullRequestBaseInfoDTO fromIssue(Issue issue) { + return new PullRequestBaseInfoDTO( + issue.getId(), + issue.getNumber(), + issue.getTitle(), + issue.getState(), + false, + false, + RepositoryInfoDTO.fromRepository(issue.getRepository()), + issue.getHtmlUrl()); + } +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequestInfoDTO.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequestInfoDTO.java new file mode 100644 index 00000000..7e461ea4 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequestInfoDTO.java @@ -0,0 +1,58 @@ +package de.tum.cit.aet.helios.gitprovider.pullrequest; + +import com.fasterxml.jackson.annotation.JsonInclude; +import de.tum.cit.aet.helios.gitprovider.issue.Issue.State; +import de.tum.cit.aet.helios.gitprovider.repository.RepositoryInfoDTO; +import de.tum.cit.aet.helios.gitprovider.user.UserInfoDTO; +import org.springframework.lang.NonNull; + +import java.time.OffsetDateTime; +import java.util.Comparator; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PullRequestInfoDTO( + @NonNull Long id, + @NonNull Integer number, + @NonNull String title, + @NonNull State state, + @NonNull Boolean isDraft, + @NonNull Boolean isMerged, + @NonNull Integer commentsCount, + UserInfoDTO author, + List assignees, + RepositoryInfoDTO repository, + @NonNull Integer additions, + @NonNull Integer deletions, + OffsetDateTime mergedAt, + OffsetDateTime closedAt, + @NonNull String htmlUrl, + OffsetDateTime createdAt, + OffsetDateTime updatedAt) { + + public static PullRequestInfoDTO fromPullRequest(PullRequest pullRequest) { + return new PullRequestInfoDTO( + pullRequest.getId(), + pullRequest.getNumber(), + pullRequest.getTitle(), + pullRequest.getState(), + pullRequest.isDraft(), + pullRequest.isMerged(), + pullRequest.getCommentsCount(), + UserInfoDTO.fromUser(pullRequest.getAuthor()), + pullRequest.getAssignees() + .stream() + .map(UserInfoDTO::fromUser) + .sorted(Comparator.comparing(UserInfoDTO::login)) + .toList(), + RepositoryInfoDTO.fromRepository(pullRequest.getRepository()), + pullRequest.getAdditions(), + pullRequest.getDeletions(), + pullRequest.getMergedAt(), + pullRequest.getClosedAt(), + pullRequest.getHtmlUrl(), + pullRequest.getCreatedAt(), + pullRequest.getUpdatedAt()); + } + +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequestRepository.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequestRepository.java new file mode 100644 index 00000000..5e9f8328 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/PullRequestRepository.java @@ -0,0 +1,10 @@ +package de.tum.cit.aet.helios.gitprovider.pullrequest; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + + +@Repository +public interface PullRequestRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/github/GitHubPullRequestConverter.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/github/GitHubPullRequestConverter.java new file mode 100644 index 00000000..57565a9f --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/github/GitHubPullRequestConverter.java @@ -0,0 +1,95 @@ +package de.tum.cit.aet.helios.gitprovider.pullrequest.github; + +import de.tum.cit.aet.helios.gitprovider.common.github.BaseGitServiceEntityConverter; +import de.tum.cit.aet.helios.gitprovider.common.github.DateUtil; +import de.tum.cit.aet.helios.gitprovider.issue.github.GitHubIssueConverter; +import de.tum.cit.aet.helios.gitprovider.pullrequest.PullRequest; +import org.kohsuke.github.GHPullRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class GitHubPullRequestConverter extends BaseGitServiceEntityConverter { + + private static final Logger logger = LoggerFactory.getLogger(GitHubPullRequestConverter.class); + + private final GitHubIssueConverter issueConverter; + + public GitHubPullRequestConverter(GitHubIssueConverter issueConverter) { + this.issueConverter = issueConverter; + } + + @Override + public PullRequest convert(@NonNull GHPullRequest source) { + return update(source, new PullRequest()); + } + + @Override + public PullRequest update(@NonNull GHPullRequest source, @NonNull PullRequest pullRequest) { + issueConverter.update(source, pullRequest); + + pullRequest.setMergedAt(DateUtil.convertToOffsetDateTime(source.getMergedAt())); + try { + pullRequest.setMergeCommitSha(source.getMergeCommitSha()); + } catch (IOException e) { + logger.error("Failed to convert mergeCommitSha field for source {}: {}", source.getId(), e.getMessage()); + } + try { + pullRequest.setDraft(source.isDraft()); + } catch (IOException e) { + logger.error("Failed to convert draft field for source {}: {}", source.getId(), e.getMessage()); + } + try { + pullRequest.setMerged(source.isMerged()); + } catch (IOException e) { + logger.error("Failed to convert merged field for source {}: {}", source.getId(), e.getMessage()); + } + try { + if (source.getMergeable() != null) { + pullRequest.setIsMergeable(Boolean.TRUE.equals(source.getMergeable())); + } + } catch (IOException e) { + logger.error("Failed to convert mergeable field for source {}: {}", source.getId(), e.getMessage()); + } + try { + pullRequest.setMergeableState(source.getMergeableState()); + } catch (IOException e) { + logger.error("Failed to convert mergeableState field for source {}: {}", source.getId(), e.getMessage()); + } + try { + pullRequest.setMaintainerCanModify(source.canMaintainerModify()); + } catch (IOException e) { + logger.error("Failed to convert maintainerCanModify field for source {}: {}", source.getId(), e.getMessage()); + } + try { + pullRequest.setCommits(source.getCommits()); + } catch (IOException e) { + logger.error("Failed to convert commits field for source {}: {}", source.getId(), e.getMessage()); + } + try { + if (pullRequest.getAdditions() == 0 || source.getAdditions() != 0) { + pullRequest.setAdditions(source.getAdditions()); + } + } catch (IOException e) { + logger.error("Failed to convert additions field for source {}: {}", source.getId(), e.getMessage()); + } + try { + if (pullRequest.getDeletions() == 0 || source.getDeletions() != 0) { + pullRequest.setDeletions(source.getDeletions()); + } + } catch (IOException e) { + logger.error("Failed to convert deletions field for source {}: {}", source.getId(), e.getMessage()); + } + try { + pullRequest.setChangedFiles(source.getChangedFiles()); + } catch (IOException e) { + logger.error("Failed to convert changedFiles field for source {}: {}", source.getId(), e.getMessage()); + } + + return pullRequest; + } +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/github/GitHubPullRequestMessageHandler.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/github/GitHubPullRequestMessageHandler.java new file mode 100644 index 00000000..24b55c59 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/github/GitHubPullRequestMessageHandler.java @@ -0,0 +1,42 @@ +package de.tum.cit.aet.helios.gitprovider.pullrequest.github; + +import de.tum.cit.aet.helios.gitprovider.common.github.GitHubMessageHandler; +import de.tum.cit.aet.helios.gitprovider.repository.github.GitHubRepositorySyncService; +import org.kohsuke.github.GHEvent; +import org.kohsuke.github.GHEventPayload; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class GitHubPullRequestMessageHandler extends GitHubMessageHandler { + + private static final Logger logger = LoggerFactory.getLogger(GitHubPullRequestMessageHandler.class); + + private final GitHubPullRequestSyncService pullRequestSyncService; + private final GitHubRepositorySyncService repositorySyncService; + + private GitHubPullRequestMessageHandler( + GitHubPullRequestSyncService pullRequestSyncService, + GitHubRepositorySyncService repositorySyncService) { + super(GHEventPayload.PullRequest.class); + this.pullRequestSyncService = pullRequestSyncService; + this.repositorySyncService = repositorySyncService; + } + + @Override + protected void handleEvent(GHEventPayload.PullRequest eventPayload) { + logger.info("Received pull request event for repository: {}, pull request: {}, action: {}", + eventPayload.getRepository().getFullName(), + eventPayload.getPullRequest().getNumber(), + eventPayload.getAction()); + repositorySyncService.processRepository(eventPayload.getRepository()); + // We don't need to handle the deleted action here, as pull requests are not deleted + pullRequestSyncService.processPullRequest(eventPayload.getPullRequest()); + } + + @Override + protected GHEvent getHandlerEvent() { + return GHEvent.PULL_REQUEST; + } +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/github/GitHubPullRequestSyncService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/github/GitHubPullRequestSyncService.java new file mode 100644 index 00000000..e331d1e4 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/pullrequest/github/GitHubPullRequestSyncService.java @@ -0,0 +1,207 @@ +package de.tum.cit.aet.helios.gitprovider.pullrequest.github; + + +import de.tum.cit.aet.helios.gitprovider.common.github.DateUtil; +import de.tum.cit.aet.helios.gitprovider.pullrequest.PullRequest; +import de.tum.cit.aet.helios.gitprovider.pullrequest.PullRequestRepository; +import de.tum.cit.aet.helios.gitprovider.repository.RepositoryRepository; +import de.tum.cit.aet.helios.gitprovider.user.UserRepository; +import de.tum.cit.aet.helios.gitprovider.user.User; +import de.tum.cit.aet.helios.gitprovider.user.github.GitHubUserConverter; +import jakarta.transaction.Transactional; +import org.kohsuke.github.GHDirection; +import org.kohsuke.github.GHIssueState; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHPullRequestQueryBuilder.Sort; +import org.kohsuke.github.GHRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.*; + +@Service +public class GitHubPullRequestSyncService { + + private static final Logger logger = LoggerFactory.getLogger(GitHubPullRequestSyncService.class); + + private final PullRequestRepository pullRequestRepository; + private final RepositoryRepository repositoryRepository; + private final UserRepository userRepository; + private final GitHubPullRequestConverter pullRequestConverter; + private final GitHubUserConverter userConverter; + + public GitHubPullRequestSyncService( + PullRequestRepository pullRequestRepository, + RepositoryRepository repositoryRepository, + UserRepository userRepository, + GitHubPullRequestConverter pullRequestConverter, + GitHubUserConverter userConverter) { + this.pullRequestRepository = pullRequestRepository; + this.repositoryRepository = repositoryRepository; + this.userRepository = userRepository; + this.pullRequestConverter = pullRequestConverter; + this.userConverter = userConverter; + } + + /** + * Synchronizes all pull requests from the specified GitHub repositories. + * + * @param repositories the list of GitHub repositories to sync pull requests + * from + * @param since an optional date to filter pull requests by their last + * update + * @return a list of GitHub pull requests that were successfully fetched and + * processed + */ + public List syncPullRequestsOfAllRepositories(List repositories, + Optional since) { + return repositories.stream() + .map(repository -> syncPullRequestsOfRepository(repository, since)) + .flatMap(List::stream) + .toList(); + } + + /** + * Synchronizes all pull requests from a specific GitHub repository. + * + * @param repository the GitHub repository to sync pull requests from + * @param since an optional date to filter pull requests by their last + * update + * @return a list of GitHub pull requests that were successfully fetched and + * processed + */ + public List syncPullRequestsOfRepository(GHRepository repository, Optional since) { + var iterator = repository.queryPullRequests() + .state(GHIssueState.ALL) + .sort(Sort.UPDATED) + .direction(GHDirection.DESC) + .list() + .withPageSize(100) + .iterator(); + + var sinceDate = since.map(date -> Date.from(date.toInstant())); + + var pullRequests = new ArrayList(); + while (iterator.hasNext()) { + var ghPullRequests = iterator.nextPage(); + var keepPullRequests = ghPullRequests.stream() + .filter(pullRequest -> { + try { + return sinceDate.isEmpty() || pullRequest.getUpdatedAt().after(sinceDate.get()); + } catch (IOException e) { + logger.error("Failed to filter pull request {}: {}", pullRequest.getId(), e.getMessage()); + return false; + } + }) + .toList(); + + pullRequests.addAll(keepPullRequests); + if (keepPullRequests.size() != ghPullRequests.size()) { + break; + } + } + + pullRequests.forEach(this::processPullRequest); + return pullRequests; + } + + /** + * Processes a single GitHub pull request by updating or creating it in the + * local repository. + * Manages associations with repositories, labels, milestones, authors, + * assignees, merged by users, + * and requested reviewers. + * + * @param ghPullRequest the GitHub pull request to process + * @return the updated or newly created PullRequest entity, or {@code null} if + * an error occurred + */ + @Transactional + public PullRequest processPullRequest(GHPullRequest ghPullRequest) { + var result = pullRequestRepository.findById(ghPullRequest.getId()) + .map(pullRequest -> { + try { + if (pullRequest.getUpdatedAt() == null || pullRequest.getUpdatedAt() + .isBefore(DateUtil.convertToOffsetDateTime(ghPullRequest.getUpdatedAt()))) { + return pullRequestConverter.update(ghPullRequest, pullRequest); + } + return pullRequest; + } catch (IOException e) { + logger.error("Failed to update pull request {}: {}", ghPullRequest.getId(), e.getMessage()); + return null; + } + }).orElseGet(() -> pullRequestConverter.convert(ghPullRequest)); + + if (result == null) { + return null; + } + + // Link with existing repository if not already linked + if (result.getRepository() == null) { + // Extract name with owner from the repository URL + // Example: https://api.github.com/repos/ls1intum/Artemis/pulls/9463 + var nameWithOwner = ghPullRequest.getUrl().toString().split("/repos/")[1].split("/pulls")[0]; + var repository = repositoryRepository.findByNameWithOwner(nameWithOwner); + if (repository != null) { + result.setRepository(repository); + } + } + + // Link author + try { + var author = ghPullRequest.getUser(); + var resultAuthor = userRepository.findById(author.getId()) + .orElseGet(() -> userRepository.save(userConverter.convert(author))); + result.setAuthor(resultAuthor); + } catch (IOException e) { + logger.error("Failed to link author for pull request {}: {}", ghPullRequest.getId(), e.getMessage()); + } + + // Link assignees + var assignees = ghPullRequest.getAssignees(); + var resultAssignees = new HashSet(); + assignees.forEach(assignee -> { + var resultAssignee = userRepository.findById(assignee.getId()) + .orElseGet(() -> userRepository.save(userConverter.convert(assignee))); + resultAssignees.add(resultAssignee); + }); + result.getAssignees().clear(); + result.getAssignees().addAll(resultAssignees); + + // Link merged by + try { + var mergedByUser = ghPullRequest.getMergedBy(); + if (mergedByUser != null) { + var resultMergedBy = userRepository.findById(ghPullRequest.getMergedBy().getId()) + .orElseGet(() -> userRepository.save(userConverter.convert(mergedByUser))); + result.setMergedBy(resultMergedBy); + } else { + result.setMergedBy(null); + } + } catch (IOException e) { + logger.error("Failed to link merged by user for pull request {}: {}", ghPullRequest.getId(), + e.getMessage()); + } + + // Link requested reviewers + try { + var requestedReviewers = ghPullRequest.getRequestedReviewers(); + var resultRequestedReviewers = new HashSet(); + requestedReviewers.forEach(requestedReviewer -> { + var resultRequestedReviewer = userRepository.findById(requestedReviewer.getId()) + .orElseGet(() -> userRepository.save(userConverter.convert(requestedReviewer))); + resultRequestedReviewers.add(resultRequestedReviewer); + }); + result.getRequestedReviewers().clear(); + result.getRequestedReviewers().addAll(resultRequestedReviewers); + } catch (IOException e) { + logger.error("Failed to link requested reviewers for pull request {}: {}", ghPullRequest.getId(), + e.getMessage()); + } + + return pullRequestRepository.save(result); + } +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/Repository.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/Repository.java new file mode 100644 index 00000000..3c49282c --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/Repository.java @@ -0,0 +1,95 @@ +package de.tum.cit.aet.helios.gitprovider.repository; + +import de.tum.cit.aet.helios.gitprovider.common.github.BaseGitServiceEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springframework.lang.NonNull; + +import java.time.OffsetDateTime; + +@Entity +@Table(name = "repository") +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class Repository extends BaseGitServiceEntity { + + @NonNull + private String name; + + @NonNull + private String nameWithOwner; + + // Whether the repository is private or public. + private boolean isPrivate; + + @NonNull + private String htmlUrl; + + private String description; + + private String homepage; + + @NonNull + private OffsetDateTime pushedAt; + + private boolean isArchived; + + // Returns whether this repository disabled. + private boolean isDisabled; + + @NonNull + @Enumerated(EnumType.STRING) + private Visibility visibility; + + private int stargazersCount; + + private int watchersCount; + + @NonNull + private String defaultBranch; + + private boolean hasIssues; + + private boolean hasProjects; + + private boolean hasWiki; + + public enum Visibility { + PUBLIC, PRIVATE, INTERNAL, UNKNOWN + } + + // Missing properties: + // Issue, Label, Milestone + // owner + // organization + + // Ignored GitHub properties: + // - subscribersCount + // - hasPages + // - hasDownloads + // - hasDiscussions + // - topics + // - size + // - fork + // - forks_count + // - default_branch + // - open_issues_count (cached number) + // - is_template + // - permissions + // - allow_rebase_merge + // - template_repository + // - allow_squash_merge + // - allow_auto_merge + // - delete_branch_on_merge + // - allow_merge_commit + // - allow_forking + // - network_count + // - license + // - parent + // - source +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/RepositoryInfoDTO.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/RepositoryInfoDTO.java new file mode 100644 index 00000000..903bb76b --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/RepositoryInfoDTO.java @@ -0,0 +1,22 @@ +package de.tum.cit.aet.helios.gitprovider.repository; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.springframework.lang.NonNull; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record RepositoryInfoDTO( + @NonNull Long id, + @NonNull String name, + @NonNull String nameWithOwner, + String description, + @NonNull String htmlUrl) { + + public static RepositoryInfoDTO fromRepository(Repository repository) { + return new RepositoryInfoDTO( + repository.getId(), + repository.getName(), + repository.getNameWithOwner(), + repository.getDescription(), + repository.getHtmlUrl()); + } +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/RepositoryRepository.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/RepositoryRepository.java new file mode 100644 index 00000000..850e9e0e --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/RepositoryRepository.java @@ -0,0 +1,10 @@ +package de.tum.cit.aet.helios.gitprovider.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +@org.springframework.stereotype.Repository +public interface RepositoryRepository + extends JpaRepository { + + Repository findByNameWithOwner(String nameWithOwner); +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/github/GitHubRepositoryConverter.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/github/GitHubRepositoryConverter.java new file mode 100644 index 00000000..10499585 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/github/GitHubRepositoryConverter.java @@ -0,0 +1,58 @@ +package de.tum.cit.aet.helios.gitprovider.repository.github; + +import de.tum.cit.aet.helios.gitprovider.common.github.BaseGitServiceEntityConverter; +import de.tum.cit.aet.helios.gitprovider.common.github.DateUtil; +import de.tum.cit.aet.helios.gitprovider.repository.Repository; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHRepository.Visibility; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +@Component +public class GitHubRepositoryConverter extends BaseGitServiceEntityConverter { + + private static final Logger logger = LoggerFactory.getLogger(GitHubRepositoryConverter.class); + + @Override + public Repository convert(@NonNull GHRepository source) { + return update(source, new Repository()); + } + + @Override + public Repository update(@NonNull GHRepository source, @NonNull Repository repository) { + convertBaseFields(source, repository); + repository.setName(source.getName()); + repository.setNameWithOwner(source.getFullName()); + repository.setPrivate(source.isPrivate()); + repository.setHtmlUrl(source.getHtmlUrl().toString()); + repository.setDescription(source.getDescription()); + repository.setHomepage(source.getHomepage()); + repository.setPushedAt(DateUtil.convertToOffsetDateTime(source.getPushedAt())); + repository.setArchived(source.isArchived()); + repository.setDisabled(source.isDisabled()); + repository.setVisibility(convertVisibility(source.getVisibility())); + repository.setStargazersCount(source.getStargazersCount()); + repository.setWatchersCount(source.getWatchersCount()); + repository.setDefaultBranch(source.getDefaultBranch()); + repository.setHasIssues(source.hasIssues()); + repository.setHasProjects(source.hasProjects()); + repository.setHasWiki(source.hasWiki()); + return repository; + } + + private Repository.Visibility convertVisibility(Visibility visibility) { + switch (visibility) { + case PRIVATE: + return Repository.Visibility.PRIVATE; + case PUBLIC: + return Repository.Visibility.PUBLIC; + case INTERNAL: + return Repository.Visibility.INTERNAL; + default: + logger.error("Unknown repository visibility: {}", visibility); + return Repository.Visibility.UNKNOWN; + } + } +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/github/GitHubRepositorySyncService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/github/GitHubRepositorySyncService.java new file mode 100644 index 00000000..c4bebdbe --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/repository/github/GitHubRepositorySyncService.java @@ -0,0 +1,128 @@ +package de.tum.cit.aet.helios.gitprovider.repository.github; + + +import de.tum.cit.aet.helios.gitprovider.common.github.DateUtil; +import de.tum.cit.aet.helios.gitprovider.repository.Repository; +import de.tum.cit.aet.helios.gitprovider.repository.RepositoryRepository; +import jakarta.transaction.Transactional; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +@Service +public class GitHubRepositorySyncService { + + private static final Logger logger = LoggerFactory.getLogger(GitHubRepositorySyncService.class); + + @Value("${monitoring.repositories}") + private String[] repositoriesToMonitor; + + private final GitHub github; + private final RepositoryRepository repositoryRepository; + private final GitHubRepositoryConverter repositoryConverter; + + public GitHubRepositorySyncService( + GitHub github, + RepositoryRepository repositoryRepository, + GitHubRepositoryConverter repositoryConverter) { + this.github = github; + this.repositoryRepository = repositoryRepository; + this.repositoryConverter = repositoryConverter; + } + + /** + * Syncs all monitored GitHub repositories. + * + * @return A list of successfully fetched GitHub repositories. + */ + public List syncAllMonitoredRepositories() { + return syncAllRepositories(List.of(repositoriesToMonitor)); + } + + /** + * Syncs all repositories owned by a specific GitHub user or organization. + * + * @param owner The GitHub username (login) of the repository owner. + */ + public void syncAllRepositoriesOfOwner(String owner) { + var builder = github.searchRepositories().user(owner); + var iterator = builder.list().withPageSize(100).iterator(); + while (iterator.hasNext()) { + var ghRepositories = iterator.nextPage(); + ghRepositories.forEach(this::processRepository); + } + } + + /** + * Syncs a list of repositories specified by their full names (e.g., + * "owner/repo"). + * + * @param nameWithOwners A list of repository full names in the format + * "owner/repo". + * @return A list of successfully fetched GitHub repositories. + */ + public List syncAllRepositories(List nameWithOwners) { + return nameWithOwners.stream() + .map(this::syncRepository) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + /** + * Syncs a single GitHub repository by its full name (e.g., "owner/repo"). + * + * @param nameWithOwner The full name of the repository in the format + * "owner/repo". + * @return An optional containing the fetched GitHub repository, or an empty + * optional if the repository could not be fetched. + */ + public Optional syncRepository(String nameWithOwner) { + try { + var repository = github.getRepository(nameWithOwner); + processRepository(repository); + return Optional.of(repository); + } catch (IOException e) { + logger.error("Failed to fetch repository {}: {}", nameWithOwner, e.getMessage()); + return Optional.empty(); + } + } + + /** + * Processes a single GitHub repository by updating or creating it in the local + * repository. + * + * @param ghRepository The GitHub repository data to process. + * @return The updated or newly created Repository entity, or {@code null} if an + * error occurred during update. + */ + @Transactional + public Repository processRepository(GHRepository ghRepository) { + var result = repositoryRepository.findById(ghRepository.getId()) + .map(repository -> { + try { + if (repository.getUpdatedAt() == null || repository.getUpdatedAt() + .isBefore(DateUtil.convertToOffsetDateTime(ghRepository.getUpdatedAt()))) { + return repositoryConverter.update(ghRepository, repository); + } + return repository; + } catch (IOException e) { + logger.error("Failed to update repository {}: {}", ghRepository.getId(), e.getMessage()); + return null; + } + }).orElseGet(() -> repositoryConverter.convert(ghRepository)); + + if (result == null) { + return null; + } + + return repositoryRepository.save(result); + } +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/User.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/User.java new file mode 100644 index 00000000..746abcea --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/User.java @@ -0,0 +1,82 @@ +package de.tum.cit.aet.helios.gitprovider.user; + + +import de.tum.cit.aet.helios.gitprovider.common.github.BaseGitServiceEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springframework.lang.NonNull; + + +@Entity +@Table(name = "user", schema = "public") +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class User extends BaseGitServiceEntity { + + @NonNull + private String login; + + @NonNull + private String avatarUrl; + + // AKA bio + private String description; + + @NonNull + // Equals login if not fetched / existing + private String name; + + private String company; + + // Url + private String blog; + + private String location; + + private String email; + + @NonNull + private String htmlUrl; + + @NonNull + @Enumerated(EnumType.STRING) + private Type type; + + private int followers; + + private int following; + + public enum Type { + USER, ORGANIZATION, BOT + } + + // Missing properties: + // - createdIssues + // - assignedIssues + // - issueComments + // - mergedPullRequests + // - requestedPullRequestReviews + // - reviews + // - reviewComments + + + // Ignored GitHub properties: + // - totalPrivateRepos + // - ownedPrivateRepos + // - publicRepos + // - publicGists + // - privateGists + // - collaborators + // - is_verified (org?) + // - disk_usage + // - suspended_at (user) + // - twitter_username + // - billing_email (org) + // - has_organization_projects (org) + // - has_repository_projects (org) +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/UserInfoDTO.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/UserInfoDTO.java new file mode 100644 index 00000000..0d6aacd3 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/UserInfoDTO.java @@ -0,0 +1,22 @@ +package de.tum.cit.aet.helios.gitprovider.user; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.springframework.lang.NonNull; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record UserInfoDTO( + @NonNull Long id, + @NonNull String login, + @NonNull String avatarUrl, + @NonNull String name, + @NonNull String htmlUrl) { + + public static UserInfoDTO fromUser(User user) { + return new UserInfoDTO( + user.getId(), + user.getLogin(), + user.getAvatarUrl(), + user.getName(), + user.getHtmlUrl()); + } +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/UserRepository.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/UserRepository.java new file mode 100644 index 00000000..3320c5f9 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/UserRepository.java @@ -0,0 +1,7 @@ +package de.tum.cit.aet.helios.gitprovider.user; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/github/GitHubUserConverter.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/github/GitHubUserConverter.java new file mode 100644 index 00000000..1a542151 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/github/GitHubUserConverter.java @@ -0,0 +1,88 @@ +package de.tum.cit.aet.helios.gitprovider.user.github; + + +import de.tum.cit.aet.helios.gitprovider.common.github.BaseGitServiceEntityConverter; +import de.tum.cit.aet.helios.gitprovider.user.User; +import org.kohsuke.github.GHUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class GitHubUserConverter extends BaseGitServiceEntityConverter { + + private static final Logger logger = LoggerFactory.getLogger(GitHubUserConverter.class); + + @Override + public User convert(@NonNull GHUser source) { + return update(source, new User()); + } + + @Override + public User update(@NonNull GHUser source, @NonNull User user) { + convertBaseFields(source, user); + user.setLogin(source.getLogin()); + user.setAvatarUrl(source.getAvatarUrl()); + user.setDescription(source.getBio()); + user.setHtmlUrl(source.getHtmlUrl().toString()); + try { + user.setName(source.getName() != null ? source.getName() : source.getLogin()); + } catch (IOException e) { + logger.error("Failed to convert user name field for source {}: {}", source.getId(), e.getMessage()); + user.setName(source.getLogin()); + } + try { + user.setCompany(source.getCompany()); + } catch (IOException e) { + logger.error("Failed to convert user company field for source {}: {}", source.getId(), e.getMessage()); + } + try { + user.setBlog(source.getBlog()); + } catch (IOException e) { + logger.error("Failed to convert user blog field for source {}: {}", source.getId(), e.getMessage()); + } + try { + user.setLocation(source.getLocation()); + } catch (IOException e) { + logger.error("Failed to convert user location field for source {}: {}", source.getId(), e.getMessage()); + } + try { + user.setEmail(source.getEmail()); + } catch (IOException e) { + logger.error("Failed to convert user email field for source {}: {}", source.getId(), e.getMessage()); + } + try { + user.setType(convertUserType(source.getType())); + } catch (IOException e) { + logger.error("Failed to convert user type field for source {}: {}", source.getId(), e.getMessage()); + } + try { + user.setFollowers(source.getFollowersCount()); + } catch (IOException e) { + logger.error("Failed to convert user followers field for source {}: {}", source.getId(), e.getMessage()); + } + try { + user.setFollowing(source.getFollowingCount()); + } catch (IOException e) { + logger.error("Failed to convert user following field for source {}: {}", source.getId(), e.getMessage()); + } + return user; + } + + private User.Type convertUserType(String type) { + switch (type) { + case "User": + return User.Type.USER; + case "Organization": + return User.Type.ORGANIZATION; + case "Bot": + return User.Type.BOT; + default: + return User.Type.USER; + } + } + +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/github/GitHubUserSyncService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/github/GitHubUserSyncService.java new file mode 100644 index 00000000..44f411e8 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/gitprovider/user/github/GitHubUserSyncService.java @@ -0,0 +1,88 @@ +package de.tum.cit.aet.helios.gitprovider.user.github; + +import de.tum.cit.aet.helios.gitprovider.common.github.DateUtil; +import de.tum.cit.aet.helios.gitprovider.user.User; +import de.tum.cit.aet.helios.gitprovider.user.UserRepository; +import jakarta.transaction.Transactional; +import org.kohsuke.github.GHUser; +import org.kohsuke.github.GitHub; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.IOException; + +@Service +public class GitHubUserSyncService { + + private static final Logger logger = LoggerFactory.getLogger(GitHubUserSyncService.class); + + private final GitHub github; + private final UserRepository userRepository; + private final GitHubUserConverter userConverter; + + public GitHubUserSyncService( + GitHub github, + UserRepository userRepository, + GitHubUserConverter userConverter) { + this.github = github; + this.userRepository = userRepository; + this.userConverter = userConverter; + } + + /** + * Sync all existing users in the local repository with their GitHub + * data. + */ + public void syncAllExistingUsers() { + userRepository.findAll() + .stream() + .map(User::getLogin) + .forEach(this::syncUser); + } + + /** + * Sync a GitHub user's data by their login and processes it to synchronize + * with the local repository. + * + * @param login The GitHub username (login) of the user to fetch. + */ + public void syncUser(String login) { + try { + processUser(github.getUser(login)); + } catch (IOException e) { + logger.error("Failed to fetch user {}: {}", login, e.getMessage()); + } + } + + /** + * Processes a GitHub user by either updating the existing user in the + * repository or creating a new one. + * + * @param ghUser The GitHub user data to process. + * @return The updated or newly created User entity, or {@code null} if an error + * occurred during update. + */ + @Transactional + public User processUser(GHUser ghUser) { + var result = userRepository.findById(ghUser.getId()) + .map(user -> { + try { + if (user.getUpdatedAt() == null || user.getUpdatedAt() + .isBefore(DateUtil.convertToOffsetDateTime(ghUser.getUpdatedAt()))) { + return userConverter.update(ghUser, user); + } + return user; + } catch (IOException e) { + logger.error("Failed to update repository {}: {}", ghUser.getId(), e.getMessage()); + return null; + } + }).orElseGet(() -> userConverter.convert(ghUser)); + + if (result == null) { + return null; + } + + return userRepository.save(result); + } +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/DataSyncStatus.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/DataSyncStatus.java new file mode 100644 index 00000000..19688252 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/DataSyncStatus.java @@ -0,0 +1,33 @@ +package de.tum.cit.aet.helios.syncing; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.lang.NonNull; + +import java.time.OffsetDateTime; + +@Entity +@Table(name = "data_sync_status") +@Getter +@Setter +@NoArgsConstructor +public class DataSyncStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + protected Long id; + + @NonNull + private OffsetDateTime startTime; + + @NonNull + private OffsetDateTime endTime; + + public enum Status { + SUCCESS, + FAILED, + IN_PROGRESS + } +} diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/DataSyncStatusRepository.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/DataSyncStatusRepository.java new file mode 100644 index 00000000..fa61a2ed --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/DataSyncStatusRepository.java @@ -0,0 +1,12 @@ +package de.tum.cit.aet.helios.syncing; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface DataSyncStatusRepository extends JpaRepository { + + public Optional findTopByOrderByStartTimeDesc(); +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/GitHubDataSyncScheduler.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/GitHubDataSyncScheduler.java new file mode 100644 index 00000000..e34451a1 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/GitHubDataSyncScheduler.java @@ -0,0 +1,41 @@ +package de.tum.cit.aet.helios.syncing; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Order(value = 2) +@Component +public class GitHubDataSyncScheduler { + + private static final Logger logger = LoggerFactory.getLogger(GitHubDataSyncScheduler.class); + private final GitHubDataSyncService dataSyncService; + + @Value("${monitoring.runOnStartup:true}") + private boolean runOnStartup; + + public GitHubDataSyncScheduler(GitHubDataSyncService dataSyncService) { + this.dataSyncService = dataSyncService; + } + + @EventListener(ApplicationReadyEvent.class) + public void run() { + if (runOnStartup) { + logger.info("Starting initial GitHub data sync..."); + dataSyncService.syncData(); + logger.info("Initial GitHub data sync completed."); + } + } + + @Scheduled(cron = "${monitoring.repository-sync-cron}") + public void syncDataCron() { + logger.info("Starting scheduled GitHub data sync..."); + dataSyncService.syncData(); + logger.info("Scheduled GitHub data sync completed."); + } +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/GitHubDataSyncService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/GitHubDataSyncService.java new file mode 100644 index 00000000..2be467bb --- /dev/null +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/GitHubDataSyncService.java @@ -0,0 +1,76 @@ +package de.tum.cit.aet.helios.syncing; + + +import de.tum.cit.aet.helios.gitprovider.pullrequest.github.GitHubPullRequestSyncService; +import de.tum.cit.aet.helios.gitprovider.repository.github.GitHubRepositorySyncService; +import de.tum.cit.aet.helios.gitprovider.user.github.GitHubUserSyncService; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.time.OffsetDateTime; +import java.util.Optional; + +@Service +public class GitHubDataSyncService { + + private static final Logger logger = LoggerFactory.getLogger(GitHubDataSyncService.class); + + @Value("${monitoring.timeframe}") + private int timeframe; + + @Value("${monitoring.runOnStartupCooldownInMinutes}") + private int runOnStartupCooldownInMinutes; + + private final DataSyncStatusRepository dataSyncStatusRepository; + private final GitHubUserSyncService userSyncService; + private final GitHubRepositorySyncService repositorySyncService; + private final GitHubPullRequestSyncService pullRequestSyncService; + + public GitHubDataSyncService( + DataSyncStatusRepository dataSyncStatusRepository, GitHubUserSyncService userSyncService, + GitHubRepositorySyncService repositorySyncService, + GitHubPullRequestSyncService pullRequestSyncService) { + this.dataSyncStatusRepository = dataSyncStatusRepository; + this.userSyncService = userSyncService; + this.repositorySyncService = repositorySyncService; + this.pullRequestSyncService = pullRequestSyncService; + } + + @Transactional + public void syncData() { + var cutoffDate = OffsetDateTime.now().minusDays(timeframe); + + // Get last sync time + var lastSync = dataSyncStatusRepository.findTopByOrderByStartTimeDesc(); + System.out.println("lastSync: " + lastSync); + if (lastSync.isPresent()) { + var lastSyncTime = lastSync.get().getStartTime(); + cutoffDate = lastSyncTime.isAfter(cutoffDate) ? lastSyncTime : cutoffDate; + } + + var cooldownTime = OffsetDateTime.now().minusMinutes(runOnStartupCooldownInMinutes); + System.out.println("cooldownTime: " + cooldownTime); + if (lastSync.isPresent() && lastSync.get().getStartTime().isAfter(cooldownTime)) { + logger.info("Skipping sync, last sync was less than {} minutes ago", runOnStartupCooldownInMinutes); + return; + } + + // Start new sync + var startTime = OffsetDateTime.now(); + + var repositories = repositorySyncService.syncAllMonitoredRepositories(); + var pullRequests = pullRequestSyncService.syncPullRequestsOfAllRepositories(repositories, Optional.of(cutoffDate)); + userSyncService.syncAllExistingUsers(); + + var endTime = OffsetDateTime.now(); + + // Store successful sync status + var syncStatus = new DataSyncStatus(); + syncStatus.setStartTime(startTime); + syncStatus.setEndTime(endTime); + dataSyncStatusRepository.save(syncStatus); + } +} diff --git a/server/application-server/src/main/resources/application.yml b/server/application-server/src/main/resources/application.yml index 08f9b818..0af410ee 100644 --- a/server/application-server/src/main/resources/application.yml +++ b/server/application-server/src/main/resources/application.yml @@ -12,7 +12,7 @@ spring: database: POSTGRESQL show-sql: false hibernate: - ddl-auto: create-drop + ddl-auto: update springdoc: default-produces-media-type: application/json @@ -25,8 +25,26 @@ nats: auth: token: ${NATS_AUTH_TOKEN} +github: + organizationName: ${ORGANIZATION_NAME} + # Can be any OAuth token, such as the PAT + authToken: ${GITHUB_AUTH_TOKEN:} + cache: + enabled: true + ttl: 500 + # in MB + size: 50 + monitoring: + # List of repositories to monitor in the format owner/repository + # Example: ls1intum/Helios or ls1intum/Helios,ls1intum/Artemis repositories: ${REPOSITORY_NAME} + runOnStartup: true + # Fetching timeframe in days + timeframe: 7 + # Cooldown in minutes before running the monitoring again + runOnStartupCooldownInMinutes: 15 + repository-sync-cron: "0 0 * * * *" logging: level: From 18e39174e6e588ae21023a2ce397c45398dbde61 Mon Sep 17 00:00:00 2001 From: Ege Kocabas Date: Sun, 17 Nov 2024 17:19:10 +0100 Subject: [PATCH 2/3] remove delete prints --- .../de/tum/cit/aet/helios/syncing/GitHubDataSyncService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/GitHubDataSyncService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/GitHubDataSyncService.java index 2be467bb..3b36ea63 100644 --- a/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/GitHubDataSyncService.java +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/syncing/GitHubDataSyncService.java @@ -45,14 +45,12 @@ public void syncData() { // Get last sync time var lastSync = dataSyncStatusRepository.findTopByOrderByStartTimeDesc(); - System.out.println("lastSync: " + lastSync); if (lastSync.isPresent()) { var lastSyncTime = lastSync.get().getStartTime(); cutoffDate = lastSyncTime.isAfter(cutoffDate) ? lastSyncTime : cutoffDate; } var cooldownTime = OffsetDateTime.now().minusMinutes(runOnStartupCooldownInMinutes); - System.out.println("cooldownTime: " + cooldownTime); if (lastSync.isPresent() && lastSync.get().getStartTime().isAfter(cooldownTime)) { logger.info("Skipping sync, last sync was less than {} minutes ago", runOnStartupCooldownInMinutes); return; From ef0bf798f91a36897287d733ab39c17cf16f0cf1 Mon Sep 17 00:00:00 2001 From: Ege Kocabas Date: Sun, 17 Nov 2024 17:23:58 +0100 Subject: [PATCH 3/3] update env example --- server/application-server/.env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/server/application-server/.env.example b/server/application-server/.env.example index 522344fd..d6ba1180 100644 --- a/server/application-server/.env.example +++ b/server/application-server/.env.example @@ -4,4 +4,5 @@ DATASOURCE_PASSWORD=helios NATS_SERVER=localhost:4222 NATS_AUTH_TOKEN='5760e8ae09adfb2756f9f8cd5cb2caa704cd3f549eaa9298be843ceb165185d815b81f90c680fa7f626b7cd63abf6ac9' REPOSITORY_NAME= +ORGANIZATION_NAME= GITHUB_AUTH_TOKEN=