Skip to content

Commit bfd8bb4

Browse files
committed
chore(git): Fix failing unit test #12350
1 parent e78e8de commit bfd8bb4

File tree

11 files changed

+296
-70
lines changed

11 files changed

+296
-70
lines changed

jabgui/src/main/java/org/jabref/gui/git/GitPullViewModel.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
import org.eclipse.jgit.api.errors.GitAPIException;
3030
import org.eclipse.jgit.revwalk.RevCommit;
3131

32+
/**
33+
* ViewModel responsible for coordinating UI-bound Git Pull workflow,
34+
* including conflict resolution.
35+
*/
3236
public class GitPullViewModel extends AbstractViewModel {
3337
private final ImportFormatPreferences importFormatPreferences;
3438
private final GitConflictResolver conflictResolver;

jablib/src/main/java/org/jabref/logic/git/GitHandler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,8 @@ public static Optional<GitHandler> fromAnyPath(Path anyPathInsideRepo) {
234234
public File getRepositoryPathAsFile() {
235235
return repositoryPathAsFile;
236236
}
237+
238+
public Git open() throws IOException {
239+
return Git.open(this.repositoryPathAsFile);
240+
}
237241
}

jablib/src/main/java/org/jabref/logic/git/GitSyncService.java

Lines changed: 99 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
import org.jabref.logic.git.merge.MergePlan;
1717
import org.jabref.logic.git.merge.SemanticMerger;
1818
import org.jabref.logic.git.model.MergeResult;
19+
import org.jabref.logic.git.status.GitStatusChecker;
20+
import org.jabref.logic.git.status.GitStatusSnapshot;
21+
import org.jabref.logic.git.status.SyncStatus;
1922
import org.jabref.logic.importer.ImportFormatPreferences;
2023
import org.jabref.model.database.BibDatabaseContext;
2124
import org.jabref.model.entry.BibEntry;
@@ -27,11 +30,23 @@
2730
import org.slf4j.LoggerFactory;
2831

2932
/**
30-
* Orchestrator for git sync service
33+
* GitSyncService currently serves as an orchestrator for Git pull/push logic.
3134
* if (hasConflict)
3235
* → UI merge;
3336
* else
3437
* → autoMerge := local + remoteDiff
38+
*
39+
* NOTICE:
40+
* - TODO:This class will be **deprecated** in the near future to avoid architecture violation (logic → gui)!
41+
* - The underlying business logic will not change significantly.
42+
* - Only the coordination responsibilities will shift to GUI/ViewModel layer.
43+
*
44+
* PLAN:
45+
* - All orchestration logic (pull/push, merge, resolve, commit)
46+
* will be **moved into corresponding ViewModels**, such as:
47+
* - GitPullViewModel
48+
* - GitPushViewModel
49+
* - GitStatusViewModel
3550
*/
3651
public class GitSyncService {
3752
private static final Logger LOGGER = LoggerFactory.getLogger(GitSyncService.class);
@@ -49,24 +64,42 @@ public GitSyncService(ImportFormatPreferences importFormatPreferences, GitHandle
4964
* Called when user clicks Pull
5065
*/
5166
public MergeResult fetchAndMerge(Path bibFilePath) throws GitAPIException, IOException, JabRefException {
52-
Git git = Git.open(bibFilePath.getParent().toFile());
67+
GitStatusSnapshot status = GitStatusChecker.checkStatus(bibFilePath);
5368

54-
// 1. fetch latest remote branch
55-
gitHandler.fetchOnCurrentBranch();
56-
57-
// 2. Locating the base / local / remote versions
58-
GitRevisionLocator locator = new GitRevisionLocator();
59-
RevisionTriple triple = locator.locateMergeCommits(git);
69+
if (!status.tracking()) {
70+
LOGGER.warn("Pull aborted: The file is not under Git version control.");
71+
return MergeResult.failure();
72+
}
6073

61-
// 3. Calling semantic merge logic
62-
MergeResult result = performSemanticMerge(git, triple.base(), triple.local(), triple.remote(), bibFilePath);
74+
if (status.conflict()) {
75+
LOGGER.warn("Pull aborted: Local repository has unresolved merge conflicts.");
76+
return MergeResult.failure();
77+
}
6378

64-
// 4. Automatic merge
65-
if (result.isSuccessful()) {
66-
gitHandler.createCommitOnCurrentBranch("Auto-merged by JabRef", !AMEND);
79+
if (status.syncStatus() == SyncStatus.UP_TO_DATE || status.syncStatus() == SyncStatus.AHEAD) {
80+
LOGGER.info("Pull skipped: Local branch is already up to date with remote.");
81+
return MergeResult.success();
6782
}
6883

69-
return result;
84+
// Status is BEHIND or DIVERGED
85+
try (Git git = gitHandler.open()) {
86+
// 1. Fetch latest remote branch
87+
gitHandler.fetchOnCurrentBranch();
88+
89+
// 2. Locate base / local / remote commits
90+
GitRevisionLocator locator = new GitRevisionLocator();
91+
RevisionTriple triple = locator.locateMergeCommits(git);
92+
93+
// 3. Perform semantic merge
94+
MergeResult result = performSemanticMerge(git, triple.base(), triple.local(), triple.remote(), bibFilePath);
95+
96+
// 4. Auto-commit merge result if successful
97+
if (result.isSuccessful()) {
98+
gitHandler.createCommitOnCurrentBranch("Auto-merged by JabRef", !AMEND);
99+
}
100+
101+
return result;
102+
}
70103
}
71104

72105
public MergeResult performSemanticMerge(Git git,
@@ -80,7 +113,6 @@ public MergeResult performSemanticMerge(Git git,
80113
Path relativePath;
81114

82115
// TODO: Validate that the .bib file is inside the Git repository earlier in the workflow.
83-
// This check might be better placed before calling performSemanticMerge.
84116
if (!bibPath.startsWith(workTree)) {
85117
throw new IllegalStateException("Given .bib file is not inside repository");
86118
}
@@ -122,43 +154,61 @@ public MergeResult performSemanticMerge(Git git,
122154
return MergeResult.success();
123155
}
124156

125-
// WIP
126-
public void push(Path bibFilePath) throws GitAPIException, IOException, JabRefException{
127-
// 1. 1: Fetch remote changes
128-
gitHandler.fetchOnCurrentBranch();
129-
130-
// 2: Check if local branch is behind remote
131-
if (gitHandler.isBehindRemote()) {
132-
LOGGER.info("Remote changes detected — performing semantic merge");
133-
134-
// 2.1: Resolve commit identifiers
135-
RevCommit baseCommit = gitHandler.findMergeBaseWithRemote();
136-
RevCommit localCommit = gitHandler.resolveHead();
137-
RevCommit remoteCommit = gitHandler.resolveRemoteHead();
138-
139-
// 2.2: Perform semantic merge of the .bib file
140-
MergeResult mergeResult = performSemanticMerge(
141-
gitHandler.getGit(),
142-
baseCommit,
143-
localCommit,
144-
remoteCommit,
145-
bibFilePath
146-
);
147-
148-
if (!mergeResult.isSuccessful()) {
149-
LOGGER.warn("Semantic merge failed — aborting push");
150-
return;
151-
}
157+
public void push(Path bibFilePath) throws GitAPIException, IOException, JabRefException {
158+
GitStatusSnapshot status = GitStatusChecker.checkStatus(bibFilePath);
159+
160+
if (!status.tracking()) {
161+
LOGGER.warn("Push aborted: file is not tracked by Git");
162+
return;
152163
}
153164

154-
// 3: Commit changes if there are any
155-
boolean committed = gitHandler.createCommitOnCurrentBranch("Changes committed by JabRef", !AMEND);
165+
switch (status.syncStatus()) {
166+
case UP_TO_DATE -> {
167+
boolean committed = gitHandler.createCommitOnCurrentBranch("Changes committed by JabRef", !AMEND);
168+
if (committed) {
169+
gitHandler.pushCommitsToRemoteRepository();
170+
} else {
171+
LOGGER.info("No changes to commit — skipping push");
172+
}
173+
}
156174

157-
// 4: Push to remote only if a commit was made
158-
if (committed) {
159-
gitHandler.pushCommitsToRemoteRepository();
160-
} else {
161-
LOGGER.info("No changes to commit — skipping push");
175+
case AHEAD -> {
176+
gitHandler.pushCommitsToRemoteRepository();
177+
}
178+
179+
case BEHIND -> {
180+
LOGGER.warn("Push aborted: Local branch is behind remote. Please pull first.");
181+
}
182+
183+
case DIVERGED -> {
184+
try (Git git = gitHandler.open()) {
185+
GitRevisionLocator locator = new GitRevisionLocator();
186+
RevisionTriple triple = locator.locateMergeCommits(git);
187+
188+
MergeResult mergeResult = performSemanticMerge(git, triple.base(), triple.local(), triple.remote(), bibFilePath);
189+
190+
if (!mergeResult.isSuccessful()) {
191+
LOGGER.warn("Semantic merge failed — aborting push");
192+
return;
193+
}
194+
195+
boolean committed = gitHandler.createCommitOnCurrentBranch("Merged changes", !AMEND);
196+
197+
if (committed) {
198+
gitHandler.pushCommitsToRemoteRepository();
199+
} else {
200+
LOGGER.info("Nothing to commit after semantic merge — skipping push");
201+
}
202+
}
203+
}
204+
205+
case CONFLICT -> {
206+
LOGGER.warn("Push aborted: Local repository has unresolved merge conflicts.");
207+
}
208+
209+
case UNTRACKED, UNKNOWN -> {
210+
LOGGER.warn("Push aborted: Untracked or unknown Git status.");
211+
}
162212
}
163213
}
164214
}

jablib/src/main/java/org/jabref/logic/git/io/GitFileWriter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import java.nio.file.Path;
77

88
import org.jabref.logic.exporter.AtomicFileWriter;
9+
import org.jabref.logic.exporter.BibDatabaseWriter;
910
import org.jabref.logic.exporter.BibWriter;
10-
import org.jabref.logic.exporter.BibtexDatabaseWriter;
1111
import org.jabref.logic.exporter.SelfContainedSaveConfiguration;
1212
import org.jabref.logic.importer.ImportFormatPreferences;
1313
import org.jabref.model.database.BibDatabaseContext;
@@ -21,7 +21,7 @@ public static void write(Path file, BibDatabaseContext bibDatabaseContext, Impor
2121
synchronized (bibDatabaseContext) {
2222
try (AtomicFileWriter fileWriter = new AtomicFileWriter(file, encoding, saveConfiguration.shouldMakeBackup())) {
2323
BibWriter bibWriter = new BibWriter(fileWriter, bibDatabaseContext.getDatabase().getNewLineSeparator());
24-
BibtexDatabaseWriter writer = new BibtexDatabaseWriter(
24+
BibDatabaseWriter writer = new BibDatabaseWriter(
2525
bibWriter,
2626
saveConfiguration,
2727
importPrefs.fieldPreferences(),

jablib/src/main/java/org/jabref/logic/git/io/GitRevisionLocator.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import org.eclipse.jgit.api.Git;
88
import org.eclipse.jgit.api.errors.GitAPIException;
99
import org.eclipse.jgit.lib.ObjectId;
10+
import org.eclipse.jgit.lib.Repository;
1011
import org.eclipse.jgit.revwalk.RevCommit;
1112
import org.eclipse.jgit.revwalk.RevWalk;
13+
import org.eclipse.jgit.revwalk.filter.RevFilter;
1214

1315
/**
1416
* Find the base/local/remote three commits:
@@ -21,26 +23,31 @@ public class GitRevisionLocator {
2123
private static final String REMOTE = "refs/remotes/origin/main";
2224

2325
public RevisionTriple locateMergeCommits(Git git) throws GitAPIException, IOException, JabRefException {
26+
Repository repo = git.getRepository();
2427
// assumes the remote branch is 'origin/main'
25-
ObjectId headId = git.getRepository().resolve(HEAD);
28+
ObjectId headId = repo.resolve(HEAD);
2629
// and uses the default remote tracking reference
2730
// does not support multiple remotes or custom remote branch names so far
28-
ObjectId remoteId = git.getRepository().resolve(REMOTE);
31+
ObjectId remoteId = repo.resolve(REMOTE);
2932
if (remoteId == null) {
3033
throw new IllegalStateException("Remote branch missing origin/main.");
3134
}
3235

3336
try (RevWalk walk = new RevWalk(git.getRepository())) {
3437
RevCommit local = walk.parseCommit(headId);
3538
RevCommit remote = walk.parseCommit(remoteId);
36-
37-
walk.setRevFilter(org.eclipse.jgit.revwalk.filter.RevFilter.MERGE_BASE);
38-
walk.markStart(local);
39-
walk.markStart(remote);
40-
41-
RevCommit base = walk.next();
39+
RevCommit base = findMergeBase(repo, local, remote);
4240

4341
return new RevisionTriple(base, local, remote);
4442
}
4543
}
44+
45+
public static RevCommit findMergeBase(Repository repo, RevCommit a, RevCommit b) throws IOException {
46+
try (RevWalk walk = new RevWalk(repo)) {
47+
walk.setRevFilter(RevFilter.MERGE_BASE);
48+
walk.markStart(a);
49+
walk.markStart(b);
50+
return walk.next();
51+
}
52+
}
4653
}

jablib/src/main/java/org/jabref/logic/git/model/MergeResult.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ public static MergeResult success() {
1414
return new MergeResult(SUCCESS, List.of());
1515
}
1616

17+
public static MergeResult failure() {
18+
return new MergeResult(false, List.of());
19+
}
20+
1721
public boolean hasConflicts() {
1822
return !conflicts.isEmpty();
1923
}

jablib/src/main/java/org/jabref/logic/git/status/GitStatusChecker.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
import org.slf4j.LoggerFactory;
1919

2020
/**
21-
* GitStatusChecker 用于从任意路径检测当前 Git 仓库状态。
22-
* 如果找不到仓库,返回 tracking = false 的 GitStatusSnapshot
23-
* 否则返回完整状态(tracking + syncStatus + conflict)。
21+
* This class is used to determine the status of a Git repository from any given path inside it.
22+
* If no repository is found, it returns a {@link GitStatusSnapshot} with tracking = false.
23+
* Otherwise, it returns a full snapshot including tracking status, sync status, and conflict state.
2424
*/
2525
public class GitStatusChecker {
2626
private static final Logger LOGGER = LoggerFactory.getLogger(GitStatusChecker.class);
@@ -39,7 +39,7 @@ public static GitStatusSnapshot checkStatus(Path anyPathInsideRepo) {
3939
boolean hasConflict = !status.getConflicting().isEmpty();
4040

4141
ObjectId localHead = repo.resolve("HEAD");
42-
ObjectId remoteHead = repo.resolve("@{u}");
42+
ObjectId remoteHead = repo.resolve("refs/remotes/origin/main");
4343
SyncStatus syncStatus = determineSyncStatus(repo, localHead, remoteHead);
4444

4545
return new GitStatusSnapshot(
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.jabref.logic.git.status;
2+
3+
import java.util.Optional;
4+
5+
public record GitStatusSnapshot(
6+
boolean tracking,
7+
SyncStatus syncStatus,
8+
boolean conflict,
9+
Optional<String> lastPulledCommit) { }

jablib/src/test/java/org/jabref/logic/git/GitSyncServiceTest.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,13 @@ void aliceBobSimple(@TempDir Path tempDir) throws Exception {
118118
aliceCommit = writeAndCommit(aliceUpdatedContent, "Fix author of a", alice, library, aliceGit);
119119
git.fetch().setRemote("origin").call();
120120

121-
// ToDo: Replace by call to GitSyncService crafting a merge commit
122-
// git.merge().include(aliceCommit).include(bobCommit).call(); // Will throw exception bc of merge conflict
123-
124121
// Debug hint: Show the created git graph on the command line
125122
// git log --graph --oneline --decorate --all --reflog
126123
}
127124

128125
@Test
129126
void pullTriggersSemanticMergeWhenNoConflicts() throws Exception {
130-
GitHandler gitHandler = mock(GitHandler.class);
127+
GitHandler gitHandler = new GitHandler(library.getParent());
131128
GitSyncService syncService = new GitSyncService(importFormatPreferences, gitHandler);
132129
MergeResult result = syncService.fetchAndMerge(library);
133130

@@ -151,7 +148,7 @@ void pullTriggersSemanticMergeWhenNoConflicts() throws Exception {
151148

152149
@Test
153150
void pushTriggersMergeAndPushWhenNoConflicts() throws Exception {
154-
GitHandler gitHandler = mock(GitHandler.class);
151+
GitHandler gitHandler = new GitHandler(library.getParent());
155152
GitSyncService syncService = new GitSyncService(importFormatPreferences, gitHandler);
156153
syncService.push(library);
157154

@@ -171,7 +168,6 @@ void pushTriggersMergeAndPushWhenNoConflicts() throws Exception {
171168
assertEquals(normalize(expected), normalize(pushedContent));
172169
}
173170

174-
175171
@Test
176172
void readFromCommits() throws Exception {
177173
String base = GitFileReader.readFileFromCommit(git, baseCommit, Path.of("library.bib"));

jablib/src/test/java/org/jabref/logic/git/merge/GitSemanticMergeExecutorTest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import java.nio.file.Files;
55
import java.nio.file.Path;
66

7+
import javafx.collections.FXCollections;
8+
79
import org.jabref.logic.git.model.MergeResult;
810
import org.jabref.logic.importer.ImportFormatPreferences;
911
import org.jabref.model.database.BibDatabaseContext;
@@ -12,9 +14,11 @@
1214

1315
import org.junit.jupiter.api.BeforeEach;
1416
import org.junit.jupiter.api.Test;
17+
import org.mockito.Answers;
1518

1619
import static org.junit.jupiter.api.Assertions.assertTrue;
1720
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.when;
1822

1923
public class GitSemanticMergeExecutorTest {
2024

@@ -40,7 +44,10 @@ public void setup() throws IOException {
4044
local.getDatabase().insertEntry(localEntry);
4145
remote.getDatabase().insertEntry(remoteEntry);
4246

43-
preferences = mock(ImportFormatPreferences.class);
47+
preferences = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS);
48+
when(preferences.fieldPreferences().getNonWrappableFields())
49+
.thenReturn(FXCollections.emptyObservableList());
50+
4451
executor = new GitSemanticMergeExecutorImpl(preferences);
4552

4653
tempFile = Files.createTempFile("merged", ".bib");

0 commit comments

Comments
 (0)