Skip to content

Commit d778fca

Browse files
authored
Merge pull request #146 from rostilos/1.5.1-rc
Add prompt constants for issue reporting and review instructions
2 parents cca18a1 + 88e3972 commit d778fca

48 files changed

Lines changed: 5336 additions & 3302 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

deployment/build/production-build.sh

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ echo "--- 1. Ensuring frontend submodule is synchronized ---"
1515
if [ -d "$FRONTEND_DIR" ] && [ ! -f "$FRONTEND_DIR/.git" ]; then
1616
echo "Stale frontend directory detected (not a submodule). Removing and re-initializing..."
1717
rm -rf "$FRONTEND_DIR"
18-
git submodule update --init --remote -- "$FRONTEND_DIR"
19-
elif [ -d "$FRONTEND_DIR" ]; then
20-
echo "Frontend submodule exists. Updating..."
21-
git submodule update --remote -- "$FRONTEND_DIR"
22-
else
18+
git submodule update --init -- "$FRONTEND_DIR"
19+
elif [ ! -d "$FRONTEND_DIR" ]; then
2320
echo "Initializing frontend submodule..."
24-
git submodule update --init --remote -- "$FRONTEND_DIR"
21+
git submodule update --init -- "$FRONTEND_DIR"
22+
else
23+
echo "Frontend submodule exists."
2524
fi
26-
(cd "$FRONTEND_DIR" && git checkout "$FRONTEND_BRANCH" && git pull origin "$FRONTEND_BRANCH")
25+
echo "Fetching latest from origin and resetting to origin/$FRONTEND_BRANCH..."
26+
(cd "$FRONTEND_DIR" && git fetch origin "$FRONTEND_BRANCH" && git reset --hard "origin/$FRONTEND_BRANCH")
27+
echo "Frontend at: $(cd "$FRONTEND_DIR" && git log --oneline -1)"
2728

2829
echo "--- 2. Injecting Environment Configurations ---"
2930

frontend

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.rostilos.codecrow.analysisengine.dag;
2+
3+
import java.util.List;
4+
5+
public record DagContext(
6+
List<String> unanalyzedCommits,
7+
String diffBase,
8+
boolean skipAnalysis
9+
) {
10+
public String getDiffBase() {
11+
return diffBase;
12+
}
13+
14+
public List<String> getUnanalyzedCommits() {
15+
return unanalyzedCommits;
16+
}
17+
18+
public boolean getSkipAnalysis() {
19+
return skipAnalysis;
20+
}
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.rostilos.codecrow.analysisengine.processor;
2+
3+
import org.rostilos.codecrow.core.model.vcs.VcsConnection;
4+
import org.rostilos.codecrow.core.model.vcs.VcsRepoInfo;
5+
6+
public record VcsRepoInfoImpl(VcsConnection vcsConnection, String workspace, String repoSlug) implements VcsRepoInfo {
7+
@Override
8+
public String getRepoSlug() {
9+
return repoSlug;
10+
}
11+
12+
public String getRepoWorkspace() {
13+
return workspace;
14+
}
15+
16+
public VcsConnection getVcsConnection() {
17+
return vcsConnection;
18+
}
19+
}

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

Lines changed: 50 additions & 353 deletions
Large diffs are not rendered by default.

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.rostilos.codecrow.analysisengine.processor.analysis;
22

3+
import org.rostilos.codecrow.analysisengine.util.ProjectVcsInfoRetriever;
34
import org.rostilos.codecrow.core.model.analysis.AnalysisLockType;
45
import org.rostilos.codecrow.core.model.codeanalysis.CodeAnalysis;
56
import org.rostilos.codecrow.core.model.codeanalysis.CodeAnalysisIssue;
@@ -98,15 +99,6 @@ public interface EventConsumer {
9899
void accept(Map<String, Object> event);
99100
}
100101

101-
private EVcsProvider getVcsProvider(Project project) {
102-
// Use unified method to get effective VCS connection
103-
var vcsConnection = project.getEffectiveVcsConnection();
104-
if (vcsConnection != null) {
105-
return vcsConnection.getProviderType();
106-
}
107-
throw new IllegalStateException("No VCS connection configured for project: " + project.getId());
108-
}
109-
110102
public Map<String, Object> process(
111103
PrProcessRequest request,
112104
EventConsumer consumer,
@@ -169,7 +161,7 @@ public Map<String, Object> process(
169161
project
170162
);
171163

172-
EVcsProvider provider = getVcsProvider(project);
164+
EVcsProvider provider = ProjectVcsInfoRetriever.getVcsProvider(project);
173165
VcsReportingService reportingService = vcsServiceFactory.getReportingService(provider);
174166

175167
if (postAnalysisCacheIfExist(project, pullRequest, request.getCommitHash(), request.getPullRequestId(), reportingService, request.getPlaceholderCommentId())) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package org.rostilos.codecrow.analysisengine.service.branch;
2+
3+
import okhttp3.OkHttpClient;
4+
import org.rostilos.codecrow.analysisengine.dag.DagContext;
5+
import org.rostilos.codecrow.analysisengine.dto.request.processor.BranchProcessRequest;
6+
import org.rostilos.codecrow.analysisengine.processor.VcsRepoInfoImpl;
7+
import org.rostilos.codecrow.analysisengine.service.vcs.VcsOperationsService;
8+
import org.rostilos.codecrow.core.model.branch.Branch;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
import org.springframework.stereotype.Service;
12+
13+
import java.io.IOException;
14+
import java.util.List;
15+
16+
@Service
17+
public class BranchDiffFetcher {
18+
private static final Logger log = LoggerFactory.getLogger(BranchDiffFetcher.class);
19+
20+
public String fetchDiff(BranchProcessRequest request, Branch existingBranch,
21+
DagContext dagCtx, VcsOperationsService operationsService,
22+
OkHttpClient client, VcsRepoInfoImpl vcsRepoInfoImpl,
23+
Long prNumber, List<String> unanalyzedCommits) throws IOException {
24+
25+
String lastSuccessfulCommit = existingBranch != null
26+
? existingBranch.getLastSuccessfulCommitHash() : null;
27+
String rawDiff = null;
28+
29+
// Tier 0: DAG-derived diff base (preferred)
30+
rawDiff = tryDagDiff(dagCtx, request, operationsService, client, vcsRepoInfoImpl, unanalyzedCommits);
31+
32+
// Tier 1: Legacy delta diff
33+
if (rawDiff == null) {
34+
rawDiff = tryDeltaDiff(lastSuccessfulCommit, request, operationsService, client, vcsRepoInfoImpl);
35+
}
36+
37+
// Tier 1.5: Aggregate individual commit diffs when range diff failed.
38+
// Handles cases where the base commit is a second-parent commit (e.g. from
39+
// a merged feature branch) and the range diff API returns empty.
40+
if (rawDiff == null && !unanalyzedCommits.isEmpty()) {
41+
rawDiff = tryAggregatedCommitDiffs(unanalyzedCommits, operationsService, client, vcsRepoInfoImpl);
42+
}
43+
44+
// Tier 2: PR diff
45+
if (rawDiff == null && prNumber != null) {
46+
rawDiff = operationsService.getPullRequestDiff(
47+
client, vcsRepoInfoImpl.workspace(), vcsRepoInfoImpl.repoSlug(), String.valueOf(prNumber));
48+
log.info("Fetched PR #{} diff for branch analysis (first analysis or delta fallback)", prNumber);
49+
}
50+
51+
// Tier 3: Single commit diff (last resort)
52+
if (rawDiff == null) {
53+
rawDiff = operationsService.getCommitDiff(
54+
client, vcsRepoInfoImpl.workspace(), vcsRepoInfoImpl.repoSlug(), request.getCommitHash());
55+
log.info("Fetched commit {} diff for branch analysis (first analysis, no delta or PR context)",
56+
request.getCommitHash());
57+
}
58+
59+
return rawDiff;
60+
}
61+
62+
private String tryDagDiff(DagContext dagCtx, BranchProcessRequest request,
63+
VcsOperationsService operationsService, OkHttpClient client,
64+
VcsRepoInfoImpl vcsRepoInfoImpl, List<String> unanalyzedCommits) {
65+
if (dagCtx.getDiffBase() == null || request.getCommitHash() == null
66+
|| dagCtx.getDiffBase().equals(request.getCommitHash())) {
67+
return null;
68+
}
69+
try {
70+
String diff = operationsService.getCommitRangeDiff(
71+
client, vcsRepoInfoImpl.workspace(), vcsRepoInfoImpl.repoSlug(),
72+
dagCtx.getDiffBase(), request.getCommitHash());
73+
if (diff != null && diff.isBlank()) {
74+
log.info("DAG-based diff ({}..{}) returned empty (likely merge commit) — falling through",
75+
shortHash(dagCtx.getDiffBase()), shortHash(request.getCommitHash()));
76+
return null;
77+
}
78+
log.info("Fetched DAG-based diff ({}..{}) — covers {} unanalyzed commits",
79+
shortHash(dagCtx.getDiffBase()), shortHash(request.getCommitHash()),
80+
unanalyzedCommits.size());
81+
return diff;
82+
} catch (IOException e) {
83+
log.warn("DAG-based diff failed (ancestor {} may be unreachable), falling back: {}",
84+
shortHash(dagCtx.getDiffBase()), e.getMessage());
85+
return null;
86+
}
87+
}
88+
89+
private String tryDeltaDiff(String lastSuccessfulCommit, BranchProcessRequest request,
90+
VcsOperationsService operationsService, OkHttpClient client,
91+
VcsRepoInfoImpl vcsRepoInfoImpl) {
92+
if (lastSuccessfulCommit == null || lastSuccessfulCommit.equals(request.getCommitHash())) {
93+
return null;
94+
}
95+
try {
96+
String diff = operationsService.getCommitRangeDiff(
97+
client, vcsRepoInfoImpl.workspace(), vcsRepoInfoImpl.repoSlug(),
98+
lastSuccessfulCommit, request.getCommitHash());
99+
if (diff != null && diff.isBlank()) {
100+
log.info("Delta diff ({}..{}) returned empty — falling through to next tier",
101+
shortHash(lastSuccessfulCommit), shortHash(request.getCommitHash()));
102+
return null;
103+
}
104+
log.info("Fetched delta diff ({}..{}) for branch analysis — captures all changes since last success",
105+
shortHash(lastSuccessfulCommit), shortHash(request.getCommitHash()));
106+
return diff;
107+
} catch (IOException e) {
108+
log.warn("Delta diff failed (base commit {} may no longer exist), falling back: {}",
109+
shortHash(lastSuccessfulCommit), e.getMessage());
110+
return null;
111+
}
112+
}
113+
114+
private String tryAggregatedCommitDiffs(List<String> unanalyzedCommits,
115+
VcsOperationsService operationsService,
116+
OkHttpClient client, VcsRepoInfoImpl vcsRepoInfoImpl) {
117+
int maxCommits = Math.min(unanalyzedCommits.size(), 50);
118+
log.info("Range diff unavailable — aggregating individual diffs for {} of {} unanalyzed commits",
119+
maxCommits, unanalyzedCommits.size());
120+
121+
StringBuilder aggregatedDiff = new StringBuilder();
122+
int fetchedCount = 0;
123+
124+
for (int i = 0; i < maxCommits; i++) {
125+
String hash = unanalyzedCommits.get(i);
126+
try {
127+
String commitDiff = operationsService.getCommitDiff(
128+
client, vcsRepoInfoImpl.workspace(), vcsRepoInfoImpl.repoSlug(), hash);
129+
if (commitDiff != null && !commitDiff.isBlank()) {
130+
aggregatedDiff.append(commitDiff);
131+
if (!commitDiff.endsWith("\n")) {
132+
aggregatedDiff.append("\n");
133+
}
134+
fetchedCount++;
135+
}
136+
} catch (Exception e) {
137+
log.warn("Failed to fetch diff for commit {} (skipping): {}",
138+
shortHash(hash), e.getMessage());
139+
}
140+
}
141+
142+
if (fetchedCount > 0) {
143+
String result = aggregatedDiff.toString();
144+
log.info("Aggregated {} individual commit diffs ({} chars) as fallback for empty range diff",
145+
fetchedCount, result.length());
146+
return result;
147+
}
148+
return null;
149+
}
150+
151+
private static String shortHash(String hash) {
152+
return hash != null ? hash.substring(0, Math.min(7, hash.length())) : "null";
153+
}
154+
}

java-ecosystem/libs/analysis-engine/src/main/java/org/rostilos/codecrow/analysisengine/processor/analysis/branch/BranchFileOperationsService.java renamed to java-ecosystem/libs/analysis-engine/src/main/java/org/rostilos/codecrow/analysisengine/service/branch/BranchFileOperationsService.java

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
package org.rostilos.codecrow.analysisengine.processor.analysis.branch;
1+
package org.rostilos.codecrow.analysisengine.service.branch;
22

33
import okhttp3.OkHttpClient;
44
import org.rostilos.codecrow.analysisengine.dto.request.processor.BranchProcessRequest;
5-
import org.rostilos.codecrow.analysisengine.processor.analysis.BranchAnalysisProcessor.VcsInfo;
5+
import org.rostilos.codecrow.analysisengine.processor.VcsRepoInfoImpl;
66
import org.rostilos.codecrow.analysisengine.service.BranchArchiveService;
77
import org.rostilos.codecrow.analysisengine.service.vcs.VcsOperationsService;
88
import org.rostilos.codecrow.analysisengine.service.vcs.VcsServiceFactory;
@@ -75,11 +75,11 @@ public BranchFileOperationsService(
7575
* avoiding rate-limiting issues (e.g. Bitbucket HTTP 429).
7676
* Returns an empty map on failure — callers must handle graceful fallback.
7777
*/
78-
public Map<String, String> downloadBranchArchive(VcsInfo vcsInfo, String branchOrCommit,
78+
public Map<String, String> downloadBranchArchive(VcsRepoInfoImpl vcsRepoInfoImpl, String branchOrCommit,
7979
Set<String> neededFiles) {
8080
try {
8181
return branchArchiveService.downloadAndExtractFiles(
82-
vcsInfo.vcsConnection(), vcsInfo.workspace(), vcsInfo.repoSlug(),
82+
vcsRepoInfoImpl.vcsConnection(), vcsRepoInfoImpl.workspace(), vcsRepoInfoImpl.repoSlug(),
8383
branchOrCommit, neededFiles);
8484
} catch (Exception e) {
8585
log.warn("Failed to download branch archive — will fall back to per-file API calls: {}",
@@ -163,9 +163,15 @@ public Branch createOrUpdateProjectBranch(Project project, BranchProcessRequest
163163
// ──────────────────── File snapshot updates ──────────────────────────────
164164

165165
/**
166-
* Update file snapshots in the latest analysis for this branch using
166+
* Update file snapshots at the <b>branch</b> level using
167167
* pre-downloaded archive contents.
168168
* <p>
169+
* Snapshots are stored keyed on {@code (branch_id, file_path)} so that
170+
* each branch has exactly one snapshot per file, always pointing to the
171+
* latest content. <b>Analysis-level snapshots remain immutable</b> — they
172+
* preserve the file content at the time each issue was originally detected,
173+
* which is critical for the Source Context viewer.
174+
* <p>
169175
* When {@code archiveContents} is non-empty, file content is read from the
170176
* map directly (no API calls). When empty, falls back to per-file VCS API.
171177
*/
@@ -175,24 +181,24 @@ public void updateFileSnapshotsForBranch(Set<String> existingFiles, Project proj
175181
if (existingFiles.isEmpty()) return;
176182

177183
try {
178-
Optional<CodeAnalysis> latestAnalysisOpt = codeAnalysisRepository
179-
.findLatestByProjectIdAndBranchName(project.getId(), request.getTargetBranchName());
180-
if (latestAnalysisOpt.isEmpty()) {
181-
log.debug("No existing analysis found for branch {} — skipping snapshot update",
184+
Optional<Branch> branchOpt = branchRepository
185+
.findByProjectIdAndBranchName(project.getId(), request.getTargetBranchName());
186+
if (branchOpt.isEmpty()) {
187+
log.debug("No branch entity found for {} — skipping snapshot update",
182188
request.getTargetBranchName());
183189
return;
184190
}
185-
CodeAnalysis latestAnalysis = latestAnalysisOpt.get();
191+
Branch branch = branchOpt.get();
186192

187193
Map<String, String> fileContents = buildFileContentsMap(
188194
existingFiles, project, request, archiveContents);
189195

190196
if (!fileContents.isEmpty()) {
191-
int updated = fileSnapshotService.updateOrPersistSnapshots(
192-
latestAnalysis, fileContents, request.getCommitHash());
197+
int updated = fileSnapshotService.persistSnapshotsForBranch(
198+
branch, fileContents, request.getCommitHash());
193199
if (updated > 0) {
194-
log.info("Updated {} file snapshots in analysis {} for branch {} (commit: {})",
195-
updated, latestAnalysis.getId(), request.getTargetBranchName(),
200+
log.info("Updated {} branch-level file snapshots for branch {} (commit: {})",
201+
updated, request.getTargetBranchName(),
196202
request.getCommitHash().substring(0, Math.min(7, request.getCommitHash().length())));
197203
}
198204
}

0 commit comments

Comments
 (0)