Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions deployment/config/java-shared/application.properties.sample
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,42 @@ codecrow.github.app.slug= - The URL-friendly app name (e.g., "codecrow")
codecrow.github.app.private-key-path= - Path to the private key .pem file
codecrow.github.app.webhook-secret= - Webhook secret for verification

# ============================================================================
# GitLab OAuth Application Configuration (for 1-click integration)
# ============================================================================
# Create a GitLab OAuth Application:
#
# For GitLab.com:
# 1. Go to https://gitlab.com/-/user_settings/applications
# 2. Click "Add new application"
#
# For Self-Hosted GitLab:
# 1. Go to https://your-gitlab-instance.com/-/user_settings/applications
# 2. Click "Add new application"
#
# Application settings:
# - Name: CodeCrow (or your preferred name)
# - Redirect URI: ${codecrow.web.base.url}/api/integrations/gitlab/app/callback
# Example: https://server.example.com/api/integrations/gitlab/app/callback
# - Confidential: Yes (checked)
# - Scopes (check all):
# * api - Full API access
# * read_user - Read authenticated user's profile
# * read_repository - Read repositories
# * write_repository - Write to repositories (for comments)
#
# After creation:
# - Copy "Application ID" to codecrow.gitlab.oauth.client-id
# - Copy "Secret" to codecrow.gitlab.oauth.client-secret
#
# Note: The redirect URI must match EXACTLY (including trailing slashes)
# ============================================================================
codecrow.gitlab.oauth.client-id=
codecrow.gitlab.oauth.client-secret=
# For self-hosted GitLab instances, set the base URL (leave empty for gitlab.com)
# Example: https://gitlab.mycompany.com
codecrow.gitlab.oauth.base-url=

# Google OAuth Configuration (for social login)
# Create OAuth 2.0 Client ID in Google Cloud Console:
# 1. Go to https://console.cloud.google.com/apis/credentials
Expand Down
Empty file.
2 changes: 2 additions & 0 deletions java-ecosystem/libs/core/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@
opens org.rostilos.codecrow.core.persistence.repository.ai to spring.core, spring.beans, spring.context;
exports org.rostilos.codecrow.core.persistence.repository.vcs;
exports org.rostilos.codecrow.core.model.vcs.config.github;
exports org.rostilos.codecrow.core.model.vcs.config.gitlab;
exports org.rostilos.codecrow.core.model.vcs.config.cloud;
exports org.rostilos.codecrow.core.model.vcs.config;
exports org.rostilos.codecrow.core.dto.bitbucket;
exports org.rostilos.codecrow.core.dto.github;
exports org.rostilos.codecrow.core.dto.gitlab;
exports org.rostilos.codecrow.core.dto.ai;
exports org.rostilos.codecrow.core.dto.user;
exports org.rostilos.codecrow.core.dto.project;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.rostilos.codecrow.core.dto.gitlab;

import org.rostilos.codecrow.core.model.vcs.EVcsConnectionType;
import org.rostilos.codecrow.core.model.vcs.EVcsProvider;
import org.rostilos.codecrow.core.model.vcs.EVcsSetupStatus;
import org.rostilos.codecrow.core.model.vcs.VcsConnection;
import org.rostilos.codecrow.core.model.vcs.config.gitlab.GitLabConfig;

import java.time.LocalDateTime;

public record GitLabDTO(
Long id,
String connectionName,
String groupId,
int repoCount,
EVcsSetupStatus setupStatus,
Boolean hasAccessToken,
LocalDateTime updatedAt,
EVcsConnectionType connectionType,
String repositoryPath
) {
public static GitLabDTO fromVcsConnection(VcsConnection vcsConnection) {
if (vcsConnection.getProviderType() != EVcsProvider.GITLAB) {
throw new IllegalArgumentException("Expected GitLab connection");
}

if (vcsConnection.getConnectionType() == EVcsConnectionType.APP || vcsConnection.getConfiguration() == null) {
return new GitLabDTO(
vcsConnection.getId(),
vcsConnection.getConnectionName(),
vcsConnection.getExternalWorkspaceSlug(),
vcsConnection.getRepoCount(),
vcsConnection.getSetupStatus(),
vcsConnection.getAccessToken() != null && !vcsConnection.getAccessToken().isBlank(),
vcsConnection.getUpdatedAt(),
vcsConnection.getConnectionType(),
vcsConnection.getRepositoryPath()
);
}

GitLabConfig config = (GitLabConfig) vcsConnection.getConfiguration();
return new GitLabDTO(
vcsConnection.getId(),
vcsConnection.getConnectionName(),
config.groupId(),
vcsConnection.getRepoCount(),
vcsConnection.getSetupStatus(),
config.accessToken() != null && !config.accessToken().isBlank(),
vcsConnection.getUpdatedAt(),
vcsConnection.getConnectionType(),
vcsConnection.getRepositoryPath()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public record ProjectDTO(
Boolean prAnalysisEnabled,
Boolean branchAnalysisEnabled,
String installationMethod,
CommentCommandsConfigDTO commentCommandsConfig
CommentCommandsConfigDTO commentCommandsConfig,
Boolean webhooksConfigured
) {
public static ProjectDTO fromProject(Project project) {
Long vcsConnectionId = null;
Expand Down Expand Up @@ -112,6 +113,12 @@ else if (project.getVcsRepoBinding() != null) {
commentCommandsConfigDTO = CommentCommandsConfigDTO.fromConfig(config.getCommentCommandsConfig());
}

// Get webhooksConfigured from VcsRepoBinding
Boolean webhooksConfigured = null;
if (project.getVcsRepoBinding() != null) {
webhooksConfigured = project.getVcsRepoBinding().isWebhooksConfigured();
}

return new ProjectDTO(
project.getId(),
project.getName(),
Expand All @@ -131,7 +138,8 @@ else if (project.getVcsRepoBinding() != null) {
prAnalysisEnabled,
branchAnalysisEnabled,
installationMethod,
commentCommandsConfigDTO
commentCommandsConfigDTO,
webhooksConfigured
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,22 @@
*/
public enum EVcsConnectionType {
// Bitbucket Cloud connection types
OAUTH_MANUAL, // User-created OAuth consumer (per-user access)
OAUTH_MANUAL, // User-created OAuth consumer (workspace-level access)
APP, // OAuth-based app installation (per-user access)
CONNECT_APP, // Atlassian Connect App (workspace-level access)

/**
* @deprecated Forge App approach abandoned due to workspace-level webhook limitations.
* Kept for backward compatibility with existing database records.
* Use CONNECT_APP or OAUTH_MANUAL instead.
*/
@Deprecated
FORGE_APP, // Atlassian Forge App (deprecated - do not use)


// GitHub connection types
GITHUB_APP, // GitHub App installation (org/account level)
OAUTH_APP, // GitHub OAuth App (per-user access)

// GitLab connection types (future)
// GitLab connection types
PERSONAL_TOKEN,
APPLICATION,

// Repository-scoped token (single repo access)
// Works with GitLab Project Access Tokens, GitHub Fine-grained PATs, Bitbucket Repo Tokens
REPOSITORY_TOKEN,

// Bitbucket Server / Data Center (future)
ACCESS_TOKEN
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ public class VcsConnection {
@Column(name = "installation_id", length = 128)
private String installationId;

/**
* Full repository path for REPOSITORY_TOKEN connections.
* For GitLab: "namespace/project" or project ID
* For GitHub: "owner/repo"
* For Bitbucket: "workspace/repo-slug"
* Only set when connectionType = REPOSITORY_TOKEN
*/
@Column(name = "repository_path", length = 512)
private String repositoryPath;

@Column(name = "access_token", length = 1024)
private String accessToken;

Expand Down Expand Up @@ -186,6 +196,14 @@ public void setInstallationId(String installationId) {
this.installationId = installationId;
}

public String getRepositoryPath() {
return repositoryPath;
}

public void setRepositoryPath(String repositoryPath) {
this.repositoryPath = repositoryPath;
}

public String getAccessToken() {
return accessToken;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.rostilos.codecrow.core.model.vcs.config.cloud.BitbucketCloudConfig;
import org.rostilos.codecrow.core.model.vcs.config.github.GitHubConfig;
import org.rostilos.codecrow.core.model.vcs.config.gitlab.GitLabConfig;

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
Expand All @@ -12,7 +13,8 @@
)
@JsonSubTypes({
@JsonSubTypes.Type(value = BitbucketCloudConfig.class, name = "bitbucket"),
@JsonSubTypes.Type(value = GitHubConfig.class, name = "github")
@JsonSubTypes.Type(value = GitHubConfig.class, name = "github"),
@JsonSubTypes.Type(value = GitLabConfig.class, name = "gitlab")
})
public sealed interface VcsConnectionConfig permits GitHubConfig, BitbucketCloudConfig {
public sealed interface VcsConnectionConfig permits GitHubConfig, BitbucketCloudConfig, GitLabConfig {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.rostilos.codecrow.core.model.vcs.config.gitlab;

import com.fasterxml.jackson.annotation.JsonTypeName;
import org.rostilos.codecrow.core.model.vcs.config.VcsConnectionConfig;

import java.util.List;

/**
* GitLab connection configuration.
* Supports both GitLab.com and self-hosted GitLab instances.
*/
@JsonTypeName("gitlab")
public record GitLabConfig(
String accessToken,
String groupId,
List<String> allowedRepos,
String baseUrl // For self-hosted GitLab instances (e.g., "https://gitlab.mycompany.com")
) implements VcsConnectionConfig {

/**
* Constructor for backward compatibility (without baseUrl).
*/
public GitLabConfig(String accessToken, String groupId, List<String> allowedRepos) {
this(accessToken, groupId, allowedRepos, null);
}

/**
* Returns the effective base URL (defaults to gitlab.com if not specified).
*/
public String effectiveBaseUrl() {
return (baseUrl != null && !baseUrl.isBlank()) ? baseUrl : "https://gitlab.com";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,19 @@ public BranchStats getBranchStats(Long projectId, String branchName) {

List<Object[]> mostProblematicFiles = getMostProblematicFiles(issues);

// Calculate counts directly from issues list for accuracy
long openCount = issues.stream().filter(i -> !i.isResolved()).count();
long resolvedCount = issues.stream().filter(BranchIssue::isResolved).count();
long highCount = issues.stream().filter(i -> i.getSeverity() == org.rostilos.codecrow.core.model.codeanalysis.IssueSeverity.HIGH && !i.isResolved()).count();
long mediumCount = issues.stream().filter(i -> i.getSeverity() == org.rostilos.codecrow.core.model.codeanalysis.IssueSeverity.MEDIUM && !i.isResolved()).count();
long lowCount = issues.stream().filter(i -> i.getSeverity() == org.rostilos.codecrow.core.model.codeanalysis.IssueSeverity.LOW && !i.isResolved()).count();

return new BranchStats(
branch.getTotalIssues(),
branch.getHighSeverityCount(),
branch.getMediumSeverityCount(),
branch.getLowSeverityCount(),
branch.getResolvedCount(),
openCount,
highCount,
mediumCount,
lowCount,
resolvedCount,
1L,
mostProblematicFiles,
branch.getUpdatedAt(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Migration: Add GITLAB to vcs_connection provider_type CHECK constraint
-- Description: Updates the CHECK constraint on vcs_connection.provider_type to include GITLAB
-- Date: 2025-01-XX

-- =====================================================
-- Step 1: Drop the existing CHECK constraint
-- =====================================================
ALTER TABLE vcs_connection DROP CONSTRAINT IF EXISTS vcs_connection_provider_type_check;

-- =====================================================
-- Step 2: Add updated CHECK constraint with GITLAB
-- =====================================================
ALTER TABLE vcs_connection ADD CONSTRAINT vcs_connection_provider_type_check
CHECK (provider_type IN ('BITBUCKET_CLOUD', 'BITBUCKET_SERVER', 'GITHUB', 'GITLAB'));
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Backfill commit_hash in code_analysis from the associated pull_request
-- For existing PR analyses that don't have a commit_hash, copy it from the PR entity
-- This is acceptable for historical data as we're capturing the PR's latest commit state

UPDATE code_analysis ca
SET commit_hash = pr.commit_hash
FROM pull_request pr
WHERE ca.pr_number = pr.pr_number
AND ca.project_id = pr.project_id
AND ca.commit_hash IS NULL
AND pr.commit_hash IS NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
alter table vcs_connection
drop constraint vcs_connection_connection_type_check;

alter table vcs_connection
add constraint vcs_connection_connection_type_check
check ((connection_type)::text = ANY
((ARRAY ['OAUTH_MANUAL'::character varying, 'APP'::character varying, 'CONNECT_APP'::character varying, 'GITHUB_APP'::character varying, 'OAUTH_APP'::character varying, 'PERSONAL_TOKEN'::character varying, 'APPLICATION'::character varying, 'ACCESS_TOKEN'::character varying, 'REPOSITORY_TOKEN'::character varying])::text[]));
3 changes: 3 additions & 0 deletions java-ecosystem/libs/vcs-client/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@
exports org.rostilos.codecrow.vcsclient.github;
exports org.rostilos.codecrow.vcsclient.github.actions;
exports org.rostilos.codecrow.vcsclient.github.dto.response;
exports org.rostilos.codecrow.vcsclient.gitlab;
exports org.rostilos.codecrow.vcsclient.gitlab.actions;
exports org.rostilos.codecrow.vcsclient.gitlab.dto.response;
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,32 @@ public OkHttpClient createGitHubClient(String accessToken) {
.build();
}

/**
* Create an OkHttpClient configured for GitLab API with bearer token authentication.
*
* @param accessToken the GitLab personal access token or OAuth token
* @return configured OkHttpClient for GitLab API
*/
public OkHttpClient createGitLabClient(String accessToken) {
if (accessToken == null || accessToken.isBlank()) {
throw new IllegalArgumentException("Access token cannot be null or empty");
}

return new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.addInterceptor(chain -> {
Request original = chain.request();
Request authorized = original.newBuilder()
.header("Authorization", "Bearer " + accessToken)
.header("Accept", "application/json")
.build();
return chain.proceed(authorized);
})
.build();
}

private void validateSettings(String clientId, String clientSecret) {
if (clientId.isEmpty()) {
throw new IllegalArgumentException("No ClientId key has been set for Bitbucket connections");
Expand Down
Loading