diff --git a/pom.xml b/pom.xml index 4a135e36..af726559 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,22 @@ mpicbg + + org.eclipse.jgit + org.eclipse.jgit + 5.13.2.202306221912-r + + + + org.eclipse.jgit + org.eclipse.jgit.ssh.apache + 5.13.2.202306221912-r + + + org.eclipse.jgit + org.eclipse.jgit.ui + 5.13.2.202306221912-r + junit diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/CommitMessageDialog.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/CommitMessageDialog.java new file mode 100644 index 00000000..2dc24a14 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/CommitMessageDialog.java @@ -0,0 +1,60 @@ +package org.mastodon.mamut.tomancak.collaboration; + +import java.awt.KeyboardFocusManager; + +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +import net.miginfocom.swing.MigLayout; + +/** + * A dialog that asks the user to enter a commit message. + * The dialog can be closed by using the mouse to clock OK or Cancel, + * by pressing CTRL-ENTER or ESCAPE, or by pressing TAB and ENTER. + */ +public class CommitMessageDialog +{ + + /** + * Show a dialog that asks the user to enter a commit message. + * + * @return The commit message as a String, or null if the dialog was cancelled. + */ + public static String showDialog() + { + JTextArea textArea = new JTextArea( 5, 40 ); + + // Make the tab key move focus to the next component and shift-tab to the previous. + textArea.setFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, null ); + textArea.setFocusTraversalKeys( KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, null ); + + JPanel panel = new JPanel(); + panel.setLayout( new MigLayout( "insets dialog" ) ); + panel.add( new JLabel( "Save point message:" ), "wrap" ); + panel.add( new JScrollPane( textArea ), "wrap" ); + panel.add( new JLabel( "Please describe briefly the changes since the last save point!" ) ); + + // Show a JOptionPane, where the TextArea has focus when the dialog is shown. + JOptionPane optionPane = new JOptionPane( panel, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) + { + @Override + public void selectInitialValue() + { + super.selectInitialValue(); + textArea.requestFocusInWindow(); + } + }; + JDialog dialog = optionPane.createDialog( "Add Save Point (commit)" ); + dialog.setVisible( true ); + dialog.dispose(); + Object result = optionPane.getValue(); + if ( result instanceof Integer && ( int ) result == JOptionPane.OK_OPTION ) + return textArea.getText(); + else + return null; + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/ErrorDialog.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/ErrorDialog.java new file mode 100644 index 00000000..8ded6e72 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/ErrorDialog.java @@ -0,0 +1,71 @@ +package org.mastodon.mamut.tomancak.collaboration; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Frame; +import java.awt.event.ItemEvent; + +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +/** + * A dialog that shows an exception message and stack trace. + * The stack trace is hidden by default and can be shown by + * clicking on "details". + */ +public class ErrorDialog +{ + + public static void showErrorMessage( String title, Exception exception ) + { + SwingUtilities.invokeLater( () -> showDialog( null, title + " (Error)", exception ) ); + } + + private static void showDialog( Frame parent, String title, Exception exception ) + { + String message = "\nThere was a problem:\n\n " + exception.getMessage() + "\n\n"; + final JScrollPane scrollPane = initScrollPane( exception ); + final JCheckBox checkBox = new JCheckBox( "show details" ); + checkBox.setForeground( Color.BLUE ); + Object[] objects = { message, checkBox, scrollPane }; + JOptionPane pane = new JOptionPane( objects, JOptionPane.ERROR_MESSAGE ); + JDialog dialog = pane.createDialog( parent, title ); + dialog.setResizable( true ); + checkBox.addItemListener( event -> { + boolean visible = event.getStateChange() == ItemEvent.SELECTED; + scrollPane.setVisible( visible ); + scrollPane.setPreferredSize( visible ? null : new Dimension( 0, 0 ) ); + dialog.pack(); + } ); + dialog.setModal( true ); + dialog.pack(); + dialog.setVisible( true ); + dialog.dispose(); + } + + private static JScrollPane initScrollPane( Exception exception ) + { + String stackTrace = ExceptionUtils.getStackTrace( exception ); + int lines = Math.min( 20, countLines( stackTrace ) ); + JTextArea textArea = new JTextArea( stackTrace, lines, 70 ); + textArea.setForeground( new Color( 0x880000 ) ); + textArea.setEditable( false ); + textArea.setFont( new Font( Font.MONOSPACED, Font.PLAIN, textArea.getFont().getSize() ) ); + final JScrollPane scrollPane = new JScrollPane( textArea ); + scrollPane.setVisible( false ); + return scrollPane; + } + + private static int countLines( String str ) + { + String[] lines = str.split( "\r\n|\r|\n" ); + return lines.length; + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/FixGraphInconsistenciesPlugin.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/FixGraphInconsistenciesPlugin.java new file mode 100644 index 00000000..e96023d5 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/FixGraphInconsistenciesPlugin.java @@ -0,0 +1,144 @@ +package org.mastodon.mamut.tomancak.collaboration; + +import org.mastodon.collection.RefCollection; +import org.mastodon.collection.RefCollections; +import org.mastodon.collection.RefList; +import org.mastodon.collection.RefSet; +import org.mastodon.collection.ref.RefArrayList; +import org.mastodon.collection.ref.RefSetImp; +import org.mastodon.mamut.model.Link; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.model.ModelGraph; +import org.mastodon.mamut.model.Spot; +import org.mastodon.mamut.plugin.MamutPlugin; +import org.mastodon.mamut.tomancak.collaboration.utils.ActionDescriptions; +import org.mastodon.mamut.tomancak.collaboration.utils.BasicDescriptionProvider; +import org.mastodon.mamut.tomancak.collaboration.utils.BasicMamutPlugin; +import org.mastodon.ui.keymap.CommandDescriptionProvider; +import org.mastodon.ui.keymap.KeyConfigContexts; +import org.scijava.plugin.Plugin; + +@Plugin( type = MamutPlugin.class ) +public class FixGraphInconsistenciesPlugin extends BasicMamutPlugin +{ + + private static final ActionDescriptions< FixGraphInconsistenciesPlugin > actionDescriptions = new ActionDescriptions<>( FixGraphInconsistenciesPlugin.class ); + + static + { + actionDescriptions.addActionDescription( + "[mastodon-tomancak] fix graph inconsistencies", + "Plugins > Trees Management > Fix Graph Inconsistencies", + "Flip reversed edges", + FixGraphInconsistenciesPlugin::fixGraphInconsistencies ); + } + + public < T > FixGraphInconsistenciesPlugin() + { + super( actionDescriptions ); + } + + private void fixGraphInconsistencies() + { + Model model = getAppModel().getModel(); + ModelGraph graph = model.getGraph(); + flipBackwardsEdges( graph ); + removeDoubleEdges( graph ); + removeSameTimepointEdge( graph ); + printWarnings( graph ); + } + + private static void printWarnings( ModelGraph graph ) + { + for ( Spot spot : graph.vertices() ) + { + if ( spot.incomingEdges().size() > 1 ) + System.out.println( "More than one parent: " + spot.getLabel() ); + + if ( spot.outgoingEdges().size() > 2 ) + System.out.println( "More than two children: " + spot.getLabel() ); + } + } + + private static void removeSameTimepointEdge( ModelGraph graph ) + { + RefCollection< Link > backwardEdge = RefCollections.createRefList( graph.edges() ); + Spot sourceRef = graph.vertexRef(); + Spot targetRef = graph.vertexRef(); + for ( Link edge : graph.edges() ) + { + Spot source = edge.getSource( sourceRef ); + Spot target = edge.getTarget( targetRef ); + if ( source.getTimepoint() == target.getTimepoint() ) + backwardEdge.add( edge ); + } + for ( Link edge : backwardEdge ) + { + Spot source = edge.getSource( sourceRef ); + Spot target = edge.getTarget( targetRef ); + System.out.println( "Remove same timepoint edge " + source.getLabel() + " -> " + target.getLabel() ); + graph.remove( edge ); + } + } + + private static void removeDoubleEdges( ModelGraph graph ) + { + Spot ref1 = graph.vertexRef(); + Spot ref2 = graph.vertexRef(); + RefSet< Spot > sources = new RefSetImp<>( graph.vertices().getRefPool() ); + RefList< Link > doubleEdge = new RefArrayList<>( graph.edges().getRefPool() ); + for ( Spot spot : graph.vertices() ) + { + if ( spot.incomingEdges().size() > 1 ) + { + for ( Link link : spot.incomingEdges() ) + { + Spot source = link.getSource( ref1 ); + if ( sources.contains( source ) ) + doubleEdge.add( link ); + else + sources.add( source ); + } + sources.clear(); + } + } + for ( Link link : doubleEdge ) + { + Spot source = link.getSource( ref1 ); + Spot target = link.getTarget( ref2 ); + System.out.println( "Remove duplicated edge: " + source.getLabel() + " -> " + target.getLabel() ); + graph.remove( link ); + } + } + + private static void flipBackwardsEdges( ModelGraph graph ) + { + RefCollection< Link > backwardEdge = RefCollections.createRefList( graph.edges() ); + Spot sourceRef = graph.vertexRef(); + Spot targetRef = graph.vertexRef(); + for ( Link edge : graph.edges() ) + { + Spot source = edge.getSource( sourceRef ); + Spot target = edge.getTarget( targetRef ); + if ( source.getTimepoint() > target.getTimepoint() ) + backwardEdge.add( edge ); + } + for ( Link edge : backwardEdge ) + { + Spot source = edge.getSource( sourceRef ); + Spot target = edge.getTarget( targetRef ); + System.out.println( "Flip backwards edge " + source.getLabel() + " -> " + target.getLabel() ); + graph.remove( edge ); + graph.addEdge( source, target ).init(); + } + } + + @Plugin( type = CommandDescriptionProvider.class ) + public static class DescriptionProvider extends BasicDescriptionProvider + { + public DescriptionProvider() + { + super( actionDescriptions, KeyConfigContexts.MASTODON, KeyConfigContexts.TRACKSCHEME ); + } + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/MastodonGitController.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/MastodonGitController.java new file mode 100644 index 00000000..f4845518 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/MastodonGitController.java @@ -0,0 +1,406 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2022 Tobias Pietzsch, Jean-Yves Tinevez + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.tomancak.collaboration; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CancellationException; + +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; + +import org.mastodon.mamut.plugin.MamutPlugin; +import org.mastodon.mamut.tomancak.collaboration.commands.MastodonGitCloneRepository; +import org.mastodon.mamut.tomancak.collaboration.commands.MastodonGitCreateRepository; +import org.mastodon.mamut.tomancak.collaboration.commands.MastodonGitNewBranch; +import org.mastodon.mamut.tomancak.collaboration.commands.MastodonGitSetAuthorCommand; +import org.mastodon.mamut.tomancak.collaboration.exceptions.GraphMergeConflictException; +import org.mastodon.mamut.tomancak.collaboration.exceptions.GraphMergeException; +import org.mastodon.mamut.tomancak.collaboration.utils.ActionDescriptions; +import org.mastodon.mamut.tomancak.collaboration.utils.BasicDescriptionProvider; +import org.mastodon.mamut.tomancak.collaboration.utils.BasicMamutPlugin; +import org.mastodon.mamut.tomancak.collaboration.settings.MastodonGitSettingsService; +import org.mastodon.ui.keymap.CommandDescriptionProvider; +import org.mastodon.ui.keymap.KeyConfigContexts; +import org.scijava.command.CommandService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; + +@Plugin( type = MamutPlugin.class ) +public class MastodonGitController extends BasicMamutPlugin +{ + @Parameter + private CommandService commandService; + + @Parameter + private MastodonGitSettingsService settingsService; + + public static final ActionDescriptions< MastodonGitController > actionDescriptions = new ActionDescriptions<>( MastodonGitController.class ); + + private static final String SHARE_PROJECT_ACTION_KEY = actionDescriptions.addActionDescription( + "[mastodon git] share project", + "Plugins > Collaborative (Git) > Initialize > Share Project", + "Upload Mastodon project to a newly created git repository.", + MastodonGitController::shareProject ); + + private static final String CLONE_REPOSITORY_ACTION_KEY = actionDescriptions.addActionDescription( + "[mastodon git] download shared project (clone)", + "Plugins > Collaborative (Git) > Initialize > Download Shared Project (clone)", + "Download a shared project, save a copy on the local disc and open it with Mastodon.", + MastodonGitController::cloneGitRepository ); + + private static final String SET_AUTHOR_ACTION_KEY = actionDescriptions.addActionDescription( + "[mastodon git] set author name", + "Plugins > Collaborative (Git) > Initialize > Set Author Name", + "Set the author name that is used for your commits.", + MastodonGitController::setAuthor ); + + private static final String SYNCHRONIZE_ACTION_KEY = actionDescriptions.addActionDescription( + "[mastodon git] synchronize (commit, pull, push)", + "Plugins > Collaborative (Git) > Synchronize (commit, pull, push)", + "Download remote changes and upload local changes.", + MastodonGitController::synchronize ); + + private static final String COMMIT_ACTION_KEY = actionDescriptions.addActionDescription( + "[mastodon git] add save point (commit)", + "Plugins > Collaborative (Git) > Add Save Point (commit)", + "Commit changes to the git repository.", + MastodonGitController::commit ); + + private static final String PUSH_ACTION_KEY = actionDescriptions.addActionDescription( + "[mastodon git] upload changes (push)", + "Plugins > Collaborative (Git) > Upload Changes (push)", + "Push local changed to the remote server.", + MastodonGitController::push ); + + private static final String PULL_ACTION_KEY = actionDescriptions.addActionDescription( + "[mastodon git] download changes (pull)", + "Plugins > Collaborative (Git) > Download Changes (pull)", + "Download changes from the remote server and merge them with my changes.", + MastodonGitController::pull ); + + private static final String RESET_ACTION_KEY = actionDescriptions.addActionDescription( + "[mastodon git] go back to latest save point (reset)", + "Plugins > Collaborative (Git) > Go Back To Latest Save Point (reset)", + "Discard all changes made since the last save point.", + MastodonGitController::reset ); + + private static final String NEW_BRANCH_ACTION_KEY = actionDescriptions.addActionDescription( + "[mastodon git] new branch", + "Plugins > Collaborative (Git) > Branches > Create New Branch", + "Create a new branch in the git repository.", + MastodonGitController::newBranch ); + + private static final String SHOW_BRANCH_NAME_ACTION_KEY = actionDescriptions.addActionDescription( + "[mastodon git] show branch name", + "Plugins > Collaborative (Git) > Branches > Show Branch Name", + "Show the name of the current git branch", + MastodonGitController::showBranchName ); + + private static final String SWITCH_ACTION_KEY = actionDescriptions.addActionDescription( + "[mastodon git] switch branch", + "Plugins > Collaborative (Git) > Branches > Switch Branch", + "Switch to a different branch in the git repository.", + MastodonGitController::switchBranch ); + + private static final String MERGE_ACTION_KEY = actionDescriptions.addActionDescription( + "[mastodon git] merge branch", + "Plugins > Collaborative (Git) > Branches > Merge Branch", + "Merge a branch into the current branch.", + MastodonGitController::mergeBranch ); + + private static final List< String > IN_REPOSITORY_ACTIONS = Arrays.asList( + COMMIT_ACTION_KEY, + PUSH_ACTION_KEY, + PULL_ACTION_KEY, + RESET_ACTION_KEY, + NEW_BRANCH_ACTION_KEY, + SWITCH_ACTION_KEY, + MERGE_ACTION_KEY ); + + private MastodonGitRepository repository; + + public MastodonGitController() + { + super( actionDescriptions ); + } + + @Override + protected void initialize() + { + super.initialize(); + repository = new MastodonGitRepository( getWindowManager() ); + updateEnableCommands(); + } + + private void setAuthor() + { + commandService.run( MastodonGitSetAuthorCommand.class, true ); + } + + private void shareProject() + { + if ( !settingsService.isAuthorSpecified() ) + { + askForAuthorName( "Please set your author name before sharing a project." ); + return; + } + MastodonGitCreateRepository.Callback callback = ( File directory, String url ) -> { + this.repository = MastodonGitRepository.shareProject( getWindowManager(), directory, url ); + updateEnableCommands(); + }; + commandService.run( MastodonGitCreateRepository.class, true, "callback", callback ); + } + + private void updateEnableCommands() + { + boolean isRepository = repository.isRepository(); + IN_REPOSITORY_ACTIONS.forEach( action -> setActionEnabled( action, isRepository ) ); + } + + private void cloneGitRepository() + { + commandService.run( MastodonGitCloneRepository.class, true ); + } + + private void commit() + { + if ( !settingsService.isAuthorSpecified() ) + { + askForAuthorName( "Please set your author name before adding a save point (commit)." ); + return; + } + run( "Add Save Point (Commit)", () -> { + if ( repository.isClean() ) + NotificationDialog.show( "Add Save Point (Commit)", + " No changes to commit." ); + else + { + String commitMessage = CommitMessageDialog.showDialog(); + if ( commitMessage == null ) + return; + repository.commitWithoutSave( commitMessage ); + } + } ); + } + + private void push() + { + run( "Upload Changes (Push)", () -> { + repository.push(); + NotificationDialog.show( "Upload Changes (Push)", + " Completed successfully." ); + } ); + } + + private void newBranch() + { + commandService.run( MastodonGitNewBranch.class, true, "repository", repository ); + } + + private void switchBranch() + { + try + { + String message = "Select a branch"; + try + { + repository.fetchAll(); + } + catch ( Exception e ) + { + message += " \n(There was a failure downloading the latest branch changes.)"; + } + List< String > branches = repository.getBranches(); + String currentBranch = repository.getCurrentBranch(); + // show JOptionPane that allows to select a branch + String selectedBranch = ( String ) JOptionPane.showInputDialog( null, message, "Switch Git Branch", JOptionPane.PLAIN_MESSAGE, null, branches.toArray(), currentBranch ); + if ( selectedBranch == null ) + return; + // switch to selected branch + run( "Switch To Branch", () -> repository.switchBranch( selectedBranch ) ); + } + catch ( Exception e ) + { + ErrorDialog.showErrorMessage( "Select Branch", e ); + } + } + + private void mergeBranch() + { + if ( !settingsService.isAuthorSpecified() ) + { + askForAuthorName( "You need to set your author name before you can merge branches." ); + return; + } + try + { + List< String > branches = repository.getBranches(); + String currentBranch = repository.getCurrentBranch(); + branches.remove( currentBranch ); + // show JOptionPane that allows to select a branch + String selectedBranch = ( String ) JOptionPane.showInputDialog( null, "Select a branch", "Switch Git Branch", JOptionPane.PLAIN_MESSAGE, null, branches.toArray(), null ); + if ( selectedBranch == null ) + return; + repository.mergeBranch( selectedBranch ); + } + catch ( Exception e ) + { + ErrorDialog.showErrorMessage( "Merge Branch", e ); + } + } + + private void pull() + { + run( "Download Changes (Pull)", () -> { + try + { + repository.pull(); + } + catch ( GraphMergeException e ) + { + if ( !( e instanceof GraphMergeConflictException ) ) + e.printStackTrace(); + SwingUtilities.invokeLater( () -> suggestPullAlternative( e.getMessage() ) ); + } + } ); + } + + private void suggestPullAlternative( String errorMessage ) + { + String title = "Conflict During Download Of Changes (Pull)"; + String message = "There was a merge conflict during the pull. Details:\n" + + " " + errorMessage + "\n\n" + + "You made changes on your computer that could not be automatically\n" + + "merged with the changes on the server.\n\n" + + "You can either:\n" + + " 1. Throw away your local changes & local save points.\n" + + " 2. Or cancel (And maybe save your local changes to a new branch,\n" + + " which you can then be merged into the remote branch.)\n"; + + String[] options = { "Discard Local Changes", "Cancel" }; + int result = JOptionPane.showOptionDialog( null, message, title, JOptionPane.YES_NO_OPTION, + JOptionPane.PLAIN_MESSAGE, null, options, options[ 0 ] ); + if ( result == JOptionPane.YES_OPTION ) + resetToRemoteBranch(); + } + + private void resetToRemoteBranch() + { + run( "Throw Away All Local Changes (Reset To Remote)", () -> repository.resetToRemoteBranch() ); + } + + private void reset() + { + run( "Go Back To Last Save Point (Reset)", () -> repository.reset() ); + } + + private void askForAuthorName( String message ) + { + String title = "Set Author Name"; + String[] options = { "Set Author Name", "Cancel" }; + int result = JOptionPane.showOptionDialog( null, message, title, JOptionPane.YES_NO_OPTION, + JOptionPane.PLAIN_MESSAGE, null, options, options[ 0 ] ); + if ( result == JOptionPane.YES_OPTION ) + setAuthor(); + } + + private void showBranchName() + { + run( "Show Branch Name", () -> { + String longBranchName = repository.getCurrentBranch(); + String shortBranchName = longBranchName.replaceAll( "^refs/heads/", "" ); + String title = "Current Branch Name"; + String message = "The current branch is:
" + shortBranchName; + SwingUtilities.invokeLater( () -> + JOptionPane.showMessageDialog( null, message, title, JOptionPane.PLAIN_MESSAGE ) ); + } ); + } + + private void synchronize() + { + run( "Synchronize Changes", () -> { + boolean clean = repository.isClean(); + if ( !clean ) + { + String commitMessage = CommitMessageDialog.showDialog(); + if ( commitMessage == null ) + return; + repository.commitWithoutSave( commitMessage ); + } + try + { + repository.pull(); + } + catch ( GraphMergeException e ) + { + if ( !( e instanceof GraphMergeConflictException ) ) + e.printStackTrace(); + SwingUtilities.invokeLater( () -> suggestPullAlternative( e.getMessage() ) ); + return; + } + repository.push(); + NotificationDialog.show( "Synchronize Changes (Commit, Pull, Push)", + " Completed successfully." ); + } ); + } + + private void run( String title, RunnableWithException action ) + { + new Thread( () -> { + try + { + action.run(); + } + catch ( CancellationException e ) + { + // ignore + } + catch ( Exception e ) + { + ErrorDialog.showErrorMessage( title, e ); + } + } ).start(); + } + + interface RunnableWithException + { + void run() throws Exception; + } + + @Plugin( type = CommandDescriptionProvider.class ) + public static class DescriptionProvider extends BasicDescriptionProvider + { + public DescriptionProvider() + { + super( actionDescriptions, KeyConfigContexts.MASTODON, KeyConfigContexts.TRACKSCHEME ); + } + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/MastodonGitRepository.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/MastodonGitRepository.java new file mode 100644 index 00000000..4ecabba5 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/MastodonGitRepository.java @@ -0,0 +1,440 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2022 Tobias Pietzsch, Jean-Yves Tinevez + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.tomancak.collaboration; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jgit.api.CheckoutCommand; +import org.eclipse.jgit.api.CommitCommand; +import org.eclipse.jgit.api.CreateBranchCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ListBranchCommand; +import org.eclipse.jgit.api.ResetCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.BranchConfig; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.PushResult; +import org.eclipse.jgit.transport.RemoteRefUpdate; +import org.mastodon.graph.io.RawGraphIO; +import org.mastodon.mamut.MainWindow; +import org.mastodon.mamut.WindowManager; +import org.mastodon.mamut.feature.MamutRawFeatureModelIO; +import org.mastodon.mamut.model.Link; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.model.Spot; +import org.mastodon.mamut.project.MamutProject; +import org.mastodon.mamut.project.MamutProjectIO; +import org.mastodon.mamut.tomancak.collaboration.credentials.PersistentCredentials; +import org.mastodon.mamut.tomancak.collaboration.exceptions.GraphMergeConflictException; +import org.mastodon.mamut.tomancak.collaboration.exceptions.GraphMergeException; +import org.mastodon.mamut.tomancak.collaboration.exceptions.MastodonGitException; +import org.mastodon.mamut.tomancak.collaboration.utils.ConflictUtils; +import org.mastodon.mamut.tomancak.collaboration.settings.MastodonGitSettingsService; +import org.mastodon.mamut.tomancak.merging.Dataset; +import org.mastodon.mamut.tomancak.merging.MergeDatasets; +import org.scijava.Context; + +import mpicbg.spim.data.SpimDataException; + +// make it one synchronized class per repository +// don't allow to open a repository twice (maybe read only) +public class MastodonGitRepository +{ + + private static final PersistentCredentials credentials = new PersistentCredentials(); + + private final WindowManager windowManager; + + private final MastodonGitSettingsService settingsService; + + public MastodonGitRepository( WindowManager windowManager ) + { + this.windowManager = windowManager; + settingsService = windowManager.getContext().service( MastodonGitSettingsService.class ); + } + + public static MastodonGitRepository shareProject( + WindowManager windowManager, + File directory, + String repositoryURL ) + throws Exception + { + if ( !directory.isDirectory() ) + throw new IllegalArgumentException( "Not a directory: " + directory ); + if ( !isEmpty( directory ) ) + throw new IllegalArgumentException( "Directory not empty: " + directory ); + Git git = Git.cloneRepository() + .setURI( repositoryURL ) + .setCredentialsProvider( credentials.getSingleUseCredentialsProvider() ) + .setDirectory( directory ) + .call(); + Path mastodonProjectPath = directory.toPath().resolve( "mastodon.project" ); + if ( Files.exists( mastodonProjectPath ) ) + throw new MastodonGitException( "The repository already contains a shared mastodon project: " + repositoryURL ); + Files.createDirectory( mastodonProjectPath ); + windowManager.getProjectManager().saveProject( mastodonProjectPath.toFile() ); + Files.copy( mastodonProjectPath.resolve( "gui.xml" ), mastodonProjectPath.resolve( "gui.xml_remote" ) ); + Files.copy( mastodonProjectPath.resolve( "project.xml" ), mastodonProjectPath.resolve( "project.xml_remote" ) ); + Files.copy( mastodonProjectPath.resolve( "dataset.xml.backup" ), mastodonProjectPath.resolve( "dataset.xml.backup_remote" ) ); + Path gitignore = directory.toPath().resolve( ".gitignore" ); + Files.write( gitignore, "/mastodon.project/gui.xml\n".getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND ); + Files.write( gitignore, "/mastodon.project/project.xml\n".getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND ); + Files.write( gitignore, "/mastodon.project/dataset.xml.backup\n".getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND ); + git.add().addFilepattern( ".gitignore" ).call(); + git.commit().setMessage( "Add .gitignore file" ).call(); + git.add().addFilepattern( "mastodon.project" ).call(); + git.commit().setMessage( "Share mastodon project" ).call(); + git.push().setCredentialsProvider( credentials.getSingleUseCredentialsProvider() ).setRemote( "origin" ).call(); + git.close(); + return new MastodonGitRepository( windowManager ); + } + + private static boolean isEmpty( File directory ) + { + String[] containedFiles = directory.list(); + return containedFiles == null || containedFiles.length == 0; + } + + public static void cloneRepository( String repositoryURL, File directory ) throws Exception + { + try (Git git = Git.cloneRepository() + .setURI( repositoryURL ) + .setCredentialsProvider( credentials.getSingleUseCredentialsProvider() ) + .setDirectory( directory ) + .call()) + { + Path mastodonProjectPath = directory.toPath().resolve( "mastodon.project" ); + Files.copy( mastodonProjectPath.resolve( "gui.xml_remote" ), mastodonProjectPath.resolve( "gui.xml" ) ); + Files.copy( mastodonProjectPath.resolve( "project.xml_remote" ), mastodonProjectPath.resolve( "project.xml" ) ); + Files.copy( mastodonProjectPath.resolve( "dataset.xml.backup_remote" ), mastodonProjectPath.resolve( "dataset.xml.backup" ) ); + } + } + + public static void openProjectInRepository( Context context, File directory ) throws Exception + { + WindowManager windowManager = new WindowManager( context ); + Path path = directory.toPath().resolve( "mastodon.project" ); + windowManager.getProjectManager().openWithDialog( new MamutProjectIO().load( path.toAbsolutePath().toString() ) ); + new MainWindow( windowManager ).setVisible( true ); + } + + public synchronized void commit( String message ) throws Exception + { + windowManager.getProjectManager().saveProject(); + commitWithoutSave( message ); + } + + public void commitWithoutSave( String message ) throws Exception + { + try (Git git = initGit()) + { + git.add().addFilepattern( "mastodon.project" ).call(); + CommitCommand commit = git.commit(); + commit.setMessage( message ); + commit.setAuthor( settingsService.getPersonIdent() ); + commit.call(); + } + } + + public synchronized void push() throws Exception + { + try (Git git = initGit()) + { + Iterable< PushResult > results = git.push().setCredentialsProvider( credentials.getSingleUseCredentialsProvider() ).setRemote( "origin" ).call(); + raiseExceptionOnUnsuccessfulPush( results ); + } + } + + private static void raiseExceptionOnUnsuccessfulPush( Iterable< PushResult > results ) + { + for ( PushResult result : results ) + { + for ( RemoteRefUpdate update : result.getRemoteUpdates() ) + { + if ( update.getStatus() == RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD ) + throw new MastodonGitException( "The remote server has changes, that you didn't download yet.\n" + + "Please download changes first. (pull)\n" + + "You can upload your changes afterwards.\n" ); + if ( update.getStatus() != RemoteRefUpdate.Status.OK && + update.getStatus() != RemoteRefUpdate.Status.UP_TO_DATE ) + throw new MastodonGitException( "Push failed: " + update.getMessage() + " " + update.getStatus() ); + } + } + } + + public synchronized void createNewBranch( String branchName ) throws Exception + { + try (Git git = initGit()) + { + git.checkout().setCreateBranch( true ).setName( branchName ).call(); + } + } + + public synchronized void switchBranch( String branchName ) throws Exception + { + File projectRoot = windowManager.getProjectManager().getProject().getProjectRoot(); + try (Git git = initGit( projectRoot )) + { + ensureClean( git, "switching the branch" ); + boolean isRemoteBranch = branchName.startsWith( "refs/remotes/" ); + if ( isRemoteBranch ) + { + String simpleName = getSimpleName( branchName ); + boolean conflict = git.branchList().call().stream().map( Ref::getName ).anyMatch( localName -> simpleName.equals( getSimpleName( localName ) ) ); + if ( conflict ) + throw new MastodonGitException( "There's already a local branch with the same name." ); + git.checkout() + .setCreateBranch( true ) + .setName( simpleName ) + .setUpstreamMode( CreateBranchCommand.SetupUpstreamMode.TRACK ) + .setStartPoint( branchName ) + .call(); + } + else + git.checkout().setName( branchName ).call(); + } + windowManager.getProjectManager().openWithDialog( new MamutProjectIO().load( projectRoot.getAbsolutePath() ) ); + } + + private synchronized String getSimpleName( String branchName ) + { + String[] parts = branchName.split( "/" ); + return parts[ parts.length - 1 ]; + } + + public synchronized List< String > getBranches() throws Exception + { + try (Git git = initGit()) + { + return git.branchList().setListMode( ListBranchCommand.ListMode.ALL ).call().stream().map( Ref::getName ).collect( Collectors.toList() ); + } + } + + public synchronized void fetchAll() throws Exception + { + try (Git git = initGit()) + { + git.fetch().setCredentialsProvider( credentials.getSingleUseCredentialsProvider() ).call(); + } + } + + public synchronized String getCurrentBranch() throws Exception + { + try (Git git = initGit()) + { + return git.getRepository().getFullBranch(); + } + } + + public synchronized void mergeBranch( String selectedBranch ) throws Exception + { + Context context = windowManager.getContext(); + MamutProject project = windowManager.getProjectManager().getProject(); + File projectRoot = project.getProjectRoot(); + try (Git git = initGit()) + { + ensureClean( git, "merging" ); + String currentBranch = getCurrentBranch(); + Dataset dsA = new Dataset( projectRoot.getAbsolutePath() ); + git.checkout().setName( selectedBranch ).call(); + Dataset dsB = new Dataset( projectRoot.getAbsolutePath() ); + git.checkout().setName( currentBranch ).call(); + git.merge().setCommit( false ).include( git.getRepository().exactRef( selectedBranch ) ).call(); // TODO selected branch, should not be a string but a ref instead + Model mergedModel = merge( dsA, dsB ); + saveModel( context, mergedModel, project ); + commitWithoutSave( "Merge commit generated with Mastodon" ); + reloadFromDisc(); + } + } + + public synchronized void pull() throws Exception + { + Context context = windowManager.getContext(); + MamutProject project = windowManager.getProjectManager().getProject(); + File projectRoot = project.getProjectRoot(); + try (Git git = initGit()) + { + ensureClean( git, "pulling" ); + try + { + boolean conflict = !git.pull() + .setCredentialsProvider( credentials.getSingleUseCredentialsProvider() ) + .setRemote( "origin" ) + .setRebase( false ) + .call().isSuccessful(); + if ( conflict ) + automaticMerge( context, project, projectRoot, git ); + } + finally + { + abortMerge( git ); + } + reloadFromDisc(); + } + } + + private void automaticMerge( Context context, MamutProject project, File projectRoot, Git git ) throws Exception + { + try + { + git.checkout().setAllPaths( true ).setStage( CheckoutCommand.Stage.OURS ).call(); + Dataset dsA = new Dataset( projectRoot.getAbsolutePath() ); + git.checkout().setAllPaths( true ).setStage( CheckoutCommand.Stage.THEIRS ).call(); + Dataset dsB = new Dataset( projectRoot.getAbsolutePath() ); + git.checkout().setAllPaths( true ).setStage( CheckoutCommand.Stage.OURS ).call(); + Model mergedModel = merge( dsA, dsB ); + if ( ConflictUtils.hasConflict( mergedModel ) ) + throw new GraphMergeConflictException(); + ConflictUtils.removeMergeConflictTagSets( mergedModel ); + saveModel( context, mergedModel, project ); + commitWithoutSave( "Automatic merge by Mastodon during pull" ); + } + catch ( GraphMergeException e ) + { + throw e; + } + catch ( Throwable t ) + { + throw new GraphMergeException( "There was a failure, when merging changes to the Model.", t ); + } + } + + private static void saveModel( Context context, Model model, MamutProject project ) throws IOException + { + project.setProjectRoot( project.getProjectRoot() ); + try (final MamutProject.ProjectWriter writer = project.openForWriting()) + { + new MamutProjectIO().save( project, writer ); + final RawGraphIO.GraphToFileIdMap< Spot, Link > idmap = model.saveRaw( writer ); + MamutRawFeatureModelIO.serialize( context, model, idmap, writer ); + } + } + + private static Model merge( Dataset dsA, Dataset dsB ) + { + final MergeDatasets.OutputDataSet output = new MergeDatasets.OutputDataSet(); + double distCutoff = 1000; + double mahalanobisDistCutoff = 1; + double ratioThreshold = 2; + MergeDatasets.merge( dsA, dsB, output, distCutoff, mahalanobisDistCutoff, ratioThreshold ); + return output.getModel(); + } + + private synchronized void reloadFromDisc() throws IOException, SpimDataException + { + MamutProject project = windowManager.getProjectManager().getProject(); + windowManager.getProjectManager().openWithDialog( project ); + } + + public synchronized void reset() throws Exception + { + try (Git git = initGit()) + { + git.reset().setMode( ResetCommand.ResetType.HARD ).call(); + reloadFromDisc(); + } + } + + private synchronized Git initGit() throws IOException + { + File projectRoot = windowManager.getProjectManager().getProject().getProjectRoot(); + return initGit( projectRoot ); + } + + private synchronized Git initGit( File projectRoot ) throws IOException + { + boolean correctFolder = projectRoot.getName().equals( "mastodon.project" ); + if ( !correctFolder ) + throw new MastodonGitException( "The current project does not appear to be in a git repo." ); + File gitRoot = projectRoot.getParentFile(); + if ( !new File( gitRoot, ".git" ).exists() ) + throw new MastodonGitException( "The current project does not appear to be in a git repo." ); + return Git.open( gitRoot ); + } + + private synchronized boolean isClean( Git git ) throws GitAPIException + { + return git.status().call().isClean(); + } + + public boolean isRepository() + { + try (Git ignored = initGit()) + { + return true; + } + catch ( Exception e ) + { + return false; + } + } + + private static void abortMerge( Git git ) throws Exception + { + Repository repository = git.getRepository(); + repository.writeMergeCommitMsg( null ); + repository.writeMergeHeads( null ); + git.reset().setMode( ResetCommand.ResetType.HARD ).call(); + } + + public void resetToRemoteBranch() throws Exception + { + try (Git git = initGit()) + { + Repository repository = git.getRepository(); + String remoteTrackingBranch = new BranchConfig( repository.getConfig(), repository.getBranch() ).getRemoteTrackingBranch(); + git.reset().setMode( ResetCommand.ResetType.HARD ).setRef( remoteTrackingBranch ).call(); + reloadFromDisc(); + } + } + + private void ensureClean( Git git, String title ) throws GitAPIException + { + windowManager.getProjectManager().saveProject(); + boolean clean = isClean( git ); + if ( !clean ) + throw new MastodonGitException( "There are uncommitted changes. Please add a save point before " + title + "." ); + } + + public boolean isClean() throws Exception + { + windowManager.getProjectManager().saveProject(); + try (Git git = initGit()) + { + return isClean( git ); + } + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/NotificationDialog.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/NotificationDialog.java new file mode 100644 index 00000000..187a9a1d --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/NotificationDialog.java @@ -0,0 +1,21 @@ +package org.mastodon.mamut.tomancak.collaboration; + +import javax.swing.JDialog; +import javax.swing.JOptionPane; +import javax.swing.Timer; + +/** + * A dialog that shows a message for a short time and then disappears. + */ +public class NotificationDialog +{ + + public static void show( String title, String message ) + { + JOptionPane pane = new JOptionPane( message, JOptionPane.PLAIN_MESSAGE ); + JDialog dialog = pane.createDialog( null, title ); + dialog.setModal( false ); + dialog.setVisible( true ); + new Timer( 1500, ignore -> dialog.dispose() ).start(); + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/AbstractCancellable.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/AbstractCancellable.java new file mode 100644 index 00000000..92af522a --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/AbstractCancellable.java @@ -0,0 +1,30 @@ +package org.mastodon.mamut.tomancak.collaboration.commands; + +import org.scijava.Cancelable; + +public class AbstractCancellable implements Cancelable +{ + + private boolean canceled = false; + + private String reason = null; + + @Override + public boolean isCanceled() + { + return canceled; + } + + @Override + public void cancel( String reason ) + { + this.canceled = true; + this.reason = reason; + } + + @Override + public String getCancelReason() + { + return reason; + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/MastodonGitCloneRepository.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/MastodonGitCloneRepository.java new file mode 100644 index 00000000..ad58da66 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/MastodonGitCloneRepository.java @@ -0,0 +1,67 @@ +package org.mastodon.mamut.tomancak.collaboration.commands; + +import java.io.File; + +import org.mastodon.mamut.tomancak.collaboration.ErrorDialog; +import org.mastodon.mamut.tomancak.collaboration.MastodonGitRepository; +import org.scijava.Context; +import org.scijava.command.Command; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; + +// TODOs: +// - warn if parentDirectory already exists +// - warn if repositoryName already exists and the corresponding directory is not empty +// - fill repositoryName with a default value based on the repositoryURL +@Plugin( type = Command.class, + label = "Mastodon Git - Download Shared Project (clone)", + menuPath = "Plugins > Mastodon Collaborative (Git) > Download Shared Project" ) +public class MastodonGitCloneRepository extends AbstractCancellable implements Command +{ + @Parameter + private Context context; + + @Parameter( label = "URL on github or gitlab", description = URL_DESCRIPTION ) + private String repositoryURL; + + private static final String URL_DESCRIPTION = "" + + "Here are two examples of valid URLs:
" + + "" + + ""; + + @Parameter( label = "Directory, to store the project:", style = "directory", description = DIRECTORY_DESCRIPTION ) + private File directory; + + private static final String DIRECTORY_DESCRIPTION = "" + + "A copy of the shared project will be downloaded to your computer.
" + + "Please select a directory where to store it.
" + + "The directory should be empty, or select \"Create new subdirectory\"." + + ""; + + @Parameter( label = "Create new subdirectory", required = false, description = CREATE_SUBDIRECTORY_DESCRIPTION ) + private boolean createSubdirectory = false; + + private static final String CREATE_SUBDIRECTORY_DESCRIPTION = "" + + "If selected, a new subdirectory will be created in the selected directory.
" + + "The name of the subdirectory will be the name of the repository." + + ""; + + @Override + public void run() + { + try + { + directory = NewDirectoryUtils.createRepositoryDirectory( createSubdirectory, directory, repositoryURL ); + MastodonGitRepository.cloneRepository( repositoryURL, directory ); + MastodonGitRepository.openProjectInRepository( context, directory ); + } + catch ( final Exception e ) + { + ErrorDialog.showErrorMessage( "Download Shared Project (Clone)", e ); + } + } + +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/MastodonGitCreateRepository.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/MastodonGitCreateRepository.java new file mode 100644 index 00000000..c8014269 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/MastodonGitCreateRepository.java @@ -0,0 +1,82 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2022 Tobias Pietzsch, Jean-Yves Tinevez + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.tomancak.collaboration.commands; + +import java.io.File; + +import org.mastodon.mamut.tomancak.collaboration.ErrorDialog; +import org.scijava.ItemVisibility; +import org.scijava.command.Command; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; + +@Plugin( type = Command.class, + label = "Share Current Project via GitHub or GitLab", + visible = false ) +public class MastodonGitCreateRepository extends AbstractCancellable implements Command +{ + @Parameter( visibility = ItemVisibility.MESSAGE ) + private final String text = "" + + "

Share Project

" + + "

Share the current project on github or gitlab.

" + + "

Go to github.com or to ure institute's gitlab and create a new repository.

" + + "

Then copy the URL of the repository and paste it below.

" + + "

A copy of will be created in the directory you specify, and then uploaded to the specified URL.

"; + + @Parameter + private Callback callback; + + @Parameter( label = "URL on github or gitlab" ) + private String repositoryURL; + + @Parameter( label = "Directory to contain the repository", style = "directory" ) + private File directory; + + @Parameter( label = "Create new subdirectory", required = false ) + private boolean createSubdirectory = false; + + @Override + public void run() + { + try + { + directory = NewDirectoryUtils.createRepositoryDirectory( createSubdirectory, directory, repositoryURL ); + callback.run( directory, repositoryURL ); + } + catch ( Exception e ) + { + ErrorDialog.showErrorMessage( "Share Project", e ); + } + } + + public interface Callback + { + void run( File directory, String repositoryURL ) throws Exception; + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/MastodonGitNewBranch.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/MastodonGitNewBranch.java new file mode 100644 index 00000000..c1a9d0aa --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/MastodonGitNewBranch.java @@ -0,0 +1,31 @@ +package org.mastodon.mamut.tomancak.collaboration.commands; + +import org.mastodon.mamut.tomancak.collaboration.ErrorDialog; +import org.mastodon.mamut.tomancak.collaboration.MastodonGitRepository; +import org.scijava.command.Command; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; + +@Plugin( type = Command.class, label = "Create New Branch", visible = false ) +public class MastodonGitNewBranch extends AbstractCancellable implements Command +{ + + @Parameter + private MastodonGitRepository repository; + + @Parameter( label = "Branch name", persist = false ) + private String branchName; + + @Override + public void run() + { + try + { + repository.createNewBranch( branchName ); + } + catch ( Exception e ) + { + ErrorDialog.showErrorMessage( "Create New Branch", e ); + } + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/MastodonGitSetAuthorCommand.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/MastodonGitSetAuthorCommand.java new file mode 100644 index 00000000..de3e178c --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/MastodonGitSetAuthorCommand.java @@ -0,0 +1,43 @@ +package org.mastodon.mamut.tomancak.collaboration.commands; + +import org.mastodon.mamut.tomancak.collaboration.settings.MastodonGitSettingsService; +import org.scijava.Initializable; +import org.scijava.ItemVisibility; +import org.scijava.command.Command; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; + +@Plugin( type = Command.class, label = "Set Author Name", visible = false ) +public class MastodonGitSetAuthorCommand extends AbstractCancellable implements Command, Initializable +{ + @Parameter + private MastodonGitSettingsService settings; + + @Parameter( visibility = ItemVisibility.MESSAGE ) + private final String description = "" + + "The name and email that you specify below
" + + "are used to identify you as the author of the
" + + "changes you make to the shared project.

" + + "Name and email are likely to become publicly visible on the internet.
" + + "You may use a nickname and dummy email address if you wish."; + + @Parameter( label = "Author name", persist = false ) + private String authorName; + + @Parameter( label = "Author email", persist = false ) + private String authorEmail = "noreply@example.com"; + + @Override + public void initialize() + { + authorName = settings.getAuthorName(); + authorEmail = settings.getAuthorEmail(); + } + + @Override + public void run() + { + settings.setAuthorName( authorName ); + settings.setAuthorEmail( authorEmail ); + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/NewDirectoryUtils.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/NewDirectoryUtils.java new file mode 100644 index 00000000..4489c4af --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/commands/NewDirectoryUtils.java @@ -0,0 +1,86 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2022 Tobias Pietzsch, Jean-Yves Tinevez + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.tomancak.collaboration.commands; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class NewDirectoryUtils +{ + + /** + * If {@code newSubdirectory} is true, create a new subdirectory in the + * given {@code directory}. The name of the subdirectory is extracted from + * the {@code repositoryURL}. + *

+ * If {@code newSubdirectory} is false, return the {@code directory} as it is. + */ + static File createRepositoryDirectory( boolean newSubdirectory, File directory, String repositoryURL ) throws IOException + { + if ( !newSubdirectory ) + return directory; + + return createSubdirectory( directory, extractRepositoryName( repositoryURL ) ); + } + + static File createSubdirectory( File parentDirectory, String repositoryName ) throws IOException + { + File directory = new File( parentDirectory, repositoryName ); + if ( directory.isDirectory() ) + { + if ( isEmptyDirectory( directory ) ) + return directory; + else + throw new IOException( "Directory already exists but is not empty: " + directory + + "\nPlease move or delete the directory and try again." ); + } + Files.createDirectory( directory.toPath() ); + return directory; + } + + private static boolean isEmptyDirectory( File directory ) + { + String[] list = directory.list(); + if ( list == null ) // not a directory + return false; + return list.length == 0; + } + + static String extractRepositoryName( String repositoryURL ) + { + Pattern pattern = Pattern.compile( "/([\\w-]+)(\\.git|/)?$" ); + Matcher matcher = pattern.matcher( repositoryURL ); + if ( matcher.find() ) + return matcher.group( 1 ); + throw new IllegalArgumentException( "Could not extract repository name from URL:" + repositoryURL ); + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/credentials/PersistentCredentials.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/credentials/PersistentCredentials.java new file mode 100644 index 00000000..33e14096 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/credentials/PersistentCredentials.java @@ -0,0 +1,81 @@ +package org.mastodon.mamut.tomancak.collaboration.credentials; + +import java.util.concurrent.CancellationException; + +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JTextField; + +import net.miginfocom.swing.MigLayout; + +import org.apache.commons.lang3.tuple.Pair; +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.URIish; + +/** + * This class is meant to be used with JGIT for a comfortable way to ask the + * user for credentials. The user is only asked once for username and password. + * The credentials are stored in memory and reused for all subsequent requests. + * It also tries to detect if the user entered wrong credentials, + * and asks for new credentials. + *

+ * WARNING: JGIT expects a {@link CredentialsProvider} to return false if the + * user cancels for example a username/password dialog. (see {@link CredentialsProvider#get}) + * This class behaves differently: It throws a {@link CancellationException} + * instead. This difference is on purpose. The CancellationException + * better describes the situation and is easier to handle, than the + * {@link org.eclipse.jgit.errors.TransportException} that JGIT throws. + */ +public class PersistentCredentials +{ + + private String username = null; + + private String password = null; + + /** + * This method simply returns the username and password if they are already + * known. It asks the user for credentials if they are not known yet, or if + * the previous attempt to use the credentials failed. + */ + private synchronized Pair< String, String > getUsernameAndPassword( URIish uri, boolean authenticationFailure ) + { + boolean missingCredentials = password == null || username == null; + if ( missingCredentials || authenticationFailure ) + queryPassword( uri.toString(), authenticationFailure ); + return Pair.of( username, password ); + } + + private void queryPassword( String url, boolean previousAuthenticationFailed ) + { + JTextField usernameField = new JTextField( 20 ); + JPasswordField passwordField = new JPasswordField( 20 ); + + final JPanel panel = new JPanel(); + panel.setLayout( new MigLayout( "insets dialog" ) ); + panel.add( new JLabel( "Please enter your credentials for the Git repository:" ), "span, wrap" ); + panel.add( new JLabel( url ), "span, wrap, gapbottom unrelated" ); + panel.add( new JLabel( "username" ) ); + panel.add( usernameField, "wrap" ); + panel.add( new JLabel( "password" ) ); + panel.add( passwordField, "wrap" ); + if ( previousAuthenticationFailed ) + panel.add( new JLabel( "(Authentication failed. Please try again!)" ), "span, wrap" ); + boolean ok = JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog( null, + panel, "Authentication for Git Repository", + JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE ); + if ( !ok ) + throw new CancellationException( "User cancelled username & password dialog." ); + + username = usernameField.getText(); + password = new String( passwordField.getPassword() ); + } + + public CredentialsProvider getSingleUseCredentialsProvider() + { + return new SingleUseCredentialsProvider( this::getUsernameAndPassword ); + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/credentials/SingleUseCredentialsProvider.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/credentials/SingleUseCredentialsProvider.java new file mode 100644 index 00000000..754a9ae5 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/credentials/SingleUseCredentialsProvider.java @@ -0,0 +1,97 @@ +package org.mastodon.mamut.tomancak.collaboration.credentials; + +import org.apache.commons.lang3.tuple.Pair; +import org.eclipse.jgit.errors.UnsupportedCredentialItem; +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.URIish; + +/** + * The JGIT api does not tell a CredentialsProvider if the credentials + * where correct. It simply asks for them again if they were wrong. + *

+ * We can exploit this behavior by counting the number of times the + * CredentialsProvider was asked for credentials. If it was asked more than + * once, we assume that the credentials were wrong. + *

+ * This only works if the CredentialsProvider is only used once. + */ +class SingleUseCredentialsProvider extends CredentialsProvider +{ + private final Callback callback; + + private int counter = 0; + + public SingleUseCredentialsProvider( Callback callback ) + { + this.callback = callback; + } + + @Override + public boolean isInteractive() + { + return true; + } + + @Override + public boolean supports( CredentialItem... items ) + { + for ( CredentialItem item : items ) + if ( !isUsernameOrPassword( item ) ) + return false; + return true; + } + + private boolean isUsernameOrPassword( CredentialItem item ) + { + return ( item instanceof CredentialItem.Username ) || ( item instanceof CredentialItem.Password ); + } + + @Override + public boolean get( URIish uri, CredentialItem... items ) throws UnsupportedCredentialItem + { + if ( !supports( items ) ) + throw new UnsupportedCredentialItem( uri, "" ); + counter++; + boolean previousAuthenticationFailed = counter > 1; + Pair< String, String > usernameAndPassword = callback.getUsernameAndPassword( uri, previousAuthenticationFailed ); + if ( usernameAndPassword == null ) + return false; + fillUsernameAndPassword( items, usernameAndPassword.getLeft(), usernameAndPassword.getRight() ); + return true; + } + + private void fillUsernameAndPassword( CredentialItem[] items, String username, String password ) + { + for ( CredentialItem item : items ) + fillItem( item, username, password ); + } + + private void fillItem( CredentialItem item, String username, String password ) + { + if ( item instanceof CredentialItem.Username ) + ( ( CredentialItem.Username ) item ).setValue( username ); + else if ( item instanceof CredentialItem.Password ) + ( ( CredentialItem.Password ) item ).setValue( password.toCharArray() ); + } + + /** + * Callback interface for {@link SingleUseCredentialsProvider}. + */ + interface Callback + { + + /** + * The {@link SingleUseCredentialsProvider} calls this method to get + * username and password for the given {@link URIish}. + * + * @param uri the URI for which credentials are requested. + * @param authenticationFailure true if the SingleUseCredentialsProvider + * thinks that the credentials were wrong. + * @return username and password or null if the user canceled the + * request. + * @see SingleUseCredentialsProvider + */ + Pair< String, String > getUsernameAndPassword( URIish uri, boolean authenticationFailure ); + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/exceptions/GraphMergeConflictException.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/exceptions/GraphMergeConflictException.java new file mode 100644 index 00000000..efd81f50 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/exceptions/GraphMergeConflictException.java @@ -0,0 +1,9 @@ +package org.mastodon.mamut.tomancak.collaboration.exceptions; + +public class GraphMergeConflictException extends GraphMergeException +{ + public GraphMergeConflictException() + { + super( "There are conflicting changes in the two versions of the ModelGraph." ); + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/exceptions/GraphMergeException.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/exceptions/GraphMergeException.java new file mode 100644 index 00000000..524c1319 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/exceptions/GraphMergeException.java @@ -0,0 +1,15 @@ +package org.mastodon.mamut.tomancak.collaboration.exceptions; + +public class GraphMergeException extends RuntimeException +{ + + public GraphMergeException( final String message ) + { + super( message ); + } + + public GraphMergeException( String message, final Throwable cause ) + { + super( message, cause ); + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/exceptions/MastodonGitException.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/exceptions/MastodonGitException.java new file mode 100644 index 00000000..6cc64617 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/exceptions/MastodonGitException.java @@ -0,0 +1,20 @@ +package org.mastodon.mamut.tomancak.collaboration.exceptions; + +/** + * Exception class that is used in + * {@link org.mastodon.mamut.tomancak.collaboration.MastodonGitRepository}. + * The exception messages should be user-friendly. Such that they can be + * printed to the user. + */ +public class MastodonGitException extends RuntimeException +{ + public MastodonGitException( final String message ) + { + super( message ); + } + + public MastodonGitException( final String message, final Throwable cause ) + { + super( message, cause ); + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/settings/DefaultMastodonGitSettingsService.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/settings/DefaultMastodonGitSettingsService.java new file mode 100644 index 00000000..777f46ae --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/settings/DefaultMastodonGitSettingsService.java @@ -0,0 +1,66 @@ +package org.mastodon.mamut.tomancak.collaboration.settings; + +import org.eclipse.jgit.lib.PersonIdent; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.prefs.PrefService; +import org.scijava.service.AbstractService; +import org.scijava.service.Service; + +@Plugin( type = Service.class ) +public class DefaultMastodonGitSettingsService extends AbstractService implements MastodonGitSettingsService +{ + + @Parameter + private PrefService prefService; + + private String authorName; + + private String authorEmail; + + @Override + public void initialize() + { + super.initialize(); + authorName = prefService.get( DefaultMastodonGitSettingsService.class, "author.name", null ); + authorEmail = prefService.get( DefaultMastodonGitSettingsService.class, "author.email", null ); + } + + @Override + public boolean isAuthorSpecified() + { + return authorName != null && authorEmail != null; + } + + @Override + public void setAuthorName( String name ) + { + this.authorName = name; + prefService.put( DefaultMastodonGitSettingsService.class, "author.name", name ); + } + + @Override + public void setAuthorEmail( String email ) + { + this.authorEmail = email; + prefService.put( DefaultMastodonGitSettingsService.class, "author.email", email ); + } + + @Override + public String getAuthorName() + { + return authorName; + } + + @Override + public String getAuthorEmail() + { + return authorEmail; + } + + @Override + public PersonIdent getPersonIdent() + { + return new PersonIdent( authorName, authorEmail ); + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/settings/MastodonGitSettingsService.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/settings/MastodonGitSettingsService.java new file mode 100644 index 00000000..f77a7b99 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/settings/MastodonGitSettingsService.java @@ -0,0 +1,21 @@ +package org.mastodon.mamut.tomancak.collaboration.settings; + +import net.imagej.ImageJService; + +import org.eclipse.jgit.lib.PersonIdent; +import org.scijava.service.Service; + +public interface MastodonGitSettingsService extends ImageJService +{ + boolean isAuthorSpecified(); + + void setAuthorName( String name ); + + void setAuthorEmail( String email ); + + String getAuthorName(); + + String getAuthorEmail(); + + PersonIdent getPersonIdent(); +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/utils/ActionDescriptions.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/utils/ActionDescriptions.java new file mode 100644 index 00000000..9f46652c --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/utils/ActionDescriptions.java @@ -0,0 +1,94 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2022 Tobias Pietzsch, Jean-Yves Tinevez + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.tomancak.collaboration.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * This class contains a list of details about actions that a plugin can + * perform. It should be used together with {@link BasicMamutPlugin} and + * {@link BasicDescriptionProvider}. + */ +public class ActionDescriptions< T > +{ + + private final Class< T > pluginClass; + + private final List< Entry< ? > > entries = new ArrayList<>(); + + public ActionDescriptions( Class< T > pluginClass ) + { + this.pluginClass = pluginClass; + } + + public final String addActionDescription( String key, String menuText, String description, Consumer< T > action ) + { + return addActionDescription( key, menuText, description, action, "not mapped" ); + } + + public final String addActionDescription( String key, String menuText, String description, Consumer< T > action, String... keyStrokes ) + { + entries.add( new Entry<>( key, menuText, keyStrokes, description, action ) ); + return key; + } + + public Class< T > getPluginClass() + { + return pluginClass; + } + + public List< Entry< ? > > getEntries() + { + return entries; + } + + public static class Entry< T > + { + public final String key; + + public final String menuEntry; + + public final String[] shortCuts; + + public final String description; + + public final Consumer< T > action; + + public Entry( String key, String menuEntry, String[] shortCuts, String description, Consumer< T > action ) + { + this.key = key; + this.menuEntry = menuEntry; + this.shortCuts = shortCuts; + this.description = description; + this.action = action; + } + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/utils/BasicDescriptionProvider.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/utils/BasicDescriptionProvider.java new file mode 100644 index 00000000..325aea79 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/utils/BasicDescriptionProvider.java @@ -0,0 +1,57 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2022 Tobias Pietzsch, Jean-Yves Tinevez + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.tomancak.collaboration.utils; + +import org.mastodon.ui.keymap.CommandDescriptionProvider; +import org.mastodon.ui.keymap.CommandDescriptions; + +/** + * This class simplifies the task of implementing a {@link CommandDescriptionProvider}. + *

+ * It provides the description of the actions listed in the {@link ActionDescriptions} object. + */ +public abstract class BasicDescriptionProvider extends CommandDescriptionProvider +{ + + private final ActionDescriptions< ? > actionDescriptions; + + protected BasicDescriptionProvider( ActionDescriptions< ? > actionDescriptions, String... contexts ) + { + super( contexts ); + this.actionDescriptions = actionDescriptions; + } + + @Override + public void getCommandDescriptions( CommandDescriptions descriptions ) + { + for ( ActionDescriptions.Entry< ? > entry : actionDescriptions.getEntries() ) + descriptions.add( entry.key, entry.shortCuts, entry.description ); + } + +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/utils/BasicMamutPlugin.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/utils/BasicMamutPlugin.java new file mode 100644 index 00000000..749215cf --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/utils/BasicMamutPlugin.java @@ -0,0 +1,141 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2022 Tobias Pietzsch, Jean-Yves Tinevez + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.tomancak.collaboration.utils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.imglib2.util.Cast; + +import org.mastodon.app.ui.ViewMenuBuilder; +import org.mastodon.mamut.MamutAppModel; +import org.mastodon.mamut.WindowManager; +import org.mastodon.mamut.plugin.MamutPlugin; +import org.mastodon.mamut.plugin.MamutPluginAppModel; +import org.mastodon.mamut.tomancak.collaboration.MastodonGitController; +import org.scijava.ui.behaviour.util.AbstractNamedAction; +import org.scijava.ui.behaviour.util.Actions; +import org.scijava.ui.behaviour.util.RunnableAction; + +/** + * A class that simplifies the creation of a {@link MamutPlugin}. + * See {@link MastodonGitController} for + * usage example. + */ +public abstract class BasicMamutPlugin implements MamutPlugin +{ + + private final List< ActionDescriptions.Entry< ? > > actionDescriptions; + + private final Map< String, String > menuTexts = new HashMap<>(); + + private final Map< String, AbstractNamedAction > actions = new HashMap<>(); + + private final List< ViewMenuBuilder.MenuItem > menuItems = new ArrayList<>(); + + private MamutAppModel appModel; + + private WindowManager windowManager; + + protected < T > BasicMamutPlugin( ActionDescriptions< T > description ) + { + if ( !this.getClass().equals( description.getPluginClass() ) ) + throw new IllegalArgumentException( "Plugin class mismatch." ); + actionDescriptions = description.getEntries(); + for ( ActionDescriptions.Entry< ? > entry : actionDescriptions ) + { + menuTexts.put( entry.key, extractMenuText( entry.menuEntry ) ); + menuItems.add( initMenuItem( entry.key, entry.menuEntry ) ); + actions.put( entry.key, new RunnableAction( entry.key, () -> entry.action.accept( Cast.unchecked( this ) ) ) ); + } + } + + public void setActionEnabled( String key, boolean enabled ) + { + actions.get( key ).setEnabled( enabled ); + } + + private String extractMenuText( String menuEntry ) + { + // From menuEntry, extract the last part, which is the menu item name. + final String[] parts = menuEntry.split( ">" ); + return parts[ parts.length - 1 ].trim(); + } + + private ViewMenuBuilder.MenuItem initMenuItem( String key, String menuEntry ) + { + final String[] parts = menuEntry.split( ">" ); + ViewMenuBuilder.MenuItem item = ViewMenuBuilder.item( key ); + for ( int i = parts.length - 2; i >= 0; i-- ) + item = ViewMenuBuilder.menu( parts[ i ].trim(), item ); + return item; + } + + @Override + public void setAppPluginModel( MamutPluginAppModel appPluginModel ) + { + appModel = appPluginModel.getAppModel(); + windowManager = appPluginModel.getWindowManager(); + actions.forEach( ( key, action ) -> action.setEnabled( appModel != null ) ); + initialize(); + } + + protected void initialize() {} + + @Override + public Map< String, String > getMenuTexts() + { + return menuTexts; + } + + @Override + public List< ViewMenuBuilder.MenuItem > getMenuItems() + { + return menuItems; + } + + @Override + public void installGlobalActions( Actions pluginActions ) + { + for ( ActionDescriptions.Entry< ? > entry : actionDescriptions ) + pluginActions.namedAction( actions.get( entry.key ), entry.shortCuts ); + } + + protected MamutAppModel getAppModel() + { + return appModel; + } + + protected WindowManager getWindowManager() + { + return windowManager; + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/collaboration/utils/ConflictUtils.java b/src/main/java/org/mastodon/mamut/tomancak/collaboration/utils/ConflictUtils.java new file mode 100644 index 00000000..e2fe1492 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/tomancak/collaboration/utils/ConflictUtils.java @@ -0,0 +1,64 @@ +package org.mastodon.mamut.tomancak.collaboration.utils; + +import java.util.List; + +import org.mastodon.mamut.model.Link; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.model.Spot; +import org.mastodon.model.tag.TagSetModel; +import org.mastodon.model.tag.TagSetStructure; + +public class ConflictUtils +{ + private ConflictUtils() + { + // prevent from instantiation + } + + public static boolean hasConflict( Model model ) + { + TagSetModel< Spot, Link > tagSetModel = model.getTagSetModel(); + return !isTagSetEmpty( tagSetModel, "Merge Conflict", "Conflict" ) || + !isTagSetEmpty( tagSetModel, "Merge Conflict (Tags)", "Tag Conflict" ) || + !isTagSetEmpty( tagSetModel, "Merge Conflict (Labels)", "Label Conflict" ); + } + + public static void removeMergeConflictTagSets( Model model ) + { + TagSetModel< Spot, Link > tagSetModel = model.getTagSetModel(); + TagSetStructure original = tagSetModel.getTagSetStructure(); + TagSetStructure replacement = new TagSetStructure(); + replacement.set( original ); + for ( TagSetStructure.TagSet tagSet : original.getTagSets() ) + if ( isConflictTagSetName( tagSet.getName() ) ) + replacement.remove( tagSet ); + tagSetModel.setTagSetStructure( replacement ); + } + + private static boolean isConflictTagSetName( String name ) + { + return name.equals( "Merge Conflict" ) || + name.equals( "Merge Conflict (Tags)" ) || + name.equals( "Merge Conflict (Labels)" ) || + name.equals( "Merge Source A" ) || + name.equals( "Merge Source B" ) || + name.startsWith( "((A)) " ) || + name.startsWith( "((B)) " ); + } + + /** + * Returns true if the given tag set is empty or if it does not exist. + */ + private static boolean isTagSetEmpty( TagSetModel< Spot, Link > tagSetModel, String tagSetName, String tagLabel ) + { + TagSetStructure tagSetStructure = tagSetModel.getTagSetStructure(); + List< TagSetStructure.TagSet > tagSets = tagSetStructure.getTagSets(); + TagSetStructure.TagSet tagSet = tagSets.stream().filter( ts -> tagSetName.equals( ts.getName() ) ).findFirst().orElse( null ); + if ( tagSet == null ) + return true; + TagSetStructure.Tag tag = tagSet.getTags().stream().filter( t -> tagLabel.equals( t.label() ) ).findFirst().orElse( null ); + if ( tag == null ) + return true; + return tagSetModel.getVertexTags().getTaggedWith( tag ).isEmpty() && tagSetModel.getEdgeTags().getTaggedWith( tag ).isEmpty(); + } +} diff --git a/src/main/java/org/mastodon/mamut/tomancak/merging/Dataset.java b/src/main/java/org/mastodon/mamut/tomancak/merging/Dataset.java index 0155e8b0..cc08792e 100644 --- a/src/main/java/org/mastodon/mamut/tomancak/merging/Dataset.java +++ b/src/main/java/org/mastodon/mamut/tomancak/merging/Dataset.java @@ -28,9 +28,6 @@ */ package org.mastodon.mamut.tomancak.merging; -import static org.mastodon.mamut.tomancak.merging.MergingUtil.getMaxNonEmptyTimepoint; -import static org.mastodon.mamut.tomancak.merging.MergingUtil.getNumTimepoints; - import java.io.IOException; import org.mastodon.mamut.model.Model; @@ -47,8 +44,6 @@ public class Dataset { private final MamutProject project; - private final int numTimepoints; - private final Model model; private final int maxNonEmptyTimepoint; @@ -56,17 +51,23 @@ public class Dataset public Dataset( final String path ) throws IOException { project = new MamutProjectIO().load( path ); - numTimepoints = getNumTimepoints( project ); model = new Model(); try (final MamutProject.ProjectReader reader = project.openForReading()) { model.loadRaw( reader ); } - maxNonEmptyTimepoint = getMaxNonEmptyTimepoint( model, numTimepoints ); - + maxNonEmptyTimepoint = maxTimepoint( model ); verify(); } + private int maxTimepoint( Model model ) + { + int max = 0; + for ( Spot spot : model.getGraph().vertices() ) + max = Math.max( max, spot.getTimepoint() ); + return max; + } + public Model model() { return model; diff --git a/src/main/java/org/mastodon/mamut/tomancak/merging/MergingUtil.java b/src/main/java/org/mastodon/mamut/tomancak/merging/MergingUtil.java index 9025434a..0653896b 100644 --- a/src/main/java/org/mastodon/mamut/tomancak/merging/MergingUtil.java +++ b/src/main/java/org/mastodon/mamut/tomancak/merging/MergingUtil.java @@ -28,18 +28,9 @@ */ package org.mastodon.mamut.tomancak.merging; -import org.mastodon.mamut.model.Model; import org.mastodon.mamut.model.Spot; import org.mastodon.mamut.model.SpotPool; -import org.mastodon.mamut.project.MamutProject; import org.mastodon.properties.ObjPropertyMap; -import org.mastodon.spatial.SpatialIndex; -import org.mastodon.spatial.SpatioTemporalIndex; -import org.mastodon.util.DummySpimData; - -import bdv.spimdata.SpimDataMinimal; -import bdv.spimdata.XmlIoSpimDataMinimal; -import mpicbg.spim.data.SpimDataException; public class MergingUtil { @@ -58,60 +49,4 @@ public static boolean hasLabel( final Spot spot ) final ObjPropertyMap< Spot, String > labels = ( ObjPropertyMap< Spot, String > ) pool.labelProperty(); return labels.isSet( spot ); } - - /** - * Returns number of time-points in {@code project}. To to that, loads - * {@code spimdata} for {@code project}. - * - * @param project - * the project. - * @return the number of time-points in the project. - */ - public static int getNumTimepoints( final MamutProject project ) - { - try - { - final String spimDataXmlFilename = project.getDatasetXmlFile().getAbsolutePath(); - SpimDataMinimal spimData = DummySpimData.tryCreate( project.getDatasetXmlFile().getName() ); - if ( spimData == null ) - spimData = new XmlIoSpimDataMinimal().load( spimDataXmlFilename ); - return spimData.getSequenceDescription().getTimePoints().size(); - } - catch ( final SpimDataException e ) - { - throw new RuntimeException( e ); - } - } - - // Helper: max timepoint that has at least one spot - - /** - * Returns the largest timepoint (index) where model has a least one spot. - * - * @param model - * the model. - * @param numTimepoints - * the number of time-points in the model. - * @return the largest timepoint (index) where model has a least one spot. - */ - public static int getMaxNonEmptyTimepoint( final Model model, final int numTimepoints ) - { - int maxNonEmptyTimepoint = 0; - final SpatioTemporalIndex< Spot > spatioTemporalIndex = model.getSpatioTemporalIndex(); - spatioTemporalIndex.readLock().lock(); - try - { - for ( int t = 0; t < numTimepoints; ++t ) - { - final SpatialIndex< Spot > index = spatioTemporalIndex.getSpatialIndex( t ); - if ( index.size() > 0 ) - maxNonEmptyTimepoint = t; - } - } - finally - { - spatioTemporalIndex.readLock().unlock(); - } - return maxNonEmptyTimepoint; - } } diff --git a/src/test/java/org/mastodon/mamut/tomancak/collaboration/CommitGraphExample.java b/src/test/java/org/mastodon/mamut/tomancak/collaboration/CommitGraphExample.java new file mode 100644 index 00000000..12e09935 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/tomancak/collaboration/CommitGraphExample.java @@ -0,0 +1,68 @@ +/*- + * #%L + * mastodon-tomancak + * %% + * Copyright (C) 2018 - 2022 Tobias Pietzsch + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.tomancak.collaboration; + +import java.awt.BorderLayout; +import java.io.File; +import java.io.IOException; + +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JScrollPane; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.awtui.CommitGraphPane; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revplot.PlotWalk; + +// Inspired by https://git.eclipse.org/r/plugins/gitiles/jgit/jgit/+/84022ac9de54ded6f04ca196264a8f2370e9da9a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Glog.java +public class CommitGraphExample +{ + public static void main( String... args ) throws IOException, GitAPIException + { + Git git = Git.open( new File( "/home/arzt/tmp/2/mgit-test" ) ); + Repository repository = git.getRepository(); + PlotWalk plotWalk = new PlotWalk( repository ); + plotWalk.markStart( plotWalk.parseCommit( repository.resolve( "HEAD" ) ) ); + JFrame frame = new JFrame( "Commit Graph!" ); + CommitGraphPane comp = new CommitGraphPane(); + comp.getCommitList().source( plotWalk ); + comp.getCommitList().fillTo( Integer.MAX_VALUE ); + JLabel label = new JLabel(); + comp.getSelectionModel().addListSelectionListener( e -> { + int rowIndex = comp.getSelectedRow(); + label.setText( comp.getCommitList().get( rowIndex ).getId().toString() ); + } ); + frame.add( new JScrollPane( comp ) ); + frame.add( label, BorderLayout.PAGE_END ); + frame.setSize( 600, 600 ); + frame.setVisible( true ); + } +} diff --git a/src/test/java/org/mastodon/mamut/tomancak/collaboration/CommitMessageDialogDemo.java b/src/test/java/org/mastodon/mamut/tomancak/collaboration/CommitMessageDialogDemo.java new file mode 100644 index 00000000..fe4303be --- /dev/null +++ b/src/test/java/org/mastodon/mamut/tomancak/collaboration/CommitMessageDialogDemo.java @@ -0,0 +1,9 @@ +package org.mastodon.mamut.tomancak.collaboration; + +public class CommitMessageDialogDemo +{ + public static void main( String... args ) + { + System.out.println( CommitMessageDialog.showDialog() ); + } +} diff --git a/src/test/java/org/mastodon/mamut/tomancak/collaboration/ErrorDialogDemo.java b/src/test/java/org/mastodon/mamut/tomancak/collaboration/ErrorDialogDemo.java new file mode 100644 index 00000000..56f702fa --- /dev/null +++ b/src/test/java/org/mastodon/mamut/tomancak/collaboration/ErrorDialogDemo.java @@ -0,0 +1,16 @@ +package org.mastodon.mamut.tomancak.collaboration; + +public class ErrorDialogDemo +{ + public static void main( String... args ) + { + try + { + throw new IllegalArgumentException( "Test exception" ); + } + catch ( Exception e ) + { + ErrorDialog.showErrorMessage( "Title", e ); + } + } +} diff --git a/src/test/java/org/mastodon/mamut/tomancak/collaboration/MastodonGitRepositoryDemo.java b/src/test/java/org/mastodon/mamut/tomancak/collaboration/MastodonGitRepositoryDemo.java new file mode 100644 index 00000000..ddfd773c --- /dev/null +++ b/src/test/java/org/mastodon/mamut/tomancak/collaboration/MastodonGitRepositoryDemo.java @@ -0,0 +1,25 @@ +package org.mastodon.mamut.tomancak.collaboration; + +import java.io.File; + +import org.scijava.Context; + +public class MastodonGitRepositoryDemo +{ + public static void main( String... args ) throws Exception + { + String projectPath = "/home/arzt/devel/mastodon/mastodon/src/test/resources/org/mastodon/mamut/examples/tiny/tiny-project.mastodon"; + String repositoryName = "mgit-test"; + String repositoryURL = "git@github.com:maarzt/mgit-test.git"; + File parentDirectory = new File( "/home/arzt/tmp/" ); + +// Context context = new Context(); +// WindowManager windowManager = new WindowManager( context ); +// windowManager.getProjectManager().open( new MamutProjectIO().load( projectPath ) ); +// MastodonGitUtils.createRepositoryAndUpload( windowManager, parentDirectory, repositoryName, repositoryURL ); + +// MastodonGitUtils.cloneRepository( repositoryURL, new File( parentDirectory, "2/" ) ); + + MastodonGitRepository.openProjectInRepository( new Context(), new File( parentDirectory, "2/" ) ); + } +} diff --git a/src/test/java/org/mastodon/mamut/tomancak/collaboration/NotificationDialogDemo.java b/src/test/java/org/mastodon/mamut/tomancak/collaboration/NotificationDialogDemo.java new file mode 100644 index 00000000..5356bb4e --- /dev/null +++ b/src/test/java/org/mastodon/mamut/tomancak/collaboration/NotificationDialogDemo.java @@ -0,0 +1,9 @@ +package org.mastodon.mamut.tomancak.collaboration; + +public class NotificationDialogDemo +{ + public static void main( String... args ) + { + NotificationDialog.show( "Upload Changes", " Changes were uploaded successfully." ); + } +} diff --git a/src/test/java/org/mastodon/mamut/tomancak/collaboration/commands/NewDirectoryUtilsTest.java b/src/test/java/org/mastodon/mamut/tomancak/collaboration/commands/NewDirectoryUtilsTest.java new file mode 100644 index 00000000..8aa12321 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/tomancak/collaboration/commands/NewDirectoryUtilsTest.java @@ -0,0 +1,52 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2022 Tobias Pietzsch, Jean-Yves Tinevez + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.tomancak.collaboration.commands; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +public class NewDirectoryUtilsTest +{ + @Test + public void testExtractRepositoryName() + { + final String expected = "mastodon-tomancak"; + List< String > urls = Arrays.asList( + "https://github.com/mastodon-sc/mastodon-tomancak.git", + "https://github.com/mastodon-sc/mastodon-tomancak/", + "git@github.com:mastodon-sc/mastodon-tomancak.git" + ); + for ( String url : urls ) + assertEquals( expected, NewDirectoryUtils.extractRepositoryName( url ) ); + } +} diff --git a/src/test/java/org/mastodon/mamut/tomancak/collaboration/sshauthentication/CustomCredentialsProvider.java b/src/test/java/org/mastodon/mamut/tomancak/collaboration/sshauthentication/CustomCredentialsProvider.java new file mode 100644 index 00000000..5889a6b6 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/tomancak/collaboration/sshauthentication/CustomCredentialsProvider.java @@ -0,0 +1,63 @@ +package org.mastodon.mamut.tomancak.collaboration.sshauthentication; + +import java.util.Scanner; + +import org.eclipse.jgit.errors.UnsupportedCredentialItem; +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.URIish; + +/** + * A {@link CredentialsProvider} that allows to answer yes/no questions + * interactively on the command line. + */ +public class CustomCredentialsProvider extends CredentialsProvider +{ + + @Override + public boolean isInteractive() + { + return true; + } + + @Override + public boolean supports( CredentialItem... items ) + { + return true; + } + + @Override + public boolean get( URIish uri, CredentialItem... items ) throws UnsupportedCredentialItem + { + boolean ok = true; + for ( CredentialItem item : items ) + ok &= processItem( item ); + if ( !ok ) + throw new UnsupportedOperationException(); + return ok; + } + + private boolean processItem( CredentialItem item ) + { + if ( item instanceof CredentialItem.InformationalMessage ) + return processInformalMessage( ( CredentialItem.InformationalMessage ) item ); + if ( item instanceof CredentialItem.YesNoType ) + return processYesNo( ( CredentialItem.YesNoType ) item ); + return false; + } + + private boolean processInformalMessage( CredentialItem.InformationalMessage item ) + { + System.out.println( item.getPromptText() ); + return true; + } + + private boolean processYesNo( CredentialItem.YesNoType item ) + { + System.out.println( item.getPromptText() + " (yes/no)" ); + String line = new Scanner( System.in ).nextLine(); + item.setValue( "yes".equals( line ) ); + return true; + } +} + diff --git a/src/test/java/org/mastodon/mamut/tomancak/collaboration/sshauthentication/GenerateKeysExample.java b/src/test/java/org/mastodon/mamut/tomancak/collaboration/sshauthentication/GenerateKeysExample.java new file mode 100644 index 00000000..a6e31cea --- /dev/null +++ b/src/test/java/org/mastodon/mamut/tomancak/collaboration/sshauthentication/GenerateKeysExample.java @@ -0,0 +1,26 @@ +package org.mastodon.mamut.tomancak.collaboration.sshauthentication; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyEncryptionContext; +import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyPairResourceWriter; + +/** + * Example of how to generate a OpenSSH key pair and write it to stdout + * with the apache sshd library. + */ +public class GenerateKeysExample +{ + + public static void main( String[] args ) throws GeneralSecurityException, IOException + { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance( "RSA" ); + keyGen.initialize( 4 * 1024 ); + KeyPair keyPair = keyGen.genKeyPair(); + OpenSSHKeyPairResourceWriter.INSTANCE.writePrivateKey( keyPair, "user@example", new OpenSSHKeyEncryptionContext(), System.out ); + OpenSSHKeyPairResourceWriter.INSTANCE.writePublicKey( keyPair, "user@example", System.out ); + } +} diff --git a/src/test/java/org/mastodon/mamut/tomancak/collaboration/sshauthentication/JGitSshAuthenticationExample.java b/src/test/java/org/mastodon/mamut/tomancak/collaboration/sshauthentication/JGitSshAuthenticationExample.java new file mode 100644 index 00000000..ef4e652f --- /dev/null +++ b/src/test/java/org/mastodon/mamut/tomancak/collaboration/sshauthentication/JGitSshAuthenticationExample.java @@ -0,0 +1,41 @@ +package org.mastodon.mamut.tomancak.collaboration.sshauthentication; + +import java.io.File; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.SshTransport; +import org.eclipse.jgit.transport.sshd.JGitKeyCache; +import org.eclipse.jgit.transport.sshd.SshdSessionFactoryBuilder; + +/** + * Example of how to use JGit with a custom SSH key. + */ +public class JGitSshAuthenticationExample +{ + public static void main( String... args ) throws Exception + { + String projectPath = "/home/arzt/devel/mastodon/mastodon/src/test/resources/org/mastodon/mamut/examples/tiny/tiny-project.mastodon"; + String repositoryName = "mgit-test"; + String repositoryURL = "git@github.com:masgitoff/mastodon-test-dataset.git"; + File parentDirectory = new File( "/home/arzt/tmp/" ); + + SshSessionFactory sshSessionFactory = new SshdSessionFactoryBuilder() + .setPreferredAuthentications( "publickey" ) + .setHomeDirectory( new File( "/home/arzt/" ) ) + .setSshDirectory( new File( "/home/arzt/ssh-experiment" ) ) + .build( new JGitKeyCache() ); + try (Git git = Git.cloneRepository() + .setURI( repositoryURL ) + .setDirectory( new File( parentDirectory, "xyz" ) ) + .setCredentialsProvider( new CustomCredentialsProvider() ) + .setTransportConfigCallback( transport -> ( ( SshTransport ) transport ).setSshSessionFactory( sshSessionFactory ) ) + .call()) + { + git.push() + .setTransportConfigCallback( transport -> ( ( SshTransport ) transport ).setSshSessionFactory( sshSessionFactory ) ) + .setCredentialsProvider( new CustomCredentialsProvider() ) + .call(); + } + } +}