-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Implement logic orchestration for Git Pull/Push operations #13518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
subhramit
merged 48 commits into
JabRef:main
from
wanling0000:clean-gsoc-git-support-init
Aug 1, 2025
Merged
Changes from all commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
43ca5f7
init: add basic test case for git sync service
wanling0000 7fa3b86
feat(git): Add SemanticMerger MVP #12350
wanling0000 6233df9
feat(git): Implement normal sync loop without conflicts #12350
wanling0000 8d3fa2b
chore(git): Apply review suggestions #12350
wanling0000 269473d
chore(git): Fix CI-related issues #12350
wanling0000 ac66eed
chore(git): Add push test + Fix failing unit test #12350
wanling0000 69575b5
chore(git): Fix variable names + refactoring (moving the GitConflictR…
wanling0000 3fc49c3
refactor(git): Apply strategy pattern for conflict resolution + add G…
wanling0000 32c30a0
fix: Repair failing unit tests and CI integration tests #12350
wanling0000 f8250d1
Fix submodules
wanling0000 477cfae
Merge remote-tracking branch 'upstream/main' into clean-gsoc-git-supp…
wanling0000 61a19b8
test: Try to fix GitHandlerTest by ensuring remote main branch exists…
wanling0000 7e36b2e
test: Try to fix GitSyncServiceTest/GitStatusCheckerTest by ensuring …
wanling0000 cf60c02
test: Try to fix GitSyncServiceTest by explicitly pushing refspec #12350
wanling0000 bf05ce9
Merge branch 'main' into clean-gsoc-git-support-init
wanling0000 5799483
test: Try to fix GitSyncServiceTest by explicitly checking out to the…
wanling0000 36e7d95
Merge branch 'clean-gsoc-git-support-init' of github.com:wanling0000/…
wanling0000 9107ddf
test: Try to fix GitSyncServiceTest #12350
wanling0000 0ea08f1
Change exception logging
koppor bd2a738
Merge branch 'main' into clean-gsoc-git-support-init
koppor a78c8c4
test: Add debug output to GitSyncServiceTest #12350
wanling0000 fe3d84e
Merge branch 'clean-gsoc-git-support-init' of github.com:wanling0000/…
wanling0000 d055947
test: Fix GitSyncServiceTest by closing Git resources and improving c…
wanling0000 22f9704
test: Fix GitSyncServiceTest by switching to init() + remoteAdd() + p…
wanling0000 a0a39cc
test: Remove redundant git field in GitSyncServiceTest #12350
wanling0000 0419771
Merge remote-tracking branch 'upstream/main' into clean-gsoc-git-supp…
wanling0000 15aca84
fix: Apply trag bot suggestion and fix bug where newly merged entries…
wanling0000 b9a072a
Merge remote-tracking branch 'upstream/main' into clean-gsoc-git-supp…
wanling0000 1e13ffd
fix: Resolve Modernizer violations #12350
wanling0000 aaeac7c
chore: Apply valid trag-bot suggestions #12350
wanling0000 fc67d0d
chore: Apply valid trag-bot suggestions in logic module; partially up…
wanling0000 c721298
chore: Apply valid trag-bot suggestions (change RevisionTriple.base t…
wanling0000 d3f5536
Use JSpecify
koppor 363698c
chore: Apply review suggestions #12350
wanling0000 2f4fd46
Merge branch 'clean-gsoc-git-support-init' of github.com:wanling0000/…
wanling0000 44f147a
chore: Fix markdown formatting issues #12350
wanling0000 14358c5
chore: Apply OpenRewrite autoformat fixes #12350
wanling0000 7dfa8b5
fix: Fix failing jablib tests #12350
wanling0000 cdb5590
Apply suggestions from code review
wanling0000 c21d4a9
chore: Rename Optional variable names from maybe to Opt for clarity #…
wanling0000 69a52c5
Merge branch 'clean-gsoc-git-support-init' of github.com:wanling0000/…
wanling0000 1052d4c
chore: Import Optional dependencies #12350
wanling0000 c371370
chore: Apply review suggestions #12350
wanling0000 6f36b5c
Merge remote-tracking branch 'upstream/main' into clean-gsoc-git-supp…
wanling0000 44c3fac
fix: Replace BibEntry.clone() with copy constructor #12350
wanling0000 2f763f4
Merge remote-tracking branch 'upstream/main' into clean-gsoc-git-supp…
wanling0000 3aa5d23
fix: Update import paths #12350
wanling0000 0319c09
retrigger CI
wanling0000 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
# Git | ||
|
||
## Why Semantic Merge? | ||
|
||
In JabRef, we aim to minimize user interruptions when collaborating on the same `.bib` library file using Git. To achieve this, we go beyond Git’s default line-based syntactic merging and implement our own semantic merge logic that understands the structure of BibTeX entries. | ||
|
||
This means: | ||
|
||
* Even if Git detects conflicting lines, | ||
* JabRef is able to recognize that both sides are editing the same BibTeX entry, | ||
* And determine—at the field level—whether there is an actual semantic conflict. | ||
|
||
## Merge Example | ||
|
||
The following example illustrates a case where Git detects a conflict, but JabRef is able to resolve it automatically. | ||
|
||
koppor marked this conversation as resolved.
Show resolved
Hide resolved
|
||
### Base Version | ||
|
||
```bibtex | ||
@article{a, | ||
author = {don't know the author}, | ||
doi = {xya}, | ||
} | ||
|
||
@article{b, | ||
author = {don't know the author}, | ||
doi = {xyz}, | ||
} | ||
``` | ||
|
||
### Bob's Side | ||
|
||
Bob reorders the entries and updates the author field of entry b: | ||
|
||
```bibtex | ||
@article{b, | ||
author = {author-b}, | ||
doi = {xyz}, | ||
} | ||
|
||
@article{a, | ||
author = {don't know the author}, | ||
doi = {xya}, | ||
} | ||
``` | ||
|
||
### Alice's Side | ||
|
||
Alice modifies the author field of entry a: | ||
|
||
```bibtex | ||
@article{a, | ||
author = {author-a}, | ||
doi = {xya}, | ||
} | ||
|
||
@article{b, | ||
author = {don't know the author}, | ||
doi = {xyz}, | ||
} | ||
``` | ||
|
||
### Merge Outcome | ||
|
||
When Alice runs git pull, Git sees that both branches have modified overlapping lines (due to reordering and content changes) and reports a syntactic conflict. | ||
|
||
However, JabRef is able to analyze the entries and determine that: | ||
|
||
* Entry a was modified only by Alice. | ||
* Entry b was modified only by Bob. | ||
* There is no conflict at the field level. | ||
* The order of entries in the file does not affect BibTeX semantics. | ||
|
||
Therefore, JabRef performs an automatic merge without requiring manual conflict resolution. | ||
|
||
## Related Test Cases | ||
|
||
The semantic conflict detection and merge resolution logic is covered by: | ||
|
||
* `org.jabref.logic.git.util.SemanticMergerTest#patchDatabase` | ||
* `org.jabref.logic.git.util.SemanticConflictDetectorTest#semanticConflicts`. | ||
|
||
## Conflict Scenarios | ||
|
||
The following table describes when semantic merge in JabRef should consider a situation as conflict or not during a three-way merge. | ||
|
||
| ID | Base | Local Change | Remote Change | Result | | ||
|------|----------------------------|------------------------------------|------------------------------------|--------| | ||
| T1 | Field present | (unchanged) | Field modified | No conflict. The local version remained unchanged, so the remote change can be safely applied. | | ||
| T2 | Field present | Field modified | (unchanged) | No conflict. The remote version did not touch the field, so the local change is preserved. | | ||
| T3 | Field present | Field changed to same value | Field changed to same value | No conflict. Although both sides changed the field, the result is identical—therefore, no conflict. | | ||
| T4 | Field present | Field changed to A | Field changed to B | Conflict. This is a true semantic conflict that requires resolution. | | ||
| T5 | Field present | Field deleted | Field modified | Conflict. One side deleted the field while the other updated it—this is contradictory. | | ||
| T6 | Field present | Field modified | Field deleted | Conflict. Similar to T5, one side deletes, the other edits—this is a conflict. | | ||
| T7 | Field present | (unchanged) | Field deleted | No conflict. Local did not modify anything, so remote deletion is accepted. | | ||
| T8 | Entry with fields A and B | Field A modified | Field B modified | No conflict. Changes are on separate fields, so they can be merged safely. | | ||
| T9 | Entry with fields A and B | Field order changed | Field order changed differently | No conflict. Field order is not semantically meaningful, so no conflict is detected. | | ||
| T10 | Entries A and B | Entry A modified | Entry B modified | No conflict. Modifications are on different entries, which are always safe to merge. | | ||
| T11 | Entry with existing fields | (unchanged) | New field added | No conflict. Remote addition can be applied without issues. | | ||
| T12 | Entry with existing fields | New field added with value A | New field added with value B | Conflict. One side added while the other side modified—there is a semantic conflict. | | ||
| T13 | Entry with existing fields | New field added | (unchanged) | No conflict. Safe to preserve the local addition. | | ||
| T14 | Entry with existing fields | New field added with value A | New field added with value A | No conflict. Even though both sides added it, the value is the same—no need for resolution. | | ||
| T15 | Entry with existing fields | New field added with value A | New field added with value B | Conflict. The same field is introduced with different values, which creates a conflict. | | ||
| T16 | (entry not present) | New entry with author A | New entry with author B | Conflict. Both sides created a new entry with the same citation key, but the fields differ. | | ||
| T17 | (entry not present) | New entry with identical fields | New entry with identical fields | No conflict. Both sides created a new entry with the same citation key and identical fields, so it can be merged safely. | |
44 changes: 44 additions & 0 deletions
44
jabgui/src/main/java/org/jabref/gui/git/GitConflictResolverDialog.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package org.jabref.gui.git; | ||
|
||
import java.util.Optional; | ||
|
||
import org.jabref.gui.DialogService; | ||
import org.jabref.gui.mergeentries.threewaymerge.MergeEntriesDialog; | ||
import org.jabref.gui.mergeentries.threewaymerge.ShowDiffConfig; | ||
import org.jabref.gui.mergeentries.threewaymerge.diffhighlighter.DiffHighlighter; | ||
import org.jabref.gui.mergeentries.threewaymerge.toolbar.ThreeWayMergeToolbar; | ||
import org.jabref.gui.preferences.GuiPreferences; | ||
import org.jabref.logic.git.conflicts.ThreeWayEntryConflict; | ||
import org.jabref.logic.l10n.Localization; | ||
import org.jabref.model.entry.BibEntry; | ||
|
||
/// A wrapper around {@link MergeEntriesDialog} for Git feature | ||
/// | ||
/// Receives a semantic conflict (ThreeWayEntryConflict), pops up an interactive GUI (belonging to mergeentries), and returns a user-confirmed BibEntry merge result. | ||
public class GitConflictResolverDialog { | ||
private final DialogService dialogService; | ||
private final GuiPreferences preferences; | ||
|
||
public GitConflictResolverDialog(DialogService dialogService, GuiPreferences preferences) { | ||
this.dialogService = dialogService; | ||
this.preferences = preferences; | ||
} | ||
|
||
public Optional<BibEntry> resolveConflict(ThreeWayEntryConflict conflict) { | ||
BibEntry base = conflict.base(); | ||
BibEntry local = conflict.local(); | ||
BibEntry remote = conflict.remote(); | ||
|
||
MergeEntriesDialog dialog = new MergeEntriesDialog(local, remote, preferences); | ||
dialog.setLeftHeaderText(Localization.lang("Local")); | ||
dialog.setRightHeaderText(Localization.lang("Remote")); | ||
ShowDiffConfig diffConfig = new ShowDiffConfig( | ||
ThreeWayMergeToolbar.DiffView.SPLIT, | ||
DiffHighlighter.BasicDiffMethod.WORDS | ||
); | ||
dialog.configureDiff(diffConfig); | ||
|
||
return dialogService.showCustomDialogAndWait(dialog) | ||
.map(result -> result.mergedEntry()); | ||
} | ||
} |
116 changes: 116 additions & 0 deletions
116
jabgui/src/main/java/org/jabref/gui/git/GitPullAction.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package org.jabref.gui.git; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Path; | ||
import java.util.Optional; | ||
|
||
import org.jabref.gui.DialogService; | ||
import org.jabref.gui.StateManager; | ||
import org.jabref.gui.actions.SimpleCommand; | ||
import org.jabref.gui.preferences.GuiPreferences; | ||
import org.jabref.logic.JabRefException; | ||
import org.jabref.logic.git.GitHandler; | ||
import org.jabref.logic.git.GitSyncService; | ||
import org.jabref.logic.git.conflicts.GitConflictResolverStrategy; | ||
import org.jabref.logic.git.merge.GitSemanticMergeExecutor; | ||
import org.jabref.logic.git.merge.GitSemanticMergeExecutorImpl; | ||
import org.jabref.logic.l10n.Localization; | ||
import org.jabref.logic.util.BackgroundTask; | ||
import org.jabref.logic.util.TaskExecutor; | ||
import org.jabref.model.database.BibDatabaseContext; | ||
|
||
import org.eclipse.jgit.api.errors.GitAPIException; | ||
|
||
public class GitPullAction extends SimpleCommand { | ||
|
||
private final DialogService dialogService; | ||
private final StateManager stateManager; | ||
private final GuiPreferences guiPreferences; | ||
private final TaskExecutor taskExecutor; | ||
|
||
public GitPullAction(DialogService dialogService, | ||
StateManager stateManager, | ||
GuiPreferences guiPreferences, | ||
TaskExecutor taskExecutor) { | ||
this.dialogService = dialogService; | ||
this.stateManager = stateManager; | ||
this.guiPreferences = guiPreferences; | ||
this.taskExecutor = taskExecutor; | ||
} | ||
|
||
@Override | ||
public void execute() { | ||
Optional<BibDatabaseContext> activeDatabaseOpt = stateManager.getActiveDatabase(); | ||
if (activeDatabaseOpt.isEmpty()) { | ||
dialogService.showErrorDialogAndWait( | ||
Localization.lang("No library open"), | ||
Localization.lang("Please open a library before pulling.") | ||
); | ||
return; | ||
} | ||
|
||
BibDatabaseContext activeDatabase = activeDatabaseOpt.get(); | ||
Optional<Path> bibFilePathOpt = activeDatabase.getDatabasePath(); | ||
if (bibFilePathOpt.isEmpty()) { | ||
dialogService.showErrorDialogAndWait( | ||
Localization.lang("No library file path"), | ||
Localization.lang("Cannot pull from Git: No file is associated with this library.") | ||
); | ||
return; | ||
} | ||
|
||
Path bibFilePath = bibFilePathOpt.get(); | ||
GitHandler handler = new GitHandler(bibFilePath.getParent()); | ||
InAnYan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
GitConflictResolverDialog dialog = new GitConflictResolverDialog(dialogService, guiPreferences); | ||
InAnYan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
GitConflictResolverStrategy resolver = new GuiGitConflictResolverStrategy(dialog); | ||
GitSemanticMergeExecutor mergeExecutor = new GitSemanticMergeExecutorImpl(guiPreferences.getImportFormatPreferences()); | ||
|
||
GitSyncService syncService = new GitSyncService(guiPreferences.getImportFormatPreferences(), handler, resolver, mergeExecutor); | ||
GitStatusViewModel statusViewModel = new GitStatusViewModel(stateManager, bibFilePath); | ||
GitPullViewModel viewModel = new GitPullViewModel(syncService, statusViewModel); | ||
|
||
BackgroundTask | ||
.wrap(() -> viewModel.pull()) | ||
.onSuccess(result -> { | ||
if (result.isSuccessful()) { | ||
dialogService.showInformationDialogAndWait( | ||
Localization.lang("Git Pull"), | ||
Localization.lang("Successfully merged and updated.") | ||
); | ||
} else { | ||
dialogService.showWarningDialogAndWait( | ||
Localization.lang("Git Pull"), | ||
Localization.lang("Merge completed with conflicts.") | ||
); | ||
} | ||
}) | ||
.onFailure(ex -> { | ||
if (ex instanceof JabRefException e) { | ||
dialogService.showErrorDialogAndWait( | ||
Localization.lang("Git Pull Failed"), | ||
e.getLocalizedMessage(), | ||
e | ||
); | ||
} else if (ex instanceof GitAPIException e) { | ||
dialogService.showErrorDialogAndWait( | ||
Localization.lang("Git Pull Failed"), | ||
Localization.lang("An unexpected Git error occurred: %0", e.getLocalizedMessage()), | ||
e | ||
); | ||
} else if (ex instanceof IOException e) { | ||
dialogService.showErrorDialogAndWait( | ||
Localization.lang("Git Pull Failed"), | ||
Localization.lang("I/O error: %0", e.getLocalizedMessage()), | ||
e | ||
); | ||
} else { | ||
dialogService.showErrorDialogAndWait( | ||
Localization.lang("Git Pull Failed"), | ||
Localization.lang("Unexpected error: %0", ex.getLocalizedMessage()), | ||
ex | ||
); | ||
} | ||
}) | ||
.executeWith(taskExecutor); | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
jabgui/src/main/java/org/jabref/gui/git/GitPullViewModel.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package org.jabref.gui.git; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Path; | ||
import java.util.Optional; | ||
|
||
import org.jabref.gui.AbstractViewModel; | ||
import org.jabref.logic.JabRefException; | ||
import org.jabref.logic.git.GitSyncService; | ||
import org.jabref.logic.git.model.MergeResult; | ||
import org.jabref.logic.l10n.Localization; | ||
import org.jabref.model.database.BibDatabaseContext; | ||
|
||
import org.eclipse.jgit.api.errors.GitAPIException; | ||
|
||
public class GitPullViewModel extends AbstractViewModel { | ||
private final GitSyncService syncService; | ||
private final GitStatusViewModel gitStatusViewModel; | ||
|
||
public GitPullViewModel(GitSyncService syncService, GitStatusViewModel gitStatusViewModel) { | ||
this.syncService = syncService; | ||
this.gitStatusViewModel = gitStatusViewModel; | ||
} | ||
|
||
public MergeResult pull() throws IOException, GitAPIException, JabRefException { | ||
InAnYan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Optional<BibDatabaseContext> databaseContextOpt = gitStatusViewModel.getDatabaseContext(); | ||
if (databaseContextOpt.isEmpty()) { | ||
throw new JabRefException(Localization.lang("Cannot pull: No active BibDatabaseContext.")); | ||
} | ||
|
||
BibDatabaseContext localBibDatabaseContext = databaseContextOpt.get(); | ||
Path bibFilePath = localBibDatabaseContext.getDatabasePath().orElseThrow(() -> | ||
new JabRefException(Localization.lang("Cannot pull: .bib file path missing in BibDatabaseContext.")) | ||
); | ||
|
||
MergeResult result = syncService.fetchAndMerge(localBibDatabaseContext, bibFilePath); | ||
|
||
if (result.isSuccessful()) { | ||
gitStatusViewModel.updateStatusFromContext(localBibDatabaseContext); | ||
} | ||
|
||
return result; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.