Added file changes.

This commit is contained in:
Andrew Lalis 2022-04-18 20:55:18 +02:00
parent 7aa45c4635
commit d16d81209d
7 changed files with 369 additions and 0 deletions

38
pom.xml Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>nl.andrewl</groupId>
<artifactId>distribugit</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit -->
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.1.0.202203080745-r</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit.ssh.jsch -->
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
<version>6.1.0.202203080745-r</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.36</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>

View File

@ -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<String> repositoryURIs = selector.getURIs();
stepsTotal = 2 * repositoryURIs.size();
Map<String, Path> 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<String, Path> downloadRepositories(List<String> uris) throws IOException {
Map<String, Path> 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<String, Path> 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();
}
}

View File

@ -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.");
}
});
}
}

View File

@ -0,0 +1,9 @@
package nl.andrewl.distribugit;
import org.eclipse.jgit.api.Git;
public interface RepositoryAction {
void doAction(Git git) throws Exception;
}

View File

@ -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<String> getURIs() throws Exception;
static RepositorySelector fromCollection(Collection<String> uris) {
return () -> new ArrayList<>(uris);
}
static RepositorySelector from(String... uris) {
return () -> List.of(uris);
}
}

View File

@ -0,0 +1,6 @@
package nl.andrewl.distribugit;
public interface StatusListener {
void progressUpdated(float percentage);
void messageReceived(String message);
}

View File

@ -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;
}
});
}
}
}