Skip to content

WIP: GSoC Git Support - Support semantic pull merge without conflicts #714

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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

wanling0000
Copy link

Closes JabRef#12350

Steps to test

This PR implements the first part of Git sync support by enabling automatic semantic merges when there are no conflicts. The scenario is tested via TDD in GitSyncServiceTest, simulating the following steps:

  1. Alice creates an initial commit and pushes it to the remote.
  2. Bob clones the repo and modifies entry b, then pushes.
  3. Alice modifies entry a, then pulls.

Since they changed different entries, we expect a clean merge without user intervention. The orchestrator is GitSyncService, and helper utilities and value objects are temporarily located in org.jabref.logic.git.util.

Mandatory checks

  • I own the copyright of the code submitted and I license it under the MIT license
  • [.] Change in CHANGELOG.md described in a way that is understandable for the average user (if change is visible to the user)
  • Tests created for changes (if applicable)
  • Manually tested changed features in running JabRef (always required)
  • [.] Screenshots added in PR description (if change is visible to the user)
  • Checked developer's documentation: Is the information available and up to date? If not, I outlined it in this pull request.
  • [.] Checked documentation: Is the information available and up to date? If not, I created an issue at https://github.com/JabRef/user-documentation/issues or, even better, I submitted a pull request to the documentation repository.

Copy link
Member

@subhramit subhramit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was not easy - really beautiful work, Wanling. For a WIP, this is exceptionally good!
I am leaving some comments - the earlier we get habituated to some idiomatic practices (some JabRef-specific, some general), the easier it will be to stick to them during the rest of the project.

Please don't be overwhelmed by the number of comments - most of them are minor. Your code is already good, so the only way I could help make it better was by nit-picking.

Comment on lines +32 to +34
private static final Logger LOGGER = LoggerFactory.getLogger(GitSyncService.class);
private final ImportFormatPreferences importFormatPreferences;
private GitHandler gitHandler;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I usually separate the logger from the other variables with a newline

Suggested change
private static final Logger LOGGER = LoggerFactory.getLogger(GitSyncService.class);
private final ImportFormatPreferences importFormatPreferences;
private GitHandler gitHandler;
private static final Logger LOGGER = LoggerFactory.getLogger(GitSyncService.class);
private final ImportFormatPreferences importFormatPreferences;
private GitHandler gitHandler;


// 4. Automatic merge
if (result.successful()) {
gitHandler.createCommitOnCurrentBranch("Auto-merged by JabRef", false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declare a constant called AMEND = true in the class and use !AMEND here to make the code more self-documenting.

/**
* Called when user clicks Pull
*/
public MergeResult pullAndMerge(Path bibFilePath) throws Exception {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid throwing generic exceptions. Since it might be tedious work to replace them, I will leave suggestions based on how the final exception list should look like, provided you have replaced them for the internal callee methods as well.

This is just to help - do not commit suggestion directly, as you will need some new imports.

Suggested change
public MergeResult pullAndMerge(Path bibFilePath) throws Exception {
public MergeResult pullAndMerge(Path bibFilePath) throws GitAPIException, IOException, JabRefException {

RevCommit baseCommit,
RevCommit localCommit,
RevCommit remoteCommit,
Path bibFilePath) throws Exception {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Path bibFilePath) throws Exception {
Path bibFilePath) throws IOException, JabRefException {

MergeResult result = performSemanticMerge(git, triple.base(), triple.local(), triple.remote(), bibFilePath);

// 4. Automatic merge
if (result.successful()) {
Copy link
Member

@subhramit subhramit Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: about how code "sounds" when reading.

Hypothetically, if result was of type ResultType, ResultType being an enum for success states, we could do something like if(result == ResultType.SUCCESS) - which sounds "natural".
Since here, result is a MergeResult record instance, and we are using a(n implicit) method to evaluate, can you change the name of the record parameter to isSuccessful so that the implicit method name also changes? if(result.isSuccessful()) will read better.

In general, method names should be imperatives for actions, and predicates for boolean queries.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I hadn't thought about how much smoother that reads. Naming really is an art 😄

List<BibEntry> entries = context.getEntries();
assertEquals(1, entries.size());

BibEntry entry = entries.get(0);
Copy link
Member

@subhramit subhramit Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
BibEntry entry = entries.get(0);
BibEntry entry = entries.getFirst();

Comment on lines +48 to +49
assertEquals(inputEntries.get(0).getCitationKey(), outputEntries.get(0).getCitationKey());
assertEquals(inputEntries.get(0).getField(StandardField.AUTHOR), outputEntries.get(0).getField(StandardField.AUTHOR));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assertEquals(inputEntries.get(0).getCitationKey(), outputEntries.get(0).getCitationKey());
assertEquals(inputEntries.get(0).getField(StandardField.AUTHOR), outputEntries.get(0).getField(StandardField.AUTHOR));
assertEquals(inputEntries.getFirst().getCitationKey(), outputEntries.getFirst().getCitationKey());
assertEquals(inputEntries.getFirst().getField(StandardField.AUTHOR), outputEntries.getFirst().getField(StandardField.AUTHOR));

Comment on lines +474 to +475
BibDatabaseContext baseCtx = parse(baseCommit);
BibDatabaseContext remoteCtx = parse(remoteCommit);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

baseDatabaseContext, remoteDatabaseContext

Comment on lines +32 to +39
BibDatabaseContext baseCtx = GitBibParser.parseBibFromGit(base, importFormatPreferences);
BibDatabaseContext localCtx = GitBibParser.parseBibFromGit(local, importFormatPreferences);
BibDatabaseContext remoteCtx = GitBibParser.parseBibFromGit(remote, importFormatPreferences);

MergePlan plan = SemanticConflictDetector.extractMergePlan(baseCtx, remoteCtx);
SemanticMerger.applyMergePlan(localCtx, plan);

BibEntry patched = localCtx.getDatabase().getEntryByCitationKey("a").orElseThrow();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All Ctx -> DatabaseContext

Comment on lines +60 to +62
BibDatabaseContext baseCtx = parse(baseCommit);
BibDatabaseContext localCtx = parse(localCommit);
BibDatabaseContext remoteCtx = parse(remoteCommit);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all Ctx -> DatabaseContext

Copy link
Member

@koppor koppor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small comment on the process

Git git = Git.open(bibFilePath.getParent().toFile());

// 1. fetch latest remote branch
gitHandler.pullOnCurrentBranch();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git.fetch().call();

(because pull is fetch+merge, but JabRef should do the merge (see below)

public class GitSyncService {
private static final Logger LOGGER = LoggerFactory.getLogger(GitSyncService.class);
private final ImportFormatPreferences importFormatPreferences;
private GitHandler gitHandler;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git handler could be changed? if not, then it's better to add final

// 4. Automatic merge
if (result.successful()) {
gitHandler.createCommitOnCurrentBranch("Auto-merged by JabRef", false);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performSemanticMerge is a function that mutates some state? What if the result of semantic merge would be unsuccessful, will there be like part of the work finished and part of the work left untouched?

Oh, as we only handle non-conflicting cases, for now, we can skip this moment

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mhm, I see. If there is a conflict, then it would not change anything, but if there are no conflicts, then the result will be successful

if (bibPath.startsWith(workTree)) {
relativePath = workTree.relativize(bibPath);
} else {
throw new IllegalStateException("Given .bib file is not inside repository");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can leave the exception for now, but in future we will check beforehand if a bib file is inside a repository, right?


// WIP
public void push(Path bibFilePath) throws Exception {
this.gitHandler = new GitHandler(bibFilePath.getParent());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One interesting moment to check for: will this command find a git repository in upper-level directories?

If I use git command, while I'm in some org/jabref/logic/... folders, I still can use git commit. I just don't know whether this is supported on some standard-level, or this is a feature if git CLI application


/**
* Implementation-only merge logic: applies changes from remote (relative to base) to local.
* does not check for "modifications" or "conflicts" — all decisions should be handled in advance by the SemanticConflictDetector
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small moment:

Suggested change
* does not check for "modifications" or "conflicts"all decisions should be handled in advance by the SemanticConflictDetector
* does not check for "modifications" or "conflicts"all decisions should be handled in advance by the {@link SemanticConflictDetector}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add (simple) git support
4 participants