From d16d81209df0b32a50461f3e4e3b3b5ec246625a Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Mon, 18 Apr 2022 20:55:18 +0200 Subject: [PATCH] Added file changes. --- pom.xml | 38 ++++ .../nl/andrewl/distribugit/DistribuGit.java | 191 ++++++++++++++++++ .../andrewl/distribugit/GitCredentials.java | 64 ++++++ .../andrewl/distribugit/RepositoryAction.java | 9 + .../distribugit/RepositorySelector.java | 21 ++ .../andrewl/distribugit/StatusListener.java | 6 + .../java/nl/andrewl/distribugit/Utils.java | 40 ++++ 7 files changed, 369 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/nl/andrewl/distribugit/DistribuGit.java create mode 100644 src/main/java/nl/andrewl/distribugit/GitCredentials.java create mode 100644 src/main/java/nl/andrewl/distribugit/RepositoryAction.java create mode 100644 src/main/java/nl/andrewl/distribugit/RepositorySelector.java create mode 100644 src/main/java/nl/andrewl/distribugit/StatusListener.java create mode 100644 src/main/java/nl/andrewl/distribugit/Utils.java diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..149ef2e --- /dev/null +++ b/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + nl.andrewl + distribugit + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + + org.eclipse.jgit + org.eclipse.jgit + 6.1.0.202203080745-r + + + + org.eclipse.jgit + org.eclipse.jgit.ssh.jsch + 6.1.0.202203080745-r + + + + org.slf4j + slf4j-simple + 1.7.36 + runtime + + + \ No newline at end of file diff --git a/src/main/java/nl/andrewl/distribugit/DistribuGit.java b/src/main/java/nl/andrewl/distribugit/DistribuGit.java new file mode 100644 index 0000000..c7ac9c7 --- /dev/null +++ b/src/main/java/nl/andrewl/distribugit/DistribuGit.java @@ -0,0 +1,191 @@ +package nl.andrewl.distribugit; + +import org.eclipse.jgit.api.CloneCommand; +import org.eclipse.jgit.api.Git; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DistribuGit { + private final RepositorySelector selector; + private final RepositoryAction action; + private final GitCredentials credentials; + private final StatusListener statusListener; + private final Path workingDir; + private final boolean strictFail; + private final boolean cleanup; + + private int stepsComplete; + private int stepsTotal; + + public DistribuGit( + RepositorySelector selector, + RepositoryAction action, + GitCredentials credentials, + StatusListener statusListener, + Path workingDir, + boolean strictFail, + boolean cleanup + ) { + this.selector = selector; + this.action = action; + this.credentials = credentials; + this.statusListener = statusListener; + this.workingDir = workingDir; + this.strictFail = strictFail; + this.cleanup = cleanup; + } + + public void doActions() throws IOException { + stepsComplete = 0; + Utils.delete(workingDir); // Delete the directory if it already exists. + Files.createDirectory(workingDir); + statusListener.messageReceived("Prepared temporary directory for repositories."); + try { + List repositoryURIs = selector.getURIs(); + stepsTotal = 2 * repositoryURIs.size(); + Map repoDirs = downloadRepositories(repositoryURIs); + applyActionToRepositories(repoDirs); + } catch (Exception e) { + throw new IOException(e); + } finally { + if (cleanup) { + statusListener.messageReceived("Removing all repositories."); + Utils.delete(workingDir); + } + } + } + + private void completeStep() { + stepsComplete++; + statusListener.progressUpdated(stepsComplete / (float) stepsTotal); + } + + private Map downloadRepositories(List uris) throws IOException { + Map repositoryDirs = new HashMap<>(); + int dirIdx = 1; + for (String repositoryURI : uris) { + Path repoDir = workingDir.resolve(Integer.toString(dirIdx++)); + try { + statusListener.messageReceived("Cloning repository " + repositoryURI + " to " + repoDir); + CloneCommand clone = Git.cloneRepository(); + credentials.addCredentials(clone); + clone.setDirectory(repoDir.toFile()); + clone.setURI(repositoryURI); + try (var ignored = clone.call()) { + repositoryDirs.put(repositoryURI, repoDir); + } catch (Exception e) { + if (strictFail) { + throw new IOException(e); + } else { + repositoryDirs.put(repositoryURI, null); + e.printStackTrace(); + } + } + } catch (Exception e) { + if (strictFail) { + throw new IOException(e); + } + e.printStackTrace(); + } + completeStep(); + } + return repositoryDirs; + } + + private void applyActionToRepositories(Map repoDirs) throws IOException { + for (var entry : repoDirs.entrySet()) { + if (entry.getValue() != null) { + try (Git git = Git.open(entry.getValue().toFile())) { + statusListener.messageReceived("Applying action to repository " + entry.getKey()); + action.doAction(git); + } catch (Exception e) { + if (strictFail) { + throw new IOException(e); + } + e.printStackTrace(); + } + } else { + statusListener.messageReceived("Skipping action on repository " + entry.getKey() + " because it could not be downloaded."); + } + completeStep(); + } + } + + public static class Builder { + private RepositorySelector selector; + private RepositoryAction action; + private GitCredentials credentials = cmd -> {}; + private StatusListener statusListener; + private Path workingDir = Path.of(".", ".distribugit_tmp"); + private boolean strictFail = true; + private boolean cleanup = false; + + public Builder selector(RepositorySelector selector) { + this.selector = selector; + return this; + } + + public Builder action(RepositoryAction action) { + this.action = action; + return this; + } + + public Builder credentials(GitCredentials credentials) { + this.credentials = credentials; + return this; + } + + public Builder statusListener(StatusListener listener) { + this.statusListener = listener; + return this; + } + + public Builder workingDir(Path dir) { + this.workingDir = dir; + return this; + } + + public Builder strictFail(boolean strictFail) { + this.strictFail = strictFail; + return this; + } + + public Builder cleanup(boolean cleanup) { + this.cleanup = cleanup; + return this; + } + + public DistribuGit build() { + return new DistribuGit(selector, action, credentials, statusListener, workingDir, strictFail, cleanup); + } + } + + public static void main(String[] args) throws IOException { + new Builder() + .selector(RepositorySelector.from( + "https://github.com/andrewlalis/RandomHotbar.git", + "https://github.com/andrewlalis/CoyoteCredit.git", + "https://github.com/andrewlalis/SignalsAndSystems2021.git" + )) + .action(git -> System.out.println("Cloned!")) + .statusListener(new StatusListener() { + @Override + public void progressUpdated(float percentage) { + System.out.printf("Progress: %.1f%%%n", percentage * 100); + } + + @Override + public void messageReceived(String message) { + System.out.println("Message: " + message); + } + }) + .strictFail(true) + .cleanup(true) + .build().doActions(); + } +} diff --git a/src/main/java/nl/andrewl/distribugit/GitCredentials.java b/src/main/java/nl/andrewl/distribugit/GitCredentials.java new file mode 100644 index 0000000..4aa182b --- /dev/null +++ b/src/main/java/nl/andrewl/distribugit/GitCredentials.java @@ -0,0 +1,64 @@ +package nl.andrewl.distribugit; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import org.eclipse.jgit.api.TransportCommand; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.SshTransport; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.eclipse.jgit.transport.ssh.jsch.JschConfigSessionFactory; +import org.eclipse.jgit.transport.ssh.jsch.OpenSshConfig; +import org.eclipse.jgit.util.FS; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +public interface GitCredentials { + void addCredentials(TransportCommand gitCommand) throws Exception; + + static GitCredentials ofUsernamePassword(String username, String password) { + return cmd -> cmd.setCredentialsProvider(new UsernamePasswordCredentialsProvider(username, password)); + } + + static GitCredentials ofSshKey() { + return ofSshKey(Path.of(System.getProperty("user.home"), ".ssh", "id_rsa")); + } + + static GitCredentials ofSshKey(Path privateKeyFile) { + return ofSshKey( + privateKeyFile, + privateKeyFile.getParent().resolve(privateKeyFile.getFileName().toString() + ".pub"), + null + ); + } + + static GitCredentials ofSshKey(Path privateKeyFile, Path publicKeyFile, String passphrase) { + System.out.println("Using private key at " + privateKeyFile.toAbsolutePath()); + SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() { + @Override + protected void configure(OpenSshConfig.Host hc, Session session) { + session.setConfig("StrictHostKeyChecking", "no"); // Don't require the host to be in the system's known hosts list. + } + + @Override + protected JSch createDefaultJSch(FS fs) throws JSchException { + var jsch = super.createDefaultJSch(fs); + jsch.removeAllIdentity(); + jsch.addIdentity( + privateKeyFile.toAbsolutePath().toString(), + publicKeyFile.toAbsolutePath().toString(), + passphrase == null ? null : passphrase.getBytes(StandardCharsets.UTF_8) + ); + return jsch; + } + }; + return cmd -> cmd.setTransportConfigCallback(transport -> { + if (transport instanceof SshTransport sshTransport) { + sshTransport.setSshSessionFactory(sshSessionFactory); + } else { + throw new IllegalStateException("Invalid git transport method: " + transport.getClass().getSimpleName() + "; Cannot apply SSH session factory to this type of transport."); + } + }); + } +} diff --git a/src/main/java/nl/andrewl/distribugit/RepositoryAction.java b/src/main/java/nl/andrewl/distribugit/RepositoryAction.java new file mode 100644 index 0000000..93c4f0d --- /dev/null +++ b/src/main/java/nl/andrewl/distribugit/RepositoryAction.java @@ -0,0 +1,9 @@ +package nl.andrewl.distribugit; + +import org.eclipse.jgit.api.Git; + +public interface RepositoryAction { + void doAction(Git git) throws Exception; + + +} diff --git a/src/main/java/nl/andrewl/distribugit/RepositorySelector.java b/src/main/java/nl/andrewl/distribugit/RepositorySelector.java new file mode 100644 index 0000000..8a052f9 --- /dev/null +++ b/src/main/java/nl/andrewl/distribugit/RepositorySelector.java @@ -0,0 +1,21 @@ +package nl.andrewl.distribugit; + + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Component which produces a list of repositories to operate on. + */ +public interface RepositorySelector { + List getURIs() throws Exception; + + static RepositorySelector fromCollection(Collection uris) { + return () -> new ArrayList<>(uris); + } + + static RepositorySelector from(String... uris) { + return () -> List.of(uris); + } +} diff --git a/src/main/java/nl/andrewl/distribugit/StatusListener.java b/src/main/java/nl/andrewl/distribugit/StatusListener.java new file mode 100644 index 0000000..3535185 --- /dev/null +++ b/src/main/java/nl/andrewl/distribugit/StatusListener.java @@ -0,0 +1,6 @@ +package nl.andrewl.distribugit; + +public interface StatusListener { + void progressUpdated(float percentage); + void messageReceived(String message); +} diff --git a/src/main/java/nl/andrewl/distribugit/Utils.java b/src/main/java/nl/andrewl/distribugit/Utils.java new file mode 100644 index 0000000..ea554be --- /dev/null +++ b/src/main/java/nl/andrewl/distribugit/Utils.java @@ -0,0 +1,40 @@ +package nl.andrewl.distribugit; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +public class Utils { + public static void deleteNoThrow(Path path) { + try { + delete(path); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void delete(Path path) throws IOException { + if (path == null) return; + if (!Files.exists(path)) return; + if (Files.isRegularFile(path)) { + Files.delete(path); + } else { + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + } +}