Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
2c1a9b5
feat(git): add “Share to GitHub” flow
wanling0000 Aug 10, 2025
755b84b
add module-info
wanling0000 Aug 10, 2025
9f4f675
Resolve conflicts
wanling0000 Aug 10, 2025
66d2e49
fix(git): add missing imports
wanling0000 Aug 10, 2025
79720ee
test: init repo in SlrGitHandlerTest; fix localization keys
wanling0000 Aug 10, 2025
f090c38
fix: fail fast on null/blank parameters
wanling0000 Aug 10, 2025
e880044
fix: Avoid silent failure in GitPreferences
wanling0000 Aug 10, 2025
667d317
refactor: avoid null init
wanling0000 Aug 10, 2025
a8cbdb9
Merge `upstream/main` into `feat/gsoc-git-share-to-github`
wanling0000 Aug 11, 2025
8864b63
add GitHandler class
wanling0000 Aug 11, 2025
1f0b3f6
fix: keep RuntimeException catch but using log WARN
wanling0000 Aug 11, 2025
cc01903
Test Stacked branch
wanling0000 Aug 11, 2025
0e3c9b1
fix: checkstyle error
wanling0000 Aug 11, 2025
16bc319
Merge `upstream/main` into `feat/gsoc-git-share-to-github`
wanling0000 Aug 11, 2025
609aae3
fix: add missing imports
wanling0000 Aug 11, 2025
cc55735
Use BackgroundTask and field Validator
wanling0000 Aug 15, 2025
d9cda75
Refactor GitPreferences align with ProxyPreferences
wanling0000 Aug 15, 2025
746eb72
Add https-only validator
wanling0000 Aug 15, 2025
2d05b88
Add TODOs and usage of StringUtil.isBlank
koppor Aug 15, 2025
afa356c
WIP - save intermediate GitPreferences refactor
wanling0000 Aug 15, 2025
8d2f3bc
WIP - save intermediate GitPreferences refactor
wanling0000 Aug 15, 2025
9f7e8f5
WIP
koppor Aug 17, 2025
809cbe1
refactor: GitHub share dialog
wanling0000 Aug 18, 2025
0316c2b
Merge remote-tracking branch 'upstream/main' into clean/gsoc-git-shar…
wanling0000 Aug 18, 2025
1ec62d9
Compile fix
koppor Aug 18, 2025
9b16562
Merge branch 'feat/gsoc-git-share-to-github' of github.com:wanling000…
koppor Aug 18, 2025
f3bf354
Convert GitPreferences to record (as suggested by IntelliJ)
koppor Aug 18, 2025
1e0a344
Fix git PAT storage
wanling0000 Aug 18, 2025
984d9c4
Merge branch 'feat/gsoc-git-share-to-github' of github.com:wanling000…
wanling0000 Aug 18, 2025
dae0377
Add TODO note
koppor Aug 18, 2025
cf5d724
Merge branch 'feat/gsoc-git-share-to-github' of github.com:wanling000…
koppor Aug 18, 2025
59adde0
Changes by subhr
koppor Aug 18, 2025
5848b1a
Changes by fwl
koppor Aug 18, 2025
8a137ac
Revert "Convert GitPreferences to record (as suggested by IntelliJ)"
koppor Aug 18, 2025
12b5d74
Compile fix
koppor Aug 18, 2025
4472d81
Disable non-working test
koppor Aug 18, 2025
46cf427
Add ing to ADR-0016
koppor Aug 18, 2025
a4d667f
Rewrite Optional to ""
koppor Aug 18, 2025
87353e1
Fix markdown lint
koppor Aug 18, 2025
3f8a864
Link to ADR-0016
koppor Aug 18, 2025
5a2ce1e
Fix casing
koppor Aug 18, 2025
1af29d3
Fix architecture
koppor Aug 18, 2025
c49a1cf
Fix typo
koppor Aug 18, 2025
01684be
Refine comments
koppor Aug 18, 2025
3602e72
Remove TODO
koppor Aug 18, 2025
160c398
More language cleanup
koppor Aug 18, 2025
9ec8879
Fix empty line
koppor Aug 18, 2025
752b7fe
Cleanup language again
koppor Aug 18, 2025
614a0ff
Remove TODO
koppor Aug 18, 2025
1f89a5a
Fix localization
koppor Aug 18, 2025
d7a2ff6
Remove separator
koppor Aug 18, 2025
5ec253e
Git is first
koppor Aug 18, 2025
ed1ae89
Fix conversion
koppor Aug 18, 2025
095f076
Use "notify" instead of a popup
koppor Aug 18, 2025
2df3788
Remove obsolete string
koppor Aug 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,14 @@ public enum StandardActions implements Action {
GROUP_SUBGROUP_RENAME(Localization.lang("Rename subgroup"), KeyBinding.GROUP_SUBGROUP_RENAME),
GROUP_ENTRIES_REMOVE(Localization.lang("Remove selected entries from this group")),

CLEAR_EMBEDDINGS_CACHE(Localization.lang("Clear embeddings cache"));
CLEAR_EMBEDDINGS_CACHE(Localization.lang("Clear embeddings cache")),

GIT(Localization.lang("Git"), IconTheme.JabRefIcons.GIT_SYNC),
GIT_PULL(Localization.lang("Pull")),
GIT_PUSH(Localization.lang("Push")),
GIT_COMMIT(Localization.lang("Commit")),
GIT_SHARE(Localization.lang("Share this library to GitHub"));


private String text;
private final String description;
Expand Down
7 changes: 7 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.jabref.gui.externalfiles.AutoLinkFilesAction;
import org.jabref.gui.externalfiles.DownloadFullTextAction;
import org.jabref.gui.externalfiles.FindUnlinkedFilesAction;
import org.jabref.gui.git.GitShareToGitHubAction;
import org.jabref.gui.help.AboutAction;
import org.jabref.gui.help.ErrorConsoleAction;
import org.jabref.gui.help.HelpAction;
Expand Down Expand Up @@ -186,6 +187,12 @@ private void createMenu() {

new SeparatorMenuItem(),

factory.createSubMenu(StandardActions.GIT,
factory.createMenuItem(StandardActions.GIT_SHARE, new GitShareToGitHubAction(dialogService, stateManager, preferences, taskExecutor))
),

new SeparatorMenuItem(),

factory.createMenuItem(StandardActions.SHOW_PREFS, new ShowPreferencesAction(frame, dialogService)),

new SeparatorMenuItem(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.jabref.gui.git;

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.util.TaskExecutor;

public class GitShareToGitHubAction extends SimpleCommand {
private final DialogService dialogService;
private final StateManager stateManager;
private final GuiPreferences preferences;
private final TaskExecutor taskExecutor;

public GitShareToGitHubAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences, TaskExecutor taskExecutor) {
this.dialogService = dialogService;
this.stateManager = stateManager;
this.preferences = preferences;
this.taskExecutor = taskExecutor;
}

@Override
public void execute() {
dialogService.showCustomDialogAndWait(new GitShareToGitHubDialogView(stateManager, dialogService, taskExecutor, preferences));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package org.jabref.gui.git;

import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;

import org.jabref.gui.DialogService;
import org.jabref.gui.StateManager;
import org.jabref.gui.desktop.os.NativeDesktop;
import org.jabref.gui.frame.ExternalApplicationsPreferences;
import org.jabref.gui.util.BaseDialog;
import org.jabref.gui.util.ControlHelper;
import org.jabref.gui.util.IconValidationDecorator;
import org.jabref.logic.git.prefs.GitPreferences;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.TaskExecutor;

import com.airhacks.afterburner.views.ViewLoader;
import de.saxsys.mvvmfx.utils.validation.visualization.ControlsFxVisualizer;

public class GitShareToGitHubDialogView extends BaseDialog<Void> {
private static final String GITHUB_PAT_DOCS_URL =
"https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens";

private static final String GITHUB_NEW_REPO_URL = "https://github.com/new";

@FXML private TextField repositoryUrl;
@FXML private TextField username;
@FXML private PasswordField personalAccessToken;
@FXML private ButtonType shareButton;
@FXML private Label patHelpIcon;
@FXML private Tooltip patHelpTooltip;
@FXML private CheckBox rememberSettingsCheck;
@FXML private Label repoHelpIcon;
@FXML private Tooltip repoHelpTooltip;

private final GitShareToGitHubDialogViewModel viewModel;

private final StateManager stateManager;
private final DialogService dialogService;
private final TaskExecutor taskExecutor;
private final ExternalApplicationsPreferences externalApplicationsPreferences;

private final ControlsFxVisualizer visualizer = new ControlsFxVisualizer();

public GitShareToGitHubDialogView(StateManager stateManager, DialogService dialogService, TaskExecutor taskExecutor, ExternalApplicationsPreferences externalApplicationsPreferences, GitPreferences gitPreferences) {
this.stateManager = stateManager;
this.dialogService = dialogService;
this.taskExecutor = taskExecutor;
this.externalApplicationsPreferences = externalApplicationsPreferences;

this.setTitle(Localization.lang("Share this library to GitHub"));
this.viewModel = new GitShareToGitHubDialogViewModel(gitPreferences, stateManager, dialogService);

ViewLoader.view(this)
.load()
.setAsDialogPane(this);
ControlHelper.setAction(shareButton, this.getDialogPane(), _ -> shareToGitHub());
}

@FXML
private void initialize() {
patHelpTooltip.setText(
Localization.lang("Click to open GitHub Personal Access Token documentation")
);

username.setPromptText(Localization.lang("Your GitHub username"));
personalAccessToken.setPromptText(Localization.lang("PAT with repo access"));

repoHelpTooltip.setText(
Localization.lang("Create an empty repository on GitHub, then copy the HTTPS URL (ends with .git). Click to open GitHub.")
);
Tooltip.install(repoHelpIcon, repoHelpTooltip);
repoHelpIcon.setOnMouseClicked(e ->
NativeDesktop.openBrowserShowPopup(
GITHUB_NEW_REPO_URL,
dialogService,
externalApplicationsPreferences
)
);

Tooltip.install(patHelpIcon, patHelpTooltip);
patHelpIcon.setOnMouseClicked(e ->
NativeDesktop.openBrowserShowPopup(
GITHUB_PAT_DOCS_URL,
dialogService,
externalApplicationsPreferences
)
);

repositoryUrl.textProperty().bindBidirectional(viewModel.repositoryUrlProperty());
username.textProperty().bindBidirectional(viewModel.githubUsernameProperty());
personalAccessToken.textProperty().bindBidirectional(viewModel.githubPatProperty());
rememberSettingsCheck.selectedProperty().bindBidirectional(viewModel.rememberSettingsProperty());

Platform.runLater(() -> {
visualizer.setDecoration(new IconValidationDecorator());

visualizer.initVisualization(viewModel.repositoryUrlValidation(), repositoryUrl, true);
visualizer.initVisualization(viewModel.githubUsernameValidation(), username, true);
visualizer.initVisualization(viewModel.githubPatValidation(), personalAccessToken, true);
});
}

@FXML
private void shareToGitHub() {
BackgroundTask.wrap(() -> {
viewModel.shareToGitHub();
return true;
})
.onSuccess(result -> {
dialogService.showInformationDialogAndWait(
Localization.lang("GitHub Share"),
Localization.lang("Successfully pushed to GitHub.")
);
this.close();
})
.onFailure(e -> dialogService.showErrorDialogAndWait("GitHub share failed", e.getMessage(), e))
.executeWith(taskExecutor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package org.jabref.gui.git;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Predicate;

import javafx.beans.property.StringProperty;

import org.jabref.gui.AbstractViewModel;
import org.jabref.gui.DialogService;
import org.jabref.gui.StateManager;
import org.jabref.logic.JabRefException;
import org.jabref.logic.git.GitHandler;
import org.jabref.logic.git.prefs.GitPreferences;
import org.jabref.logic.git.status.GitStatusChecker;
import org.jabref.logic.git.status.GitStatusSnapshot;
import org.jabref.logic.git.status.SyncStatus;
import org.jabref.logic.git.util.GitHandlerRegistry;
import org.jabref.logic.git.util.GitInitService;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.database.BibDatabaseContext;

import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator;
import de.saxsys.mvvmfx.utils.validation.ValidationMessage;
import de.saxsys.mvvmfx.utils.validation.ValidationStatus;
import de.saxsys.mvvmfx.utils.validation.Validator;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/// This dialog makes the connection to GitHub configurable
/// We do not go through the JabRef preferences dialog, because need the prefernce close to the user
public class GitShareToGitHubDialogViewModel extends AbstractViewModel {
private static final Logger LOGGER = LoggerFactory.getLogger(GitShareToGitHubDialogViewModel.class);

private final StateManager stateManager;
private final DialogService dialogService;

// The preferences stored in JabRef
private final GitPreferences gitPreferences;

// The preferences of this dialog
private final StringProperty username;
Copy link
Member

Choose a reason for hiding this comment

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

Preferences for a dialog are stored in the same preferences way, just take a look at the OpenOfficePreferences, they are not visible in the global preferences dialog, just visible when clicking on the dialog

grafik

private final StringProperty pat;
// TODO: This should be a library preference -> the library is connected to repository; not all JabRef libraries to the same one
// Reason: One could have https://github.com/JabRef/JabRef-exmple-libraries as one repo and https://github.com/myexampleuser/demolibs as onther repository
// Both share the same secrets, but are different URLs.
// Also think of having two .bib files in the same folder - they will have the same repository URL -- should make no issues, but let's see...

private final StringProperty repositoryUrl;

private final Validator repositoryUrlValidator;
private final Validator githubUsernameValidator;
private final Validator githubPatValidator;

public GitShareToGitHubDialogViewModel(GitPreferences gitPreferences, StateManager stateManager, DialogService dialogService) {
this.stateManager = stateManager;
this.dialogService = dialogService;
this.gitPreferences = gitPreferences;

// Copy the existing preferences and make them available for modification
localGitPrefernces = GitPrefernces.of(preferences);
repositoryUrlValidator = new FunctionBasedValidator<>(gitPreferences.repositoryUrlProperty(), githubHttpsUrlValidator(), ValidationMessage.error(Localization.lang("Repository URL is required")));
githubUsernameValidator = new FunctionBasedValidator<>(gitPreferences.usernameProperty(), notEmptyValidator(), ValidationMessage.error(Localization.lang("GitHub username is required")));
githubPatValidator = new FunctionBasedValidator<>(gitPreferences.patProperty(), notEmptyValidator(), ValidationMessage.error(Localization.lang("Personal Access Token is required")));

applyGitPreferences();
}

public void shareToGitHub() throws JabRefException, IOException, GitAPIException {
String url = trimOrEmpty(repositoryUrl.get());
String user = trimOrEmpty(githubUsername.get());
String pat = trimOrEmpty(githubPat.get());

Optional<BibDatabaseContext> activeDatabaseOpt = stateManager.getActiveDatabase();
if (activeDatabaseOpt.isEmpty()) {
throw new JabRefException(Localization.lang("No library open"));
}

BibDatabaseContext activeDatabase = activeDatabaseOpt.get();
Optional<Path> bibFilePathOpt = activeDatabase.getDatabasePath();
if (bibFilePathOpt.isEmpty()) {
throw new JabRefException(Localization.lang("No library file path. Please save the library to a file first."));
Copy link
Member

Choose a reason for hiding this comment

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

@calixtus Would it be a better approach if we show notification instead of exception?

Copy link
Member

Choose a reason for hiding this comment

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

Currently I'm traveling, will be available again in Tuesday

}

Path bibPath = bibFilePathOpt.get();

GitInitService.initRepoAndSetRemote(bibPath, url);

GitHandlerRegistry registry = new GitHandlerRegistry();
GitHandler handler = registry.get(bibPath.getParent());

handler.setCredentials(user, pat);

GitStatusSnapshot status = GitStatusChecker.checkStatusAndFetch(handler);

if (status.syncStatus() == SyncStatus.BEHIND) {
throw new JabRefException(Localization.lang("Remote repository is not empty. Please pull changes before pushing."));
}

handler.createCommitOnCurrentBranch(Localization.lang("Share library to GitHub"), false);

if (status.syncStatus() == SyncStatus.REMOTE_EMPTY) {
handler.pushCurrentBranchCreatingUpstream();
} else {
handler.pushCommitsToRemoteRepository();
}

setGitPreferences(url, user, pat);
}

private void setGitPreferences(String url, String user, String pat) {
gitPreferences.setUsername(user);
gitPreferences.setRepositoryUrl(url);
gitPreferences.setRememberPat(rememberSettings.get());
gitPreferences.setPersonalAccessToken(pat);
}

private static String trimOrEmpty(String s) {
return s == null ? "" : s.trim();
}

public ValidationStatus repositoryUrlValidation() {
return repositoryUrlValidator.getValidationStatus();
}

public ValidationStatus githubUsernameValidation() {
return githubUsernameValidator.getValidationStatus();
}

public ValidationStatus githubPatValidation() {
return githubPatValidator.getValidationStatus();
}

private Predicate<String> notEmptyValidator() {
return input -> input != null && !input.trim().isEmpty();
}

private Predicate<String> githubHttpsUrlValidator() {
return input -> {
if (input == null || input.trim().isEmpty()) {
return false;
}
return input.trim().matches("^https://.+");
};
}
}
1 change: 1 addition & 0 deletions jabgui/src/main/java/org/jabref/gui/icon/IconTheme.java
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ public enum JabRefIcons implements JabRefIcon {
CONSISTENCY_OPTIONAL_FIELD(MaterialDesignC.CIRCLE_OUTLINE),
CONSISTENCY_UNKNOWN_FIELD(MaterialDesignH.HELP),
ABSOLUTE_PATH(MaterialDesignF.FAMILY_TREE),
GIT_SYNC(MaterialDesignG.GIT),
RELATIVE_PATH(MaterialDesignF.FILE_TREE_OUTLINE),
SHORTEN_DOI(MaterialDesignA.ARROW_COLLAPSE_HORIZONTAL);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.jabref.gui.actions.ActionHelper;
import org.jabref.gui.actions.SimpleCommand;
import org.jabref.gui.importer.actions.OpenDatabaseAction;
import org.jabref.logic.JabRefException;
import org.jabref.logic.crawler.Crawler;
import org.jabref.logic.git.SlrGitHandler;
import org.jabref.logic.importer.ParseException;
Expand Down Expand Up @@ -116,7 +117,7 @@ protected void crawl() {
preferences,
new BibEntryTypesManager(),
fileUpdateMonitor);
} catch (IOException | ParseException e) {
} catch (IOException | ParseException | JabRefException e) {
LOGGER.error("Error during reading of study definition file.", e);
dialogService.showErrorDialogAndWait(Localization.lang("Error during reading of study definition file."), e);
return;
Expand Down
Loading