Skip to content

Commit 39b813a

Browse files
authored
Merge pull request #137 from rostilos/feature/CA-20-source-code-based-issues-relations
Feature/ca 20 source code based issues relations
2 parents 83cb736 + 04d7ce4 commit 39b813a

5 files changed

Lines changed: 111 additions & 32 deletions

File tree

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[submodule "frontend"]
22
path = frontend
33
url = git@github.com:rostilos/CodeCrow-Frontend.git
4-
branch = feature/CA-20-source-code-based-issues-relations
4+
branch = main
-81.2 MB
Binary file not shown.
-618 KB
Binary file not shown.

java-ecosystem/libs/analysis-engine/src/main/java/org/rostilos/codecrow/analysisengine/processor/analysis/branch/BranchIssueMappingService.java

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import java.util.*;
1414
import java.util.stream.Collectors;
1515

16+
import org.springframework.dao.DataIntegrityViolationException;
17+
1618
/**
1719
* Handles mapping of {@link CodeAnalysisIssue} records to
1820
* {@link BranchIssue} records with content-based deduplication.
@@ -42,6 +44,27 @@ public BranchIssueMappingService(CodeAnalysisIssueRepository codeAnalysisIssueRe
4244
public void mapCodeAnalysisIssuesToBranch(Set<String> changedFiles,
4345
Set<String> filesExistingInBranch,
4446
Branch branch, Project project) {
47+
48+
// ── Build branch-wide content fingerprint set ─────────────────────────
49+
// The unique constraint uq_branch_issue_content_fp is on (branch_id, content_fingerprint)
50+
// so dedup must be branch-wide, not per-file. Pre-load all existing fingerprints once.
51+
List<BranchIssue> allBranchIssues = branchIssueRepository.findByBranchId(branch.getId());
52+
53+
Set<String> branchContentFingerprints = new HashSet<>();
54+
Set<Long> allLinkedOriginIds = new HashSet<>();
55+
for (BranchIssue bi : allBranchIssues) {
56+
if (bi.getContentFingerprint() != null) {
57+
branchContentFingerprints.add(bi.getContentFingerprint());
58+
}
59+
if (bi.getOriginIssue() != null) {
60+
allLinkedOriginIds.add(bi.getOriginIssue().getId());
61+
}
62+
}
63+
64+
log.debug("Branch {} pre-loaded {} content fingerprints and {} origin IDs for dedup",
65+
branch.getBranchName(), branchContentFingerprints.size(), allLinkedOriginIds.size());
66+
67+
// ── Per-file mapping loop ─────────────────────────────────────────────
4568
for (String filePath : changedFiles) {
4669
if (!filesExistingInBranch.contains(filePath)) {
4770
log.debug("Skipping issue mapping for file {} - does not exist in branch {} (cached)",
@@ -61,41 +84,32 @@ public void mapCodeAnalysisIssuesToBranch(Set<String> changedFiles,
6184
unresolvedIssues.size(), filePath, allIssues.size());
6285
}
6386

64-
// Build deduplication maps from ALL existing BranchIssues (resolved + unresolved)
65-
List<BranchIssue> existingBranchIssues = branchIssueRepository
87+
// Per-file legacy key map (legacy keys are file-scoped by construction)
88+
List<BranchIssue> existingBranchIssuesForFile = branchIssueRepository
6689
.findByBranchIdAndFilePath(branch.getId(), filePath);
6790

68-
Map<String, BranchIssue> contentFpMap = new HashMap<>();
6991
Map<String, BranchIssue> legacyKeyMap = new HashMap<>();
70-
for (BranchIssue bi : existingBranchIssues) {
71-
if (bi.getContentFingerprint() != null) {
72-
contentFpMap.putIfAbsent(bi.getContentFingerprint(), bi);
73-
}
92+
for (BranchIssue bi : existingBranchIssuesForFile) {
7493
legacyKeyMap.putIfAbsent(buildLegacyContentKey(bi), bi);
7594
}
7695

77-
Set<Long> linkedOriginIds = existingBranchIssues.stream()
78-
.filter(bi -> bi.getOriginIssue() != null)
79-
.map(bi -> bi.getOriginIssue().getId())
80-
.collect(Collectors.toSet());
81-
8296
int skipped = 0;
8397
int mapped = 0;
8498
for (CodeAnalysisIssue issue : unresolvedIssues) {
85-
// Tier 1: origin ID match
86-
if (linkedOriginIds.contains(issue.getId())) {
99+
// Tier 1: origin ID match (branch-wide)
100+
if (allLinkedOriginIds.contains(issue.getId())) {
87101
updateSeverityIfChanged(branch, issue);
88102
continue;
89103
}
90104

91-
// Tier 2: content fingerprint dedup
105+
// Tier 2: content fingerprint dedup (branch-wide — matches DB constraint scope)
92106
if (issue.getContentFingerprint() != null
93-
&& contentFpMap.containsKey(issue.getContentFingerprint())) {
107+
&& branchContentFingerprints.contains(issue.getContentFingerprint())) {
94108
skipped++;
95109
continue;
96110
}
97111

98-
// Tier 3: legacy key dedup
112+
// Tier 3: legacy key dedup (per-file)
99113
String legacyKey = buildLegacyContentKeyFromCAI(issue);
100114
if (legacyKeyMap.containsKey(legacyKey)) {
101115
skipped++;
@@ -104,15 +118,27 @@ public void mapCodeAnalysisIssuesToBranch(Set<String> changedFiles,
104118

105119
// No match — create new BranchIssue as a full deep copy
106120
BranchIssue bi = BranchIssue.fromCodeAnalysisIssue(issue, branch);
107-
branchIssueRepository.saveAndFlush(bi);
121+
try {
122+
branchIssueRepository.saveAndFlush(bi);
123+
} catch (DataIntegrityViolationException e) {
124+
// Safety net: concurrent insert or edge-case fingerprint collision
125+
log.warn("Duplicate content_fingerprint for branch {} file {} — skipping (fp={})",
126+
branch.getId(), filePath,
127+
bi.getContentFingerprint() != null ? bi.getContentFingerprint().substring(0, 12) + "..." : "null");
128+
skipped++;
129+
if (bi.getContentFingerprint() != null) {
130+
branchContentFingerprints.add(bi.getContentFingerprint());
131+
}
132+
continue;
133+
}
108134
mapped++;
109135

110-
// Register in maps so subsequent issues in this batch also dedup
136+
// Register in branch-wide maps so subsequent issues also dedup
111137
if (bi.getContentFingerprint() != null) {
112-
contentFpMap.put(bi.getContentFingerprint(), bi);
138+
branchContentFingerprints.add(bi.getContentFingerprint());
113139
}
114140
legacyKeyMap.put(buildLegacyContentKey(bi), bi);
115-
linkedOriginIds.add(issue.getId());
141+
allLinkedOriginIds.add(issue.getId());
116142
}
117143

118144
if (mapped > 0 || skipped > 0) {

java-ecosystem/services/web-server/src/main/java/org/rostilos/codecrow/webserver/project/service/ProjectService.java

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -369,24 +369,77 @@ public void deleteProject(Long workspaceId, Long projectId) {
369369

370370
@Transactional
371371
public Project bindRepository(Long workspaceId, Long projectId, BindRepositoryRequest request) {
372-
Project p = projectRepository.findByWorkspaceIdAndId(workspaceId, projectId)
372+
Project project = projectRepository.findByWorkspaceIdAndId(workspaceId, projectId)
373373
.orElseThrow(() -> new NoSuchElementException("Project not found"));
374374

375-
if ("BITBUCKET_CLOUD".equalsIgnoreCase(request.getProvider())) {
376-
VcsConnection conn = vcsConnectionRepository.findByWorkspace_IdAndId(workspaceId, request.getConnectionId())
377-
.orElseThrow(() -> new NoSuchElementException("Connection not found"));
375+
Workspace workspace = workspaceRepository.findById(workspaceId)
376+
.orElseThrow(() -> new NoSuchElementException("Workspace not found"));
377+
378+
VcsConnection connection = vcsConnectionRepository
379+
.findByWorkspace_IdAndId(workspaceId, request.getConnectionId())
380+
.orElseThrow(() -> new NoSuchElementException("VCS Connection not found"));
381+
382+
// Get or create VcsRepoBinding
383+
VcsRepoBinding binding = vcsRepoBindingRepository.findByProject_Id(projectId)
384+
.orElse(null);
385+
386+
if (binding == null) {
387+
binding = new VcsRepoBinding();
388+
binding.setProject(project);
389+
binding.setWorkspace(workspace);
378390
}
379-
// TODO: bind implementation
380-
return projectRepository.save(p);
391+
392+
// Update binding with new connection info
393+
binding.setVcsConnection(connection);
394+
binding.setProvider(connection.getProviderType());
395+
binding.setExternalRepoSlug(request.getRepositorySlug());
396+
binding.setExternalNamespace(
397+
request.getWorkspaceId() != null ? request.getWorkspaceId() : connection.getExternalWorkspaceSlug());
398+
binding.setExternalRepoId(
399+
request.getRepositoryId() != null ? request.getRepositoryId() : request.getRepositorySlug());
400+
binding.setDisplayName(request.getName());
401+
402+
if (request.getDefaultBranch() != null && !request.getDefaultBranch().isBlank()) {
403+
binding.setDefaultBranch(request.getDefaultBranch());
404+
}
405+
406+
// Reset webhook status — will be set up fresh
407+
binding.setWebhooksConfigured(false);
408+
binding.setWebhookId(null);
409+
410+
vcsRepoBindingRepository.save(binding);
411+
412+
// Setup webhooks automatically
413+
try {
414+
WebhookSetupResult webhookResult = setupWebhooks(workspaceId, projectId);
415+
if (!webhookResult.success()) {
416+
log.warn("Webhook setup failed for project {}: {}", projectId, webhookResult.message());
417+
}
418+
} catch (Exception e) {
419+
log.warn("Webhook setup error for project {}: {}", projectId, e.getMessage());
420+
}
421+
422+
// Use findByIdWithFullDetails to eagerly fetch VcsRepoBinding + VcsConnection
423+
// so the returned ProjectDTO contains the updated VCS info
424+
return projectRepository.findByIdWithFullDetails(projectId)
425+
.orElseThrow(() -> new NoSuchElementException("Project not found after bind"));
381426
}
382427

383428
@Transactional
384429
public Project unbindRepository(Long workspaceId, Long projectId) {
385-
Project p = projectRepository.findByWorkspaceIdAndId(workspaceId, projectId)
430+
// Verify project exists in workspace
431+
projectRepository.findByWorkspaceIdAndId(workspaceId, projectId)
386432
.orElseThrow(() -> new NoSuchElementException("Project not found"));
387-
// TODO: unbind implementation
388-
// Clear settings placeholder if used in future
389-
return projectRepository.save(p);
433+
434+
VcsRepoBinding binding = vcsRepoBindingRepository.findByProject_Id(projectId)
435+
.orElse(null);
436+
437+
if (binding != null) {
438+
vcsRepoBindingRepository.delete(binding);
439+
}
440+
441+
return projectRepository.findByWorkspaceIdAndId(workspaceId, projectId)
442+
.orElseThrow(() -> new NoSuchElementException("Project not found after unbind"));
390443
}
391444

392445
@Transactional

0 commit comments

Comments
 (0)