Skip to content

Commit 1683f1c

Browse files
committed
feat(git): add “Share to GitHub” flow
1 parent 93bf9b6 commit 1683f1c

File tree

14 files changed

+798
-47
lines changed

14 files changed

+798
-47
lines changed

jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,14 @@ public enum StandardActions implements Action {
217217
GROUP_SUBGROUP_RENAME(Localization.lang("Rename subgroup"), KeyBinding.GROUP_SUBGROUP_RENAME),
218218
GROUP_ENTRIES_REMOVE(Localization.lang("Remove selected entries from this group")),
219219

220-
CLEAR_EMBEDDINGS_CACHE(Localization.lang("Clear embeddings cache"));
220+
CLEAR_EMBEDDINGS_CACHE(Localization.lang("Clear embeddings cache")),
221+
222+
GIT(Localization.lang("Git"), IconTheme.JabRefIcons.GIT_SYNC),
223+
GIT_PULL(Localization.lang("Pull")),
224+
GIT_PUSH(Localization.lang("Push")),
225+
GIT_COMMIT(Localization.lang("Commit")),
226+
GIT_SHARE(Localization.lang("Share this library to GitHub"));
227+
221228

222229
private String text;
223230
private final String description;

jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.jabref.gui.externalfiles.AutoLinkFilesAction;
4040
import org.jabref.gui.externalfiles.DownloadFullTextAction;
4141
import org.jabref.gui.externalfiles.FindUnlinkedFilesAction;
42+
import org.jabref.gui.git.GitShareToGitHubAction;
4243
import org.jabref.gui.help.AboutAction;
4344
import org.jabref.gui.help.ErrorConsoleAction;
4445
import org.jabref.gui.help.HelpAction;
@@ -186,6 +187,12 @@ private void createMenu() {
186187

187188
new SeparatorMenuItem(),
188189

190+
factory.createSubMenu(StandardActions.GIT,
191+
factory.createMenuItem(StandardActions.GIT_SHARE, new GitShareToGitHubAction(dialogService, stateManager, preferences))
192+
),
193+
194+
new SeparatorMenuItem(),
195+
189196
factory.createMenuItem(StandardActions.SHOW_PREFS, new ShowPreferencesAction(frame, dialogService)),
190197

191198
new SeparatorMenuItem(),
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.jabref.gui.git;
2+
3+
import org.jabref.gui.DialogService;
4+
import org.jabref.gui.StateManager;
5+
import org.jabref.gui.actions.SimpleCommand;
6+
import org.jabref.gui.preferences.GuiPreferences;
7+
8+
public class GitShareToGitHubAction extends SimpleCommand {
9+
private final DialogService dialogService;
10+
private final StateManager stateManager;
11+
private final GuiPreferences preferences;
12+
13+
public GitShareToGitHubAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences) {
14+
this.dialogService = dialogService;
15+
this.stateManager = stateManager;
16+
this.preferences = preferences;
17+
}
18+
19+
@Override
20+
public void execute() {
21+
dialogService.showCustomDialogAndWait(new GitShareToGitHubDialogView(stateManager, dialogService, preferences));
22+
}
23+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package org.jabref.gui.git;
2+
3+
import javafx.fxml.FXML;
4+
import javafx.scene.control.ButtonType;
5+
import javafx.scene.control.CheckBox;
6+
import javafx.scene.control.Label;
7+
import javafx.scene.control.PasswordField;
8+
import javafx.scene.control.TextField;
9+
import javafx.scene.control.Tooltip;
10+
11+
import org.jabref.gui.DialogService;
12+
import org.jabref.gui.StateManager;
13+
import org.jabref.gui.desktop.os.NativeDesktop;
14+
import org.jabref.gui.preferences.GuiPreferences;
15+
import org.jabref.gui.util.BaseDialog;
16+
import org.jabref.gui.util.ControlHelper;
17+
import org.jabref.logic.l10n.Localization;
18+
19+
import com.airhacks.afterburner.views.ViewLoader;
20+
21+
public class GitShareToGitHubDialogView extends BaseDialog<Void> {
22+
private static final String GITHUB_PAT_DOCS_URL =
23+
"https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens";
24+
25+
private static final String GITHUB_NEW_REPO_URL = "https://github.com/new";
26+
27+
@FXML private TextField repositoryUrl;
28+
@FXML private TextField username;
29+
@FXML private PasswordField personalAccessToken;
30+
@FXML private ButtonType shareButton;
31+
@FXML private Label patHelpIcon;
32+
@FXML private Tooltip patHelpTooltip;
33+
@FXML private CheckBox rememberSettingsCheck;
34+
@FXML private Label repoHelpIcon;
35+
@FXML private Tooltip repoHelpTooltip;
36+
37+
private final GitShareToGitHubDialogViewModel viewModel;
38+
private final DialogService dialogService;
39+
private final StateManager stateManager;
40+
private final GuiPreferences preferences;
41+
42+
public GitShareToGitHubDialogView(StateManager stateManager, DialogService dialogService, GuiPreferences preferences) {
43+
this.stateManager = stateManager;
44+
this.dialogService = dialogService;
45+
this.preferences = preferences;
46+
47+
this.setTitle(Localization.lang("Share this library to GitHub"));
48+
this.viewModel = new GitShareToGitHubDialogViewModel(stateManager, dialogService);
49+
50+
ViewLoader.view(this)
51+
.load()
52+
.setAsDialogPane(this);
53+
ControlHelper.setAction(shareButton, this.getDialogPane(), event -> shareToGitHub());
54+
}
55+
56+
@FXML
57+
private void initialize() {
58+
patHelpTooltip.setText(
59+
Localization.lang("Need help?") + "\n" +
60+
Localization.lang("Click to open GitHub Personal Access Token documentation")
61+
);
62+
63+
username.setPromptText(Localization.lang("Your GitHub username"));
64+
personalAccessToken.setPromptText(Localization.lang("PAT with repo access"));
65+
66+
repoHelpTooltip.setText(
67+
Localization.lang("Create an empty repository on GitHub, then copy the HTTPS URL (ends with .git). Click to open GitHub.")
68+
);
69+
Tooltip.install(repoHelpIcon, repoHelpTooltip);
70+
repoHelpIcon.setOnMouseClicked(e ->
71+
NativeDesktop.openBrowserShowPopup(
72+
GITHUB_NEW_REPO_URL,
73+
dialogService,
74+
this.preferences.getExternalApplicationsPreferences()
75+
)
76+
);
77+
78+
Tooltip.install(patHelpIcon, patHelpTooltip);
79+
patHelpIcon.setOnMouseClicked(e ->
80+
NativeDesktop.openBrowserShowPopup(
81+
GITHUB_PAT_DOCS_URL,
82+
dialogService,
83+
this.preferences.getExternalApplicationsPreferences()
84+
)
85+
);
86+
87+
repositoryUrl.textProperty().bindBidirectional(viewModel.repositoryUrlProperty());
88+
username.textProperty().bindBidirectional(viewModel.githubUsernameProperty());
89+
personalAccessToken.textProperty().bindBidirectional(viewModel.githubPatProperty());
90+
rememberSettingsCheck.selectedProperty().bindBidirectional(viewModel.rememberSettingsProperty());
91+
}
92+
93+
@FXML
94+
private void shareToGitHub() {
95+
boolean success = viewModel.shareToGitHub();
96+
if (success) {
97+
this.close();
98+
}
99+
}
100+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package org.jabref.gui.git;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Path;
5+
import java.util.Optional;
6+
7+
import javafx.beans.property.BooleanProperty;
8+
import javafx.beans.property.SimpleBooleanProperty;
9+
import javafx.beans.property.SimpleStringProperty;
10+
import javafx.beans.property.StringProperty;
11+
12+
import org.jabref.gui.AbstractViewModel;
13+
import org.jabref.gui.DialogService;
14+
import org.jabref.gui.StateManager;
15+
import org.jabref.logic.JabRefException;
16+
import org.jabref.logic.git.GitHandler;
17+
import org.jabref.logic.git.prefs.GitPreferences;
18+
import org.jabref.logic.git.status.GitStatusChecker;
19+
import org.jabref.logic.git.status.GitStatusSnapshot;
20+
import org.jabref.logic.git.status.SyncStatus;
21+
import org.jabref.logic.git.util.GitHandlerRegistry;
22+
import org.jabref.logic.git.util.GitInitService;
23+
import org.jabref.logic.l10n.Localization;
24+
import org.jabref.model.database.BibDatabaseContext;
25+
26+
import org.eclipse.jgit.api.errors.GitAPIException;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
30+
public class GitShareToGitHubDialogViewModel extends AbstractViewModel {
31+
private static final Logger LOGGER = LoggerFactory.getLogger(GitShareToGitHubDialogViewModel.class);
32+
33+
private final StateManager stateManager;
34+
private final DialogService dialogService;
35+
private final GitPreferences gitPreferences = new GitPreferences();
36+
37+
private final StringProperty githubUsername = new SimpleStringProperty();
38+
private final StringProperty githubPat = new SimpleStringProperty();
39+
private final StringProperty repositoryUrl = new SimpleStringProperty();
40+
private final BooleanProperty rememberSettings = new SimpleBooleanProperty();
41+
42+
public GitShareToGitHubDialogViewModel(StateManager stateManager, DialogService dialogService) {
43+
this.stateManager = stateManager;
44+
this.dialogService = dialogService;
45+
46+
applyGitPreferences();
47+
}
48+
49+
public boolean shareToGitHub() {
50+
String url = trimOrEmpty(repositoryUrl.get());
51+
String user = trimOrEmpty(githubUsername.get());
52+
String pat = trimOrEmpty(githubPat.get());
53+
54+
if (url.isBlank()) {
55+
dialogService.showErrorDialogAndWait(Localization.lang("GitHub repository URL is required"));
56+
return false;
57+
}
58+
59+
if (pat.isBlank()) {
60+
dialogService.showErrorDialogAndWait(Localization.lang("Personal Access Token is required to push"));
61+
return false;
62+
}
63+
if (user.isBlank()) {
64+
dialogService.showErrorDialogAndWait(Localization.lang("GitHub username is required"));
65+
return false;
66+
}
67+
Optional<BibDatabaseContext> activeDatabaseOpt = stateManager.getActiveDatabase();
68+
if (activeDatabaseOpt.isEmpty()) {
69+
dialogService.showErrorDialogAndWait(
70+
Localization.lang("No library open")
71+
);
72+
return false;
73+
}
74+
75+
BibDatabaseContext activeDatabase = activeDatabaseOpt.get();
76+
Optional<Path> bibFilePathOpt = activeDatabase.getDatabasePath();
77+
if (bibFilePathOpt.isEmpty()) {
78+
dialogService.showErrorDialogAndWait(
79+
Localization.lang("No library file path"),
80+
Localization.lang("Cannot share: Please save the library to a file first.")
81+
);
82+
return false;
83+
}
84+
85+
Path bibPath = bibFilePathOpt.get();
86+
87+
try {
88+
GitInitService.initRepoAndSetRemote(bibPath, url);
89+
90+
GitHandlerRegistry registry = new GitHandlerRegistry();
91+
GitHandler handler = registry.get(bibPath.getParent());
92+
93+
boolean hasStoredPat = gitPreferences.getPersonalAccessToken().isPresent();
94+
if (!rememberSettingsProperty().get() || !hasStoredPat) {
95+
handler.setCredentials(user, pat);
96+
}
97+
GitStatusSnapshot status = GitStatusChecker.checkStatusAndFetch(handler);
98+
99+
if (status.syncStatus() == SyncStatus.BEHIND) {
100+
dialogService.showWarningDialogAndWait(
101+
Localization.lang("Remote repository is not empty"),
102+
Localization.lang("Please pull changes before pushing.")
103+
);
104+
return false;
105+
}
106+
107+
handler.createCommitOnCurrentBranch(Localization.lang("Share library to GitHub"), false);
108+
try {
109+
if (status.syncStatus() == SyncStatus.REMOTE_EMPTY) {
110+
handler.pushCurrentBranchCreatingUpstream();
111+
} else {
112+
handler.pushCommitsToRemoteRepository();
113+
}
114+
} catch (IOException | GitAPIException e) {
115+
LOGGER.error("Push failed", e);
116+
dialogService.showErrorDialogAndWait(Localization.lang("Git error"), e);
117+
return false;
118+
}
119+
120+
setGitPreferences(url, user, pat);
121+
122+
dialogService.showInformationDialogAndWait(
123+
Localization.lang("GitHub Share"),
124+
Localization.lang("Successfully pushed to %0", url)
125+
);
126+
return true;
127+
} catch (GitAPIException |
128+
IOException e) {
129+
LOGGER.error("Error sharing to GitHub", e);
130+
dialogService.showErrorDialogAndWait(Localization.lang("Git error"), e);
131+
return false;
132+
} catch (JabRefException e) {
133+
dialogService.showErrorDialogAndWait(Localization.lang("Git error"), e);
134+
return false;
135+
}
136+
}
137+
138+
private void applyGitPreferences() {
139+
gitPreferences.getUsername().ifPresent(githubUsername::set);
140+
gitPreferences.getPersonalAccessToken().ifPresent(token -> {
141+
githubPat.set(token);
142+
rememberSettings.set(true);
143+
});
144+
gitPreferences.getRepositoryUrl().ifPresent(repositoryUrl::set);
145+
rememberSettings.set(gitPreferences.getRememberPat() || rememberSettings.get());
146+
}
147+
148+
private void setGitPreferences(String url, String user, String pat) {
149+
gitPreferences.setUsername(user);
150+
gitPreferences.setRepositoryUrl(url);
151+
gitPreferences.setRememberPat(rememberSettings.get());
152+
153+
if (rememberSettings.get()) {
154+
gitPreferences.savePersonalAccessToken(pat, user);
155+
} else {
156+
gitPreferences.clearGitHubPersonalAccessToken();
157+
}
158+
}
159+
160+
private static String trimOrEmpty(String s) {
161+
return (s == null) ? "" : s.trim();
162+
}
163+
164+
public StringProperty githubUsernameProperty() {
165+
return githubUsername;
166+
}
167+
168+
public StringProperty githubPatProperty() {
169+
return githubPat;
170+
}
171+
172+
public BooleanProperty rememberSettingsProperty() {
173+
return rememberSettings;
174+
}
175+
176+
public StringProperty repositoryUrlProperty() {
177+
return repositoryUrl;
178+
}
179+
}

0 commit comments

Comments
 (0)