-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
feat(git): add “Share to GitHub” flow #13677
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
Changes from 22 commits
2c1a9b5
755b84b
9f4f675
66d2e49
79720ee
f090c38
e880044
667d317
a8cbdb9
8864b63
1f0b3f6
cc01903
0e3c9b1
16bc319
609aae3
cc55735
d9cda75
746eb72
2d05b88
afa356c
8d2f3bc
9f7e8f5
809cbe1
0316c2b
1ec62d9
9b16562
f3bf354
1e0a344
984d9c4
dae0377
cf5d724
59adde0
5848b1a
8a137ac
12b5d74
4472d81
46cf427
a4d667f
87353e1
3f8a864
5a2ce1e
1af29d3
c49a1cf
01684be
3602e72
160c398
9ec8879
752b7fe
614a0ff
1f89a5a
d7a2ff6
5ec253e
ed1ae89
095f076
2df3788
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 { | ||
InAnYan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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)); | ||
InAnYan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| 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; | ||
| }) | ||
InAnYan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| .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)) | ||
InAnYan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| .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 | ||
InAnYan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
InAnYan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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; | ||
|
||
| 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.")); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
InAnYan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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()) { | ||
InAnYan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return false; | ||
| } | ||
| return input.trim().matches("^https://.+"); | ||
| }; | ||
| } | ||
| } | ||

Uh oh!
There was an error while loading. Please reload this page.