diff --git a/org.archicontribs.modelrepository.tests/src/org/archicontribs/modelrepository/grafico/GitExecutorTest.java b/org.archicontribs.modelrepository.tests/src/org/archicontribs/modelrepository/grafico/GitExecutorTest.java new file mode 100644 index 0000000..5636563 --- /dev/null +++ b/org.archicontribs.modelrepository.tests/src/org/archicontribs/modelrepository/grafico/GitExecutorTest.java @@ -0,0 +1,184 @@ +package org.archicontribs.modelrepository.grafico; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.logging.Logger; + +import org.junit.jupiter.api.Test; + +public class GitExecutorTest { + + private static final Logger LOGGER = Logger.getLogger(GitExecutorTest.class.getName()); + + private GitExecutor underTest; + + public GitExecutorTest() throws GitExecutionException { + underTest = new GitExecutor(TestData.GIT_PATH, TestData.GIT_REPO); + } + + @Test + public void canFindGitOnPath() throws IOException, InterruptedException { + Process process = Runtime.getRuntime().exec(new String[] { "which", "git" }); + assertEquals(0, process.waitFor()); + } + + @Test + public void canGitVersion() throws GitExecutionException { + GitExecutionResult res = underTest.version(); + LOGGER.finest(res.outputLine()); + assertEquals(0, res.exitCode()); + } + + @Test + public void canGitReset() throws GitExecutionException { + GitExecutionResult res = underTest.reset(true, TestData.GIT_HISTORICAL_COMMIT_ID); + LOGGER.finest(res.outputLine()); + assertEquals(0, res.exitCode()); + } + + @Test + public void canGitClean() throws GitExecutionException, IOException { + File f = new File(TestData.GIT_FOLDER, "cleanMe"); + assertTrue(f.createNewFile()); + assertTrue(f.exists()); + + GitExecutionResult res = underTest.clean(); + LOGGER.finest(res.outputLine()); + assertEquals(0, res.exitCode()); + + assertFalse(f.exists()); + } + + private void resetTestScenario() { + try { + canGitReset(); + canGitClean(); + } catch (GitExecutionException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void canGitFetch() throws GitExecutionException { + GitExecutionResult res = underTest.fetch(); + LOGGER.finest(res.outputLine()); + assertEquals(0, res.exitCode()); + } + + + @Test + public void canGitCommit() throws GitExecutionException { + resetTestScenario(); // arrange + + GitExecutionResult res = underTest.commit("empty commit", false, true, false); + LOGGER.finest(res.outputLine()); + assertEquals(0, res.exitCode()); + } + + @Test + public void canGitPull() throws GitExecutionException { + resetTestScenario(); // arrange + + GitExecutionResult res = underTest.pull(); // FF_ONLY + LOGGER.finest(res.outputLine()); + assertEquals(0, res.exitCode()); + } + + @Test + public void canGitPullDetectConflict() throws GitExecutionException { + resetTestScenario(); // arrange + + GitExecutionResult resCommit = underTest.commit("empty commit", false, true, false); + LOGGER.finest(resCommit.outputLine()); + + GitExecutionResult res = underTest.pull(); + LOGGER.finest(res.outputLine()); + assertEquals(128, res.exitCode()); + } + + @Test + public void canGitPullRebase() throws GitExecutionException { + resetTestScenario(); // arrange + + GitExecutionResult resCommit = underTest.commit("empty commit", false, true, false); + LOGGER.finest(resCommit.outputLine()); + + GitExecutionResult res = underTest.pull(GitExecutor.PullMode.REBASE_MERGE); + LOGGER.finest(res.outputLine()); + assertEquals(0, res.exitCode()); + } + + @Test + public void canGitRebaseAbort() throws GitExecutionException, IOException { + resetTestScenario(); // arrange + + File targetF = Paths.get(TestData.GIT_FOLDER.getPath(), TestData.GIT_FILE.getName()).toFile(); + Files.move(TestData.GIT_FILE.toPath(), targetF.toPath(), + StandardCopyOption.REPLACE_EXISTING); + + GitExecutionResult resAdd = underTest.add(TestData.GIT_FILE); + assertEquals(0, resAdd.exitCode()); + + GitExecutionResult resCommit = underTest.commit("file deleted accidentaly oopsy"); + LOGGER.finest(resCommit.outputLine()); + + GitExecutionResult res = underTest.rebase(TestData.GIT_HISTORICAL_ONTO_COMMIT_ID); + LOGGER.finest(res.outputLine()); + assertEquals(1, res.exitCode()); + + GitExecutionResult abortResult = underTest.rebaseAbort(); + assertEquals(0, abortResult.exitCode()); + } + + @Test + public void canDetectUncommittedFile() throws GitExecutionException, IOException { + resetTestScenario(); // arrange + + assertFalse(underTest.hasChanges()); + + Files.move(TestData.GIT_FILE.toPath(), Paths.get(TestData.GIT_FOLDER.getPath(), TestData.GIT_FILE.getName()), + StandardCopyOption.REPLACE_EXISTING); + + assertTrue(underTest.hasChanges()); + } + + @Test + public void canStageAllFiles() throws GitExecutionException, IOException { + resetTestScenario(); // arrange + + Files.move(TestData.GIT_FILE.toPath(), Paths.get(TestData.GIT_FOLDER.getPath(), TestData.GIT_FILE.getName()), + StandardCopyOption.REPLACE_EXISTING); + + assertEquals(0, underTest.addAll().exitCode()); + } + + @Test + public void canStageFile() throws GitExecutionException, IOException { + resetTestScenario(); // arrange + + File targetF = Paths.get(TestData.GIT_FOLDER.getPath(), TestData.GIT_FILE.getName()).toFile(); + assertTrue(targetF.createNewFile()); + + assertEquals(0, underTest.add(targetF).exitCode()); + + File nonExistentF = Paths.get(TestData.GIT_FOLDER.getPath(), "nonExistentFile").toFile(); + assertFalse(nonExistentF.exists()); + + assertNotEquals(0, underTest.add(nonExistentF).exitCode()); + } + + @Test + public void canRemoteSshCredentials() { + // TODO + } +} diff --git a/org.archicontribs.modelrepository.tests/src/org/archicontribs/modelrepository/grafico/TestData.java b/org.archicontribs.modelrepository.tests/src/org/archicontribs/modelrepository/grafico/TestData.java new file mode 100644 index 0000000..c0647c9 --- /dev/null +++ b/org.archicontribs.modelrepository.tests/src/org/archicontribs/modelrepository/grafico/TestData.java @@ -0,0 +1,14 @@ +package org.archicontribs.modelrepository.grafico; + +import java.io.File; + +interface TestData { + + final File GIT_REPO = new File("/home/jan/projs/egit"); + final File GIT_FILE = new File(GIT_REPO, "pom.xml"); + final File GIT_FOLDER = new File(GIT_REPO, "icons"); + final File GIT_PATH = new File("/usr/local/bin/git"); + final String GIT_HISTORICAL_COMMIT_ID = "e90d864edca6eb34d0b7a1f0dcc767bcd4970bb5"; + final String GIT_HISTORICAL_ONTO_COMMIT_ID = "cd8c66d521371cbd1163b136f991a9598055d84a"; + +} \ No newline at end of file diff --git a/org.archicontribs.modelrepository.tests/src/org/archicontribs/modelrepository/grafico/launchers/GitExecutorTest.launch b/org.archicontribs.modelrepository.tests/src/org/archicontribs/modelrepository/grafico/launchers/GitExecutorTest.launch new file mode 100644 index 0000000..2fc9b02 --- /dev/null +++ b/org.archicontribs.modelrepository.tests/src/org/archicontribs/modelrepository/grafico/launchers/GitExecutorTest.launch @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/org.archicontribs.modelrepository.tests/src/org/archicontribs/modelrepository/grafico/launchers/archi.product.launch b/org.archicontribs.modelrepository.tests/src/org/archicontribs/modelrepository/grafico/launchers/archi.product.launch new file mode 100644 index 0000000..16401d8 --- /dev/null +++ b/org.archicontribs.modelrepository.tests/src/org/archicontribs/modelrepository/grafico/launchers/archi.product.launch @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.archicontribs.modelrepository/.classpath b/org.archicontribs.modelrepository/.classpath index 735ab3b..bdd4c5f 100644 --- a/org.archicontribs.modelrepository/.classpath +++ b/org.archicontribs.modelrepository/.classpath @@ -4,7 +4,11 @@ - + + + + + diff --git a/org.archicontribs.modelrepository/META-INF/MANIFEST.MF b/org.archicontribs.modelrepository/META-INF/MANIFEST.MF index c290385..e5476f2 100644 --- a/org.archicontribs.modelrepository/META-INF/MANIFEST.MF +++ b/org.archicontribs.modelrepository/META-INF/MANIFEST.MF @@ -3,12 +3,12 @@ Bundle-ManifestVersion: 2 Bundle-Name: coArchi Bundle-SymbolicName: org.archicontribs.modelrepository;singleton:=true Bundle-Localization: plugin -Bundle-Version: 0.9.2.qualifier +Bundle-Version: 0.9.3.qualifier Bundle-Vendor: Archi Require-Bundle: org.eclipse.help.ui, com.archimatetool.editor;bundle-version="4.9.0" Bundle-ActivationPolicy: lazy -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-RequiredExecutionEnvironment: JavaSE-17 Eclipse-BundleShape: dir Bundle-Activator: org.archicontribs.modelrepository.ModelRepositoryPlugin Export-Package: org.archicontribs.modelrepository, diff --git a/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/actions/AbstractModelAction.java b/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/actions/AbstractModelAction.java index 9899daf..45af81f 100644 --- a/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/actions/AbstractModelAction.java +++ b/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/actions/AbstractModelAction.java @@ -18,6 +18,7 @@ import org.archicontribs.modelrepository.grafico.IArchiRepository; import org.archicontribs.modelrepository.grafico.IRepositoryListener; import org.archicontribs.modelrepository.grafico.RepositoryListenerManager; +import org.archicontribs.modelrepository.grafico.ShellArchiRepository; import org.archicontribs.modelrepository.preferences.IPreferenceConstants; import org.eclipse.jface.action.Action; import org.eclipse.jface.dialogs.MessageDialog; @@ -35,6 +36,7 @@ public abstract class AbstractModelAction extends Action implements IGraficoModelAction { private IArchiRepository fRepository; + private ShellArchiRepository shellRepository; protected IWorkbenchWindow fWindow; @@ -46,14 +48,27 @@ protected AbstractModelAction(IWorkbenchWindow window) { public void setRepository(IArchiRepository repository) { fRepository = repository; setEnabled(shouldBeEnabled()); + setShellRepository(new ShellArchiRepository(repository.getLocalRepositoryFolder())); } @Override public IArchiRepository getRepository() { return fRepository; } + + public boolean isShellModeAvailable() { + return this.shellRepository != null; + } - @Override + public void setShellRepository(ShellArchiRepository shellRepository) { + this.shellRepository = shellRepository; + } + + public ShellArchiRepository getShellRepository() { + return shellRepository; + } + + @Override public void update() { setEnabled(shouldBeEnabled()); } @@ -124,8 +139,12 @@ protected boolean offerToCommitChanges() { boolean amend = commitDialog.getAmend(); try { - getRepository().commitChanges(commitMessage, amend); - + if (isShellModeAvailable()) { + getShellRepository().commit(commitMessage, amend); + } else { + getRepository().commitChanges(commitMessage, amend); + + } // Save the checksum getRepository().saveChecksum(); } diff --git a/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/actions/RefreshModelAction.java b/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/actions/RefreshModelAction.java index bb7466a..253c7ab 100644 --- a/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/actions/RefreshModelAction.java +++ b/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/actions/RefreshModelAction.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.security.GeneralSecurityException; +import java.util.logging.Logger; import org.archicontribs.modelrepository.IModelRepositoryImages; import org.archicontribs.modelrepository.authentication.ProxyAuthenticator; @@ -18,6 +19,7 @@ import org.archicontribs.modelrepository.grafico.GraficoModelLoader; import org.archicontribs.modelrepository.grafico.GraficoUtils; import org.archicontribs.modelrepository.grafico.IRepositoryListener; +import org.archicontribs.modelrepository.grafico.ShellArchiRepository.PullOutcome; import org.archicontribs.modelrepository.merge.MergeConflictHandler; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.dialogs.MessageDialog; @@ -51,7 +53,7 @@ * @author Phillip Beauvoir */ public class RefreshModelAction extends AbstractModelAction { - + protected static final int PULL_STATUS_ERROR = -1; protected static final int PULL_STATUS_OK = 0; protected static final int PULL_STATUS_UP_TO_DATE = 1; @@ -59,6 +61,8 @@ public class RefreshModelAction extends AbstractModelAction { protected static final int USER_OK = 0; protected static final int USER_CANCEL = 1; + + private static final Logger LOGGER = Logger.getLogger(RefreshModelAction.class.getName()); public RefreshModelAction(IWorkbenchWindow window) { super(window); @@ -66,11 +70,12 @@ public RefreshModelAction(IWorkbenchWindow window) { setText(Messages.RefreshModelAction_0); setToolTipText(Messages.RefreshModelAction_0); } - + public RefreshModelAction(IWorkbenchWindow window, IArchimateModel model) { this(window); + if(model != null) { - setRepository(new ArchiRepository(GraficoUtils.getLocalRepositoryFolderForModel(model))); + setRepository(new ArchiRepository(GraficoUtils.getLocalRepositoryFolderForModel(model))); } } @@ -169,7 +174,8 @@ protected int init() throws IOException, GitAPIException { getRepository().exportModelToGraficoFiles(); // Then offer to Commit - if(getRepository().hasChangesToCommit()) { + if((super.isShellModeAvailable() && super.getShellRepository().hasChanges()) + || getRepository().hasChangesToCommit()) { if(!offerToCommitChanges()) { return USER_CANCEL; } @@ -186,7 +192,21 @@ protected int pull(UsernamePassword npw, ProgressMonitorDialog pmDialog) throws Display.getCurrent().readAndDispatch(); // update dialog try { - pullResult = getRepository().pullFromRemote(npw, new ProgressMonitorWrapper(pmDialog.getProgressMonitor())); + if (super.isShellModeAvailable()) { + PullOutcome pullOutcome = super.getShellRepository().pullFromRemote(npw); + switch(pullOutcome) { + case ALREADY_UP_TO_DATE: + return PULL_STATUS_UP_TO_DATE; + case PULLED_SUCCESSFULLY: + // places loaded in model in IEditorModelManager + new GraficoModelLoader(getRepository()).loadModel(); + return PULL_STATUS_OK; + case PULL_INCOMPLETE: + LOGGER.warning("Shell mode run into trouble, falling back to jgit handlings."); + } + } + + pullResult = getRepository().pullFromRemote(npw, new ProgressMonitorWrapper(pmDialog.getProgressMonitor())); } catch(Exception ex) { // If this exception is thrown then the remote doesn't have the ref which can happen when pulling on a branch, @@ -289,7 +309,6 @@ protected int pull(UsernamePassword npw, ProgressMonitorDialog pmDialog) throws commitMessage += "\n\n" + Messages.RefreshModelAction_3 + "\n" + restoredObjects; //$NON-NLS-1$ //$NON-NLS-2$ } - // TODO - not sure if amend should be false or true here? getRepository().commitChanges(commitMessage, false); } diff --git a/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/grafico/GitExecutionException.java b/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/grafico/GitExecutionException.java new file mode 100644 index 0000000..f057ba2 --- /dev/null +++ b/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/grafico/GitExecutionException.java @@ -0,0 +1,11 @@ +package org.archicontribs.modelrepository.grafico; + +public class GitExecutionException extends Exception { + + private static final long serialVersionUID = 1L; + + public GitExecutionException(Throwable cause) { + super(cause); + } + +} \ No newline at end of file diff --git a/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/grafico/GitExecutionResult.java b/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/grafico/GitExecutionResult.java new file mode 100644 index 0000000..d703cd1 --- /dev/null +++ b/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/grafico/GitExecutionResult.java @@ -0,0 +1,4 @@ +package org.archicontribs.modelrepository.grafico; + +public record GitExecutionResult(int exitCode, String outputLine) { +} \ No newline at end of file diff --git a/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/grafico/GitExecutor.java b/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/grafico/GitExecutor.java new file mode 100644 index 0000000..e911464 --- /dev/null +++ b/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/grafico/GitExecutor.java @@ -0,0 +1,151 @@ +package org.archicontribs.modelrepository.grafico; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +/** + * GitExecutor allows direct access to git-cli. + * + * Majorly inspired by + * https://github.com/JetBrains/intellij-community/tree/8b6e5ebc0cfccaad14323e47337ddac47c8347aa/plugins/git4idea/src/git4idea + * + * @author Jan Esser + */ +public class GitExecutor { + + public static enum PullMode { + FF_ONLY, REBASE_MERGE + } + + private static final String[] from(String command, String... commandArgs) { + List commandStrings = new ArrayList(1 + (commandArgs != null ? commandArgs.length : 0)); + commandStrings.add(command); + + for (String commandArg : commandArgs) + if (commandArg != null && commandArg != "") + commandStrings.add(commandArg); + + return commandStrings.toArray(new String[0]); + } + + private final File gitPath; + private final File gitRepo; + + public GitExecutor(File localRepoFolder) throws GitExecutionException { + this(new File("git"), localRepoFolder); + } + + public GitExecutor(File gitPath, File gitRepo) throws GitExecutionException { + this.gitPath = gitPath; + this.gitRepo = gitRepo; + + version(); // assure git is operational + } + + private GitExecutionResult gitExec(File gitPath, File workingDir, String... gitCommands) + throws IOException, InterruptedException { + String[] commandStrings = from(gitPath.getPath(), gitCommands); + + ProcessBuilder pb = new ProcessBuilder(commandStrings); + pb.redirectErrorStream(true); + pb.directory(workingDir); + Process p = pb.start(); + BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); + // TODO add supplier -> consumer her to also process structured/multi-line + String line = br.readLine(); + return new GitExecutionResult(p.waitFor(), line); + } + + private GitExecutionResult gitExec(String gitCommand, String... gitCommandArgs) throws GitExecutionException { + try { + return gitExec(gitPath, gitRepo, from(gitCommand, gitCommandArgs)); + } catch (IOException | InterruptedException ex) { + throw new GitExecutionException(ex); + } + } + + public GitExecutionResult version() throws GitExecutionException { + return gitExec("--version"); + } + + public GitExecutionResult fetch() throws GitExecutionException { + return gitExec("fetch"); + } + + public GitExecutionResult pull() throws GitExecutionException { + return pull(PullMode.FF_ONLY); + } + + public GitExecutionResult pull(PullMode mode) throws GitExecutionException { + String pullArg; + switch (mode) { + case FF_ONLY: + pullArg = "--ff-only"; + break; + case REBASE_MERGE: + pullArg = "--rebase=merges"; + break; + default: + throw new RuntimeException("NOT IMPLEMENTED"); + } + + return gitExec("pull", pullArg); + } + + public GitExecutionResult reset(boolean hard, String commitId) throws GitExecutionException { + return gitExec("reset", hard ? "--hard" : "", commitId); + } + + public GitExecutionResult commit(String message) throws GitExecutionException { + return commit(message, false, false, false); + } + + public GitExecutionResult commit(String message, boolean amend, boolean allowEmpty, boolean commitAll) + throws GitExecutionException { + if (commitAll) + this.addAll(); + + return gitExec("commit", // + "-m", message, // + amend ? "--amend" : "", // + allowEmpty ? "--allow-empty" : "" // + ); + } + + public GitExecutionResult rebase(String onto) throws GitExecutionException { + return gitExec("rebase", "--onto", onto); + } + + public GitExecutionResult rebaseAbort() throws GitExecutionException { + return gitExec("rebase", "--abort"); + } + + public boolean hasChanges() throws GitExecutionException { + // https://stackoverflow.com/a/3879077 + GitExecutionResult refreshIndexResult = gitExec("update-index", "--refresh"); + if (refreshIndexResult.exitCode() != 0) + return true; + + GitExecutionResult diffIndexResult = gitExec("diff-index", "--quiet", "HEAD"); + if (diffIndexResult.exitCode() != 0) + return true; + + return false; // otherwise + } + + public GitExecutionResult addAll() throws GitExecutionException { + return gitExec("add", "-A", ":/"); + } + + public GitExecutionResult add(File f) throws GitExecutionException { + return gitExec("add", "-A", this.gitRepo.toPath().relativize(f.toPath()).toString()); + } + + public GitExecutionResult clean() throws GitExecutionException { + return gitExec("clean", "-fd", ":/"); + } +} \ No newline at end of file diff --git a/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/grafico/ShellArchiRepository.java b/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/grafico/ShellArchiRepository.java new file mode 100644 index 0000000..200a215 --- /dev/null +++ b/org.archicontribs.modelrepository/src/org/archicontribs/modelrepository/grafico/ShellArchiRepository.java @@ -0,0 +1,66 @@ +package org.archicontribs.modelrepository.grafico; + +import java.io.File; +import java.io.IOException; + +import org.archicontribs.modelrepository.authentication.UsernamePassword; +import org.archicontribs.modelrepository.grafico.GitExecutor.PullMode; + +import com.archimatetool.editor.actions.AbstractModelAction; + +/** + * ShellArchiRepository challenges ArchiRepository with alternative GIT + * back-end. + * + * Hexagonal force field: driven by: descendants of {@link AbstractModelAction}, driver: git-executor + */ +public class ShellArchiRepository { + + public static enum PullOutcome { + ALREADY_UP_TO_DATE, PULLED_SUCCESSFULLY, PULL_INCOMPLETE + } + + private final GitExecutor executor; + + public ShellArchiRepository(File localRepoFolder) { + try { + this.executor = new GitExecutor(new File("/snap/eclipse-pde/current/usr/bin/git"), localRepoFolder); + } catch (GitExecutionException e) { + throw new RuntimeException(e); + } + } + + public PullOutcome pullFromRemote(UsernamePassword npw) throws IOException { + try { + GitExecutionResult result = executor.pull(PullMode.REBASE_MERGE); + switch (result.exitCode()) { + case 0: + if (result.outputLine().endsWith(".")) + return PullOutcome.ALREADY_UP_TO_DATE; + else + return PullOutcome.PULLED_SUCCESSFULLY; + default: + return PullOutcome.PULL_INCOMPLETE; + } + } catch (GitExecutionException e) { + throw new IOException(e); + } + } + + public boolean hasChanges() throws IOException { + try { + return executor.hasChanges(); + } catch (GitExecutionException e) { + throw new IOException(e); + } + } + + public boolean commit(String commitMessage, boolean amend) throws IOException { + try { + return 0 == executor.commit(commitMessage, amend, false, true).exitCode(); + } catch (GitExecutionException e) { + throw new IOException(e); + } + } + +}