Skip to content

Commit b3bc033

Browse files
committed
refactor(git): Apply strategy pattern for conflict resolution + add GitMergeUtil tests #12350
1 parent 55b4ff6 commit b3bc033

File tree

13 files changed

+313
-164
lines changed

13 files changed

+313
-164
lines changed

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@
88
import org.jabref.gui.mergeentries.newmergedialog.diffhighlighter.DiffHighlighter;
99
import org.jabref.gui.mergeentries.newmergedialog.toolbar.ThreeWayMergeToolbar;
1010
import org.jabref.gui.preferences.GuiPreferences;
11-
import org.jabref.logic.git.conflicts.GitConflictResolver;
1211
import org.jabref.logic.git.conflicts.ThreeWayEntryConflict;
1312
import org.jabref.model.entry.BibEntry;
1413

1514
/**
1615
* UI wrapper
1716
* Receives a semantic conflict (ThreeWayEntryConflict), pops up an interactive GUI (belonging to mergeentries), and returns a user-confirmed BibEntry merge result.
1817
*/
19-
public class GitConflictResolverDialog implements GitConflictResolver {
18+
public class GitConflictResolverDialog {
2019
private final DialogService dialogService;
2120
private final GuiPreferences preferences;
2221

@@ -25,7 +24,6 @@ public GitConflictResolverDialog(DialogService dialogService, GuiPreferences pre
2524
this.preferences = preferences;
2625
}
2726

28-
@Override
2927
public Optional<BibEntry> resolveConflict(ThreeWayEntryConflict conflict) {
3028
BibEntry base = conflict.base();
3129
BibEntry local = conflict.local();

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import org.jabref.gui.actions.SimpleCommand;
1111
import org.jabref.gui.preferences.GuiPreferences;
1212
import org.jabref.logic.JabRefException;
13+
import org.jabref.logic.git.GitHandler;
14+
import org.jabref.logic.git.GitSyncService;
15+
import org.jabref.logic.git.conflicts.GitConflictResolverStrategy;
1316
import org.jabref.logic.git.model.MergeResult;
1417
import org.jabref.model.database.BibDatabaseContext;
1518

@@ -53,12 +56,15 @@ public void execute() {
5356

5457
Path bibFilePath = database.getDatabasePath().get();
5558
try {
56-
GitPullViewModel viewModel = new GitPullViewModel(
57-
guiPreferences.getImportFormatPreferences(),
58-
new GitConflictResolverDialog(dialogService, guiPreferences),
59-
dialogService
60-
);
61-
MergeResult result = viewModel.pull(bibFilePath);
59+
GitHandler handler = new GitHandler(bibFilePath.getParent());
60+
GitConflictResolverDialog dialog = new GitConflictResolverDialog(dialogService, guiPreferences);
61+
GitConflictResolverStrategy resolver = new GuiConflictResolverStrategy(dialog);
62+
63+
GitSyncService syncService = new GitSyncService(guiPreferences.getImportFormatPreferences(), handler, resolver);
64+
GitStatusViewModel statusViewModel = new GitStatusViewModel(bibFilePath);
65+
66+
GitPullViewModel viewModel = new GitPullViewModel(syncService, statusViewModel);
67+
MergeResult result = viewModel.pull();
6268

6369
if (result.isSuccessful()) {
6470
dialogService.showInformationDialogAndWait("Git Pull", "Successfully merged and updated.");

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

Lines changed: 8 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -2,119 +2,32 @@
22

33
import java.io.IOException;
44
import java.nio.file.Path;
5-
import java.util.ArrayList;
6-
import java.util.List;
7-
import java.util.Optional;
85

96
import org.jabref.gui.AbstractViewModel;
10-
import org.jabref.gui.DialogService;
117
import org.jabref.logic.JabRefException;
12-
import org.jabref.logic.git.GitConflictResolver;
13-
import org.jabref.logic.git.GitHandler;
14-
import org.jabref.logic.git.conflicts.GitConflictResolver;
15-
import org.jabref.logic.git.conflicts.SemanticConflictDetector;
16-
import org.jabref.logic.git.conflicts.ThreeWayEntryConflict;
17-
import org.jabref.logic.git.io.GitBibParser;
18-
import org.jabref.logic.git.io.GitFileReader;
19-
import org.jabref.logic.git.io.GitFileWriter;
20-
import org.jabref.logic.git.io.GitRevisionLocator;
21-
import org.jabref.logic.git.io.RevisionTriple;
22-
import org.jabref.logic.git.merge.GitMergeUtil;
23-
import org.jabref.logic.git.merge.MergePlan;
24-
import org.jabref.logic.git.merge.SemanticMerger;
8+
import org.jabref.logic.git.GitSyncService;
259
import org.jabref.logic.git.model.MergeResult;
26-
import org.jabref.logic.importer.ImportFormatPreferences;
27-
import org.jabref.model.database.BibDatabaseContext;
28-
import org.jabref.model.entry.BibEntry;
2910

30-
import org.eclipse.jgit.api.Git;
3111
import org.eclipse.jgit.api.errors.GitAPIException;
32-
import org.eclipse.jgit.revwalk.RevCommit;
3312

3413
public class GitPullViewModel extends AbstractViewModel {
35-
private final ImportFormatPreferences importFormatPreferences;
36-
private final GitConflictResolver conflictResolver;
37-
private final DialogService dialogService;
38-
private final GitHandler gitHandler;
14+
private final GitSyncService syncService;
3915
private final GitStatusViewModel gitStatusViewModel;
4016
private final Path bibFilePath;
4117

42-
public GitPullViewModel(ImportFormatPreferences importFormatPreferences,
43-
GitConflictResolver conflictResolver,
44-
DialogService dialogService,
45-
GitHandler gitHandler,
46-
GitStatusViewModel gitStatusViewModel) {
47-
this.importFormatPreferences = importFormatPreferences;
48-
this.conflictResolver = conflictResolver;
49-
this.dialogService = dialogService;
50-
this.gitHandler = gitHandler;
18+
public GitPullViewModel(GitSyncService syncService, GitStatusViewModel gitStatusViewModel) {
19+
this.syncService = syncService;
5120
this.gitStatusViewModel = gitStatusViewModel;
5221
this.bibFilePath = gitStatusViewModel.getCurrentBibFile();
5322
}
5423

5524
public MergeResult pull() throws IOException, GitAPIException, JabRefException {
56-
// Open the Git repository from the parent folder of the .bib file
57-
Git git = Git.open(gitHandler.getRepositoryPathAsFile());
25+
MergeResult result = syncService.fetchAndMerge(bibFilePath);
5826

59-
gitHandler.fetchOnCurrentBranch();
60-
61-
// Determine the three-way merge base, local, and remote commits
62-
GitRevisionLocator locator = new GitRevisionLocator();
63-
RevisionTriple triple = locator.locateMergeCommits(git);
64-
65-
RevCommit baseCommit = triple.base();
66-
RevCommit localCommit = triple.local();
67-
RevCommit remoteCommit = triple.remote();
68-
69-
// Ensure file is inside the Git working tree
70-
Path repoRoot = gitHandler.getRepositoryPathAsFile().toPath().toRealPath();
71-
Path resolvedBibPath = bibFilePath.toRealPath();
72-
if (!resolvedBibPath.startsWith(repoRoot)) {
73-
throw new JabRefException("The provided .bib file is not inside the Git repository.");
27+
if (result.isSuccessful()) {
28+
gitStatusViewModel.updateStatusFromPath(bibFilePath);
7429
}
75-
Path relativePath = repoRoot.relativize(resolvedBibPath);
76-
77-
// 1. Load three versions
78-
String baseContent = GitFileReader.readFileFromCommit(git, baseCommit, relativePath);
79-
String localContent = GitFileReader.readFileFromCommit(git, localCommit, relativePath);
80-
String remoteContent = GitFileReader.readFileFromCommit(git, remoteCommit, relativePath);
81-
82-
BibDatabaseContext base = GitBibParser.parseBibFromGit(baseContent, importFormatPreferences);
83-
BibDatabaseContext local = GitBibParser.parseBibFromGit(localContent, importFormatPreferences);
84-
BibDatabaseContext remote = GitBibParser.parseBibFromGit(remoteContent, importFormatPreferences);
85-
86-
// 2. Conflict detection
87-
List<ThreeWayEntryConflict> conflicts = SemanticConflictDetector.detectConflicts(base, local, remote);
88-
89-
// 3. If there are conflicts, prompt user to resolve them via GUI
90-
BibDatabaseContext effectiveRemote = remote;
91-
if (!conflicts.isEmpty()) {
92-
List<BibEntry> resolvedRemoteEntries = new ArrayList<>();
93-
for (ThreeWayEntryConflict conflict : conflicts) {
94-
// Ask user to resolve this conflict via GUI dialog
95-
Optional<BibEntry> maybeResolved = conflictResolver.resolveConflict(conflict);
96-
if (maybeResolved.isPresent()) {
97-
resolvedRemoteEntries.add(maybeResolved.get());
98-
} else {
99-
// User canceled the merge dialog → abort the whole merge
100-
throw new JabRefException("Merge aborted: Not all conflicts were resolved by user.");
101-
}
102-
}
103-
// Replace original conflicting entries in remote with resolved versions
104-
effectiveRemote = GitMergeUtil.replaceEntries(remote, resolvedRemoteEntries);
105-
}
106-
107-
// Extract merge plan and apply it to the local database
108-
MergePlan plan = SemanticConflictDetector.extractMergePlan(base, effectiveRemote);
109-
SemanticMerger.applyMergePlan(local, plan);
110-
111-
// Save merged result to .bib file
112-
GitFileWriter.write(bibFilePath, local, importFormatPreferences);
113-
114-
// Create Git commit for the merged result
115-
gitHandler.createCommitOnCurrentBranch("Auto-merged by JabRef", true);
11630

117-
gitStatusViewModel.updateStatusFromPath(bibFilePath);
118-
return MergeResult.success();
31+
return result;
11932
}
12033
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package org.jabref.gui.git;
2+
3+
import java.nio.file.Path;
4+
import java.util.Optional;
5+
6+
import javafx.beans.property.BooleanProperty;
7+
import javafx.beans.property.ObjectProperty;
8+
import javafx.beans.property.SimpleBooleanProperty;
9+
import javafx.beans.property.SimpleObjectProperty;
10+
import javafx.beans.property.SimpleStringProperty;
11+
import javafx.beans.property.StringProperty;
12+
13+
import org.jabref.gui.AbstractViewModel;
14+
import org.jabref.logic.git.GitHandler;
15+
import org.jabref.logic.git.status.GitStatusChecker;
16+
import org.jabref.logic.git.status.GitStatusSnapshot;
17+
import org.jabref.logic.git.status.SyncStatus;
18+
19+
/**
20+
* ViewModel that holds current Git sync status for the open .bib database.
21+
* 统一维护当前路径绑定的 GitHandler 状态,包括:
22+
* - 是否是 Git 仓库
23+
* - 当前是否被 Git 跟踪
24+
* - 是否存在冲突
25+
* - 当前同步状态(UP_TO_DATE、DIVERGED 等)
26+
*/
27+
public class GitStatusViewModel extends AbstractViewModel {
28+
private final Path currentBibFile;
29+
private final ObjectProperty<SyncStatus> syncStatus = new SimpleObjectProperty<>(SyncStatus.UNTRACKED);
30+
private final BooleanProperty isTracking = new SimpleBooleanProperty(false);
31+
private final BooleanProperty conflictDetected = new SimpleBooleanProperty(false);
32+
private final StringProperty lastPulledCommit = new SimpleStringProperty("");
33+
private GitHandler activeHandler = null;
34+
35+
public GitStatusViewModel(Path bibFilePath) {
36+
this.currentBibFile = bibFilePath;
37+
updateStatusFromPath(bibFilePath);
38+
}
39+
40+
/**
41+
* Try to detect Git repository status from the given file or folder path.
42+
*
43+
* @param fileOrFolderInRepo Any path (file or folder) assumed to be inside a Git repository
44+
*/
45+
public void updateStatusFromPath(Path fileOrFolderInRepo) {
46+
Optional<GitHandler> maybeHandler = GitHandler.fromAnyPath(fileOrFolderInRepo);
47+
48+
if (!maybeHandler.isPresent()) {
49+
reset();
50+
return;
51+
}
52+
53+
GitHandler handler = maybeHandler.get();
54+
this.activeHandler = handler;
55+
56+
GitStatusSnapshot snapshot = GitStatusChecker.checkStatus(fileOrFolderInRepo);
57+
58+
setTracking(snapshot.tracking());
59+
setSyncStatus(snapshot.syncStatus());
60+
setConflictDetected(snapshot.conflict());
61+
snapshot.lastPulledCommit().ifPresent(this::setLastPulledCommit);
62+
}
63+
64+
/**
65+
* Clears all internal state to defaults.
66+
* Should be called when switching projects or Git context is lost
67+
*/
68+
public void reset() {
69+
setSyncStatus(SyncStatus.UNTRACKED);
70+
setTracking(false);
71+
setConflictDetected(false);
72+
setLastPulledCommit("");
73+
}
74+
75+
public ObjectProperty<SyncStatus> syncStatusProperty() {
76+
return syncStatus;
77+
}
78+
79+
public SyncStatus getSyncStatus() {
80+
return syncStatus.get();
81+
}
82+
83+
public void setSyncStatus(SyncStatus status) {
84+
this.syncStatus.set(status);
85+
}
86+
87+
public BooleanProperty isTrackingProperty() {
88+
return isTracking;
89+
}
90+
91+
public boolean isTracking() {
92+
return isTracking.get();
93+
}
94+
95+
public void setTracking(boolean tracking) {
96+
this.isTracking.set(tracking);
97+
}
98+
99+
public BooleanProperty conflictDetectedProperty() {
100+
return conflictDetected;
101+
}
102+
103+
public boolean isConflictDetected() {
104+
return conflictDetected.get();
105+
}
106+
107+
public void setConflictDetected(boolean conflict) {
108+
this.conflictDetected.set(conflict);
109+
}
110+
111+
public StringProperty lastPulledCommitProperty() {
112+
return lastPulledCommit;
113+
}
114+
115+
public String getLastPulledCommit() {
116+
return lastPulledCommit.get();
117+
}
118+
119+
public void setLastPulledCommit(String commitHash) {
120+
this.lastPulledCommit.set(commitHash);
121+
}
122+
123+
public Optional<GitHandler> getActiveHandler() {
124+
return Optional.ofNullable(activeHandler);
125+
}
126+
127+
public Path getCurrentBibFile() {
128+
return currentBibFile;
129+
}
130+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.jabref.gui.git;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Optional;
6+
7+
import org.jabref.logic.git.conflicts.GitConflictResolverStrategy;
8+
import org.jabref.logic.git.conflicts.ThreeWayEntryConflict;
9+
import org.jabref.logic.git.merge.GitMergeUtil;
10+
import org.jabref.model.database.BibDatabaseContext;
11+
import org.jabref.model.entry.BibEntry;
12+
13+
public class GuiConflictResolverStrategy implements GitConflictResolverStrategy {
14+
private final GitConflictResolverDialog dialog;
15+
16+
public GuiConflictResolverStrategy(GitConflictResolverDialog dialog) {
17+
this.dialog = dialog;
18+
}
19+
20+
@Override
21+
public Optional<BibDatabaseContext> resolveConflicts(List<ThreeWayEntryConflict> conflicts, BibDatabaseContext remote) {
22+
List<BibEntry> resolved = new ArrayList<>();
23+
for (ThreeWayEntryConflict conflict : conflicts) {
24+
Optional<BibEntry> maybeConflict = dialog.resolveConflict(conflict);
25+
if (maybeConflict.isEmpty()) {
26+
return Optional.empty();
27+
}
28+
resolved.add(maybeConflict.get());
29+
}
30+
return Optional.of(GitMergeUtil.replaceEntries(remote, resolved));
31+
}
32+
}

jablib/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
exports org.jabref.logic.git.merge;
111111
exports org.jabref.logic.git.io;
112112
exports org.jabref.logic.git.model;
113+
exports org.jabref.logic.git.status;
113114

114115
requires java.base;
115116

0 commit comments

Comments
 (0)