package bluej.groupwork.ui;

import bluej.Config;
import bluej.groupwork.actions.CommitAction;
import bluej.groupwork.actions.PushAction;
import bluej.groupwork.Repository;
import bluej.groupwork.StatusHandle;
import bluej.groupwork.StatusListener;
import bluej.groupwork.TeamStatusInfo;
import bluej.groupwork.TeamStatusInfo.Status;
import bluej.groupwork.TeamUtils;
import bluej.groupwork.TeamworkCommand;
import bluej.groupwork.TeamworkCommandResult;
import bluej.pkgmgr.BlueJPackageFile;
import bluej.pkgmgr.Project;
import bluej.utility.Debug;
import bluej.utility.DialogManager;
import bluej.utility.FXWorker;
import bluej.utility.javafx.FXCustomizedDialog;
import bluej.utility.javafx.JavaFXUtil;
import bluej.utility.javafx.NoMultipleSelectionModel;
import bluej.utility.Utility;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TextArea;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Window;

import threadchecker.OnThread;
import threadchecker.Tag;


| A user interface to commit and push. Used by DCVS systems, like Git. | | @author Fabio Heday | @author Amjad Altadmri | @OnThread(Tag.FXPlatform) public class CommitAndPushFrame extends FXCustomizedDialog<Void> implements CommitAndPushInterface{ private final Project project; private Repository repository; private Set<TeamStatusInfo> changedLayoutFiles = new HashSet<>(); private ObservableList<TeamStatusInfo> commitListModel = FXCollections.observableArrayList(); private ObservableList<TeamStatusInfo> pushListModel = FXCollections.observableArrayList(); private final CheckBox includeLayout = new CheckBox(Config.getString("team.commit.includelayout")); private final TextArea commitText = new TextArea(); private final ActivityIndicator progressBar = new ActivityIndicator(); private final ListView<TeamStatusInfo> pushFiles = new ListView<>(pushListModel); private CommitAction commitAction; private PushAction pushAction; private CommitAndPushWorker commitAndPushWorker; private boolean pushNeeded = false; private boolean updateNeeded = false; public CommitAndPushFrame(Project proj) { super(null, "team.commit.dcvs.title", "team-commit-push"); project = proj; repository = project.getTeamSettingsController().trytoEstablishRepository(false); getDialogPane().setContent(makeMainPane()); prepareButtonPane(); }
| Create the user-interface for the error display dialog. | private Pane makeMainPane() { ListView<TeamStatusInfo> commitFiles = new ListView<>(commitListModel); commitFiles.setPlaceholder(new Label(Config.getString("team.nocommitfiles"))); commitFiles.setCellFactory(param -> new TeamStatusInfoCell(project)); commitFiles.setSelectionModel(new NoMultipleSelectionModel<>()); commitFiles.disableProperty().bind(Bindings.isEmpty(commitListModel)); ScrollPane commitFileScrollPane = new ScrollPane(commitFiles); commitFileScrollPane.setFitToWidth(true); commitFileScrollPane.setFitToHeight(true); VBox.setMargin(commitFileScrollPane, new Insets(0, 0, 20, 0)); commitText.setPrefRowCount(20); commitText.setPrefColumnCount(35); commitText.setPromptText(Config.getString("team.commit.message")); VBox.setMargin(commitText, new Insets(0, 0, 10, 0)); commitAction = new CommitAction(this); Button commitButton = new Button(); commitAction.useButton(project, commitButton); commitButton.requestFocus(); commitText.disableProperty().bind(Bindings.isEmpty(commitListModel)); commitAction.disabledProperty().bind(Bindings.or(commitText.disabledProperty(), commitText.textProperty().isEmpty())); includeLayout.setOnAction(event -> { CheckBox layoutCheck = (CheckBox) event.getSource(); if (layoutCheck.isSelected()) { addModifiedLayouts(); } else { removeModifiedLayouts(); } }); includeLayout.setDisable(true); HBox commitButtonPane = new HBox(); JavaFXUtil.addStyleClass(commitButtonPane, "button-hbox"); commitButtonPane.setAlignment(Pos.CENTER_RIGHT); commitButtonPane.getChildren().addAll(includeLayout, commitButton); pushAction = new PushAction(this); Button pushButton = new Button(); pushAction.useButton(project, pushButton); Label pushFilesLabel = new Label(Config.getString("team.commitPush.push.files")); pushFiles.setCellFactory(param -> new TeamStatusInfoCell(project)); pushFiles.setSelectionModel(new NoMultipleSelectionModel<>()); pushFiles.disableProperty().bind(Bindings.isEmpty(pushListModel)); ScrollPane pushFileScrollPane = new ScrollPane(pushFiles); pushFileScrollPane.setFitToWidth(true); pushFileScrollPane.setFitToHeight(true); HBox pushButtonPane = new HBox(); progressBar.setRunning(false); JavaFXUtil.addStyleClass(pushButtonPane, "button-hbox"); pushButtonPane.setAlignment(Pos.CENTER_RIGHT); pushButtonPane.getChildren().addAll(progressBar, pushButton); VBox mainPane = new VBox(); JavaFXUtil.addStyleClass(mainPane, "main-pane"); mainPane.getChildren().addAll(new Label(Config.getString("team.commitPush.commit.files")), commitFileScrollPane, new Label(Config.getString("team.commit.comment")), commitText, commitButtonPane, new Separator(Orientation.HORIZONTAL), pushFilesLabel, pushFileScrollPane, pushButtonPane); return mainPane; }
| Prepare the button panel with a close button | private void prepareButtonPane() { getDialogPane().getButtonTypes().setAll(ButtonType.CLOSE); this.setOnCloseRequest(event -> { if (commitAndPushWorker != null) { commitAndPushWorker.abort(); } if (commitAction != null) { commitAction.cancel(); } close(); }); } @Override @OnThread(Tag.FXPlatform) public void setVisible(boolean show) { if (show) { commitText.setText(""); includeLayout.setSelected(false); includeLayout.setDisable(true); changedLayoutFiles.clear(); commitListModel.clear(); pushAction.setEnabled(false); pushListModel.clear(); repository = project.getTeamSettingsController().trytoEstablishRepository(false); if (repository != null) { try { project.saveAllEditors(); project.saveAll(); } catch (IOException ioe) { String msg = DialogManager.getMessage("team-error-saving-project"); if (msg != null) { msg = Utility.mergeStrings(msg, ioe.getLocalizedMessage()); String msgFinal = msg; DialogManager.showErrorTextFX(this.asWindow(), msgFinal); } } startProgress(); commitAndPushWorker = new CommitAndPushWorker(); commitAndPushWorker.start(); if (!isShowing()) { show(); } } else { hide(); } } else { hide(); } } public void setComment(String newComment) { commitText.setText(newComment); } @Override public void reset() { commitListModel.clear(); pushListModel.clear(); setComment(""); progressBar.setMessage(""); } private void removeModifiedLayouts() { commitListModel.removeAll(changedLayoutFiles); } @Override public String getComment() { return commitText.getText(); }
| Get a list of the layout files to be committed | | @return | @Override public Set getChangedLayoutFiles() { return changedLayoutFiles.stream().map(info -> info.getFile()).collect(Collectors.toSet()); }
| Get a set of the layout files which have changed (with status info). | | @return the set of the layout files which have changed. | @Override public Set getChangedLayoutInfo() { return changedLayoutFiles; } @Override public boolean includeLayout() { return includeLayout != null && includeLayout.isSelected(); } private void addModifiedLayouts() { Set<File> displayedLayouts = new HashSet<>(); for (TeamStatusInfo info : changedLayoutFiles) { File parentFile = info.getFile().getParentFile(); if (!displayedLayouts.contains(parentFile)) { commitListModel.add(info); displayedLayouts.add(info.getFile().getParentFile()); } } }
| Start the activity indicator. | @Override @OnThread(Tag.FXPlatform) public void startProgress() { progressBar.setRunning(true); }
| Stop the activity indicator. Call from any thread. | @Override @OnThread(Tag.Any) public void stopProgress() { JavaFXUtil.runNowOrLater(() -> progressBar.setRunning(false)); } @Override @OnThread(Tag.FXPlatform) public Project getProject() { return project; } @OnThread(Tag.FXPlatform) public void displayMessage(String msg) { progressBar.setMessage(msg); } @OnThread(Tag.FXPlatform) public Window asWindow() { Scene scene = getDialogPane().getScene(); if (scene == null) return null; else{ return scene.getWindow(); } }
| Gets the list of files that would be pushed by a push. | public List getFilesToPush() { return Utility.mapList(pushListModel, s -> s.getFile()); } class CommitAndPushWorker extends FXWorker implements StatusListener { List<TeamStatusInfo> response; TeamworkCommand command; TeamworkCommandResult result; private boolean aborted, isPushAvailable; public CommitAndPushWorker() { super(); response = new ArrayList<>(); FileFilter filter = project.getTeamSettingsController().getFileFilter(true, false); command = repository.getStatus(this, filter, false); } public boolean isPushAvailable() { return this.isPushAvailable; }
| | @see bluej.groupwork.StatusListener#gotStatus(bluej.groupwork.TeamStatusInfo) | @OnThread(Tag.Any) @Override public void gotStatus(TeamStatusInfo info) { response.add(info); }
| | @see bluej.groupwork.StatusListener#statusComplete(bluej.groupwork.CommitHandle) | @OnThread(Tag.Worker) @Override public void statusComplete(StatusHandle statusHandle) { commitAction.setStatusHandle(statusHandle); pushNeeded = statusHandle.pushNeeded(); updateNeeded = statusHandle.pullNeeded(); pushAction.setStatusHandle(statusHandle); } @OnThread(Tag.Worker) @Override public Object construct() { result = command.getResult(); return response; } public void abort() { command.cancel(); aborted = true; } @Override public void finished() { stopProgress(); if (!aborted) { if (result.isError()) { TeamUtils.handleServerResponseFX(result, CommitAndPushFrame.this.asWindow()); CommitAndPushFrame.this.hide(); } else if (response != null) { Set<File> filesToCommit = new HashSet<>(); Set<File> filesToAdd = new LinkedHashSet<>(); Set<File> filesToDelete = new HashSet<>(); List<TeamStatusInfo> info = response; getCommitFileSets(info, filesToCommit, filesToAdd, filesToDelete, changedLayoutFiles); includeLayout.setDisable(changedLayoutFiles.isEmpty()); commitAction.setFiles(filesToCommit); commitAction.setNewFiles(filesToAdd); commitAction.setDeletedFiles(filesToDelete); updateListModel(commitListModel, filesToCommit, info); updateListModel(commitListModel, filesToAdd, info); updateListModel(commitListModel, filesToDelete, info); if (updateNeeded) { pushListModel.clear(); isPushAvailable = false; } else { Set<File> filesToPush = new HashSet<>(); getPushFileSets(info, filesToPush); updateListModel(pushListModel, filesToPush, info); this.isPushAvailable = pushNeeded; } pushAction.setEnabled(this.isPushAvailable); } if (!commitListModel.isEmpty()) { commitText.requestFocus(); } if (pushListModel.isEmpty()) { if (isPushAvailable) { pushFiles.setPlaceholder(new Label(Config.getString("team.pushNeeded"))); } else if (updateNeeded) { pushFiles.setPlaceholder(new Label(Config.getString("team.pullNeeded"))); } else { pushFiles.setPlaceholder(new Label(Config.getString("team.nopushfiles"))); } } } }
| Go through the status list, and figure out which files to commit, and | of those which are to be added (i.e. which aren't in the repository) | and which are to be removed. | | @param info The list of files with status (List of TeamStatusInfo) | @param filesToCommit The set to store the files to commit in | @param filesToAdd The set to store the files to be added in | @param filesToRemove The set to store the files to be removed in | @param mergeConflicts The set to store files with merge conflicts in. | @param deleteConflicts The set to store files with conflicts in, which need to be | resolved by first deleting the local file | @param otherConflicts The set to store files with "locally deleted" conflicts (locally |* deleted, remotely modified). * @param needsMerge The set of files which are updated locally as well as in the * repository (required merging). | @param conflicts The set to store unresolved conflicts in | | @param remote false if this is a non-distributed repository. | private void getCommitFileSets(List<TeamStatusInfo> info, Set<File> filesToCommit, Set<File> filesToAdd, Set<File> filesToRemove, Set<TeamStatusInfo> modifiedLayoutFiles) { for (TeamStatusInfo statusInfo : info) { File file = statusInfo.getFile(); boolean isPkgFile = BlueJPackageFile.isPackageFileName(file.getName()); Status status = statusInfo.getStatus(true); if (status == Status.NEEDS_ADD || status == Status.CONFLICT_ADD) { filesToAdd.add(file); filesToCommit.add(file); statusInfo.setStatus(Status.NEEDS_ADD); } else if (status == Status.DELETED) { filesToRemove.add(file); filesToCommit.add(file); } else if (status == Status.CONFLICT_LDRM) { filesToCommit.add(file); } else if (status == Status.CONFLICT_LMRD) { if (file.exists()) { statusInfo.setStatus(Status.NEEDS_MERGE); filesToCommit.add(file); } else { statusInfo.setStatus(Status.DELETED); filesToRemove.add(file); filesToCommit.add(file); } } else if (status == Status.HAS_CONFLICTS) { } else if (status == Status.NEEDS_MERGE) { filesToAdd.add(file); filesToCommit.add(file); } else if (status == Status.NEEDS_COMMIT) { if (isPkgFile) { modifiedLayoutFiles.add(statusInfo); } else { filesToCommit.add(file); } } else if (status != Status.UP_TO_DATE) { Debug.message("Commit and push: unhandled file status: " + status + " (for " + file + ")"); } } includeLayout.setDisable(changedLayoutFiles.isEmpty()); }
| Go through the status list, and figure out which files to commit, and | of those which are to be added (i.e. which aren't in the repository) | and which are to be removed. | | @param info The list of files with status (List of TeamStatusInfo) | @param filesToPush The set to store the files that will be pushed | private void getPushFileSets(List<TeamStatusInfo> info, Set<File> filesToPush) { for (TeamStatusInfo statusInfo : info) { File file = statusInfo.getFile(); Status status = statusInfo.getStatus(false); if (status != Status.UP_TO_DATE && status != Status.NEEDS_CHECKOUT && status != Status.NEEDS_UPDATE) { filesToPush.add(file); } } }
| Update the list model with a file list. | @param fileSet | @param info | private void updateListModel(ObservableList<TeamStatusInfo> listModel, Set<File> fileSet, List<TeamStatusInfo> info) { listModel.addAll(fileSet.stream().map(file -> getTeamStatusInfoFromFile(file, info)) .filter(Objects::nonNull) .filter(statusInfo -> !listModel.contains(statusInfo)) .collect(Collectors.toList())); }
| Returns the status info for a specific file from an info list. | | @param file The file which its status info is needed. | @param infoList The list which contains files info. | @return The team status info for the file, or null if the list doesn't | include information about it. | private TeamStatusInfo getTeamStatusInfoFromFile(File file, List<TeamStatusInfo> infoList) { Optional<TeamStatusInfo> statusInfo = infoList.stream().filter( info -> info.getFile().equals(file) ).findFirst(); return statusInfo.isPresent() ? statusInfo.get() : null; } } }

.   CommitAndPushFrame
.   makeMainPane
.   prepareButtonPane
.   setVisible
.   setComment
.   reset
.   removeModifiedLayouts
.   getComment
.   getChangedLayoutFiles
.   getChangedLayoutInfo
.   includeLayout
.   addModifiedLayouts
.   startProgress
.   stopProgress
.   getProject
.   displayMessage
.   asWindow
.   getFilesToPush

top, use, map, class CommitAndPushWorker

.   CommitAndPushWorker
.   isPushAvailable
.   gotStatus
.   statusComplete
.   construct
.   abort
.   finished
.   getCommitFileSets
.   getPushFileSets
.   updateListModel
.   getTeamStatusInfoFromFile




691 neLoCode + 42 LoComm