diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3013da1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.distribugit_tmp/
+.distribugit_test_tmp/
+.idea/
+target/
+*.iml
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 7f1d4e2..9e4afbf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
nl.andrewl
distribugit
- 1.2.0
+ 1.3.0
17
@@ -46,6 +46,21 @@
picocli
4.6.3
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.8.2
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.8.2
+ test
+
diff --git a/src/main/java/nl/andrewl/distribugit/DistribuGit.java b/src/main/java/nl/andrewl/distribugit/DistribuGit.java
index db9dced..445d9d7 100644
--- a/src/main/java/nl/andrewl/distribugit/DistribuGit.java
+++ b/src/main/java/nl/andrewl/distribugit/DistribuGit.java
@@ -9,6 +9,7 @@ import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ForkJoinPool;
@@ -120,7 +121,7 @@ public class DistribuGit {
if (finalizationAction != null) {
applyActionToRepositories(repos, finalizationAction);
}
- repos.values().forEach(Git::close);
+ repos.values().stream().filter(Objects::nonNull).forEach(Git::close);
} catch (Exception e) {
throw new IOException(e);
} finally {
diff --git a/src/main/java/nl/andrewl/distribugit/RepositorySelector.java b/src/main/java/nl/andrewl/distribugit/RepositorySelector.java
index b98452e..b67f5a8 100644
--- a/src/main/java/nl/andrewl/distribugit/RepositorySelector.java
+++ b/src/main/java/nl/andrewl/distribugit/RepositorySelector.java
@@ -23,4 +23,12 @@ public interface RepositorySelector {
static RepositorySelector from(String... uris) {
return () -> List.of(uris);
}
+
+ static RepositorySelector from(RepositorySelector... selectors) {
+ return () -> {
+ List uris = new ArrayList<>();
+ for (var selector : selectors) uris.addAll(selector.getURIs());
+ return uris;
+ };
+ }
}
diff --git a/src/main/java/nl/andrewl/distribugit/cli/DistribuGitCommand.java b/src/main/java/nl/andrewl/distribugit/cli/DistribuGitCommand.java
index 0d0d4c4..ba3dcca 100644
--- a/src/main/java/nl/andrewl/distribugit/cli/DistribuGitCommand.java
+++ b/src/main/java/nl/andrewl/distribugit/cli/DistribuGitCommand.java
@@ -1,40 +1,46 @@
package nl.andrewl.distribugit.cli;
-import nl.andrewl.distribugit.*;
-import nl.andrewl.distribugit.selectors.GitHubSelectorBuilder;
+import nl.andrewl.distribugit.DistribuGit;
+import nl.andrewl.distribugit.GitCredentials;
+import nl.andrewl.distribugit.RepositoryAction;
+import nl.andrewl.distribugit.StatusListener;
import picocli.CommandLine;
-import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.Callable;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-@CommandLine.Command(name = "distribugit")
+@CommandLine.Command(name = "distribugit", description = "DistribuGit command-line tool for performing distributed git operations.", mixinStandardHelpOptions = true)
public class DistribuGitCommand implements Callable {
- private static final Pattern SELECTOR_EXPRESSION_PATTERN = Pattern.compile("([\\w-]+):(.+)");
- private static final Pattern ORG_REPO_PREFIX_PATTERN = Pattern.compile("(.+)/(.+)");
-
@CommandLine.Option(names = {"-d", "--dir"}, description = "The working directory for DistribuGit", defaultValue = "./.distribugit_tmp")
- private Path workingDir;
+ public Path workingDir;
- @CommandLine.Option(names = {"-s", "--selector"}, description = "The repository selector to use. Format: \"slug:content\"", required = true)
- private String selectorExpression;
+ @CommandLine.Option(
+ names = {"-s", "--selector"},
+ description = """
+ The repository selector to use.
+ The following selector types are permitted:
+ - "org-repo-prefix:{orgName}/{repoPrefix}": Selects repositories from a GitHub organization whose name begins with the given prefix.
+ - "stdin": Selects repository URIs that have been provided to the program via stdin, with one URI per line.
+ - "file:{filePath}": Selects repository URIs written in a file, with one URI per line.
+ """,
+ required = true
+ )
+ public String selectorExpression;
@CommandLine.Option(names = {"-a", "--action"}, description = "The command to run on each repository.", required = true)
- private String actionCommand;
+ public String actionCommand;
@CommandLine.Option(names = {"-fa", "--finalization-action"}, description = "A command to run on each repository after all normal actions.")
- private String finalizationActionCommand;
+ public String finalizationActionCommand;
@CommandLine.Option(names = {"-t", "--access-token"}, description = "The access token to use to perform operations.")
- private String accessToken;
+ public String accessToken;
@CommandLine.Option(names = {"-sf", "--strict-fail"}, description = "Whether to preemptively fail if any error occurs.", defaultValue = "true")
- private boolean strictFail;
+ public boolean strictFail;
@CommandLine.Option(names = {"-cl", "--cleanup"}, description = "Whether to remove all repository files when done.", defaultValue = "false")
- private boolean cleanup;
+ public boolean cleanup;
@Override
public Integer call() throws Exception {
@@ -42,7 +48,7 @@ public class DistribuGitCommand implements Callable {
.workingDir(workingDir)
.strictFail(strictFail)
.cleanup(cleanup)
- .selector(parseSelectorExpression(selectorExpression))
+ .selector(SelectorExpressionParser.parse(selectorExpression, accessToken))
.action(RepositoryAction.ofCommand(actionCommand.split("\\s+")));
if (finalizationActionCommand != null) {
builder.finalizationAction(RepositoryAction.ofCommand(finalizationActionCommand.split("\\s+")));
@@ -65,20 +71,6 @@ public class DistribuGitCommand implements Callable {
return 0;
}
- private RepositorySelector parseSelectorExpression(String expr) throws IOException {
- Matcher m = SELECTOR_EXPRESSION_PATTERN.matcher(expr);
- if (!m.find()) throw new IllegalArgumentException("Invalid selector expression. Should be \"selector-type:expression\".");
- String slug = m.group(1);
- String content = m.group(2);
- if (slug.equalsIgnoreCase("org-repo-prefix")) {
- Matcher m1 = ORG_REPO_PREFIX_PATTERN.matcher(content);
- if (!m1.find()) throw new IllegalArgumentException("Invalid content for org-repo-prefix select. Should be \"orgName/prefix\"");
- return GitHubSelectorBuilder.fromPersonalAccessToken(accessToken).orgAndPrefix(m1.group(1), m1.group(2));
- } else {
- throw new IllegalArgumentException("Unsupported selector type: \"" + slug + "\".");
- }
- }
-
public static void main(String[] args) {
new CommandLine(new DistribuGitCommand()).execute(args);
}
diff --git a/src/main/java/nl/andrewl/distribugit/cli/SelectorExpressionParser.java b/src/main/java/nl/andrewl/distribugit/cli/SelectorExpressionParser.java
new file mode 100644
index 0000000..99ba71e
--- /dev/null
+++ b/src/main/java/nl/andrewl/distribugit/cli/SelectorExpressionParser.java
@@ -0,0 +1,77 @@
+package nl.andrewl.distribugit.cli;
+
+import nl.andrewl.distribugit.RepositorySelector;
+import nl.andrewl.distribugit.selectors.GitHubSelectorBuilder;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SelectorExpressionParser {
+ private static final Pattern SELECTOR_EXPRESSION_PATTERN = Pattern.compile("([\\w-]+):(.+)");
+ private static final Pattern ORG_REPO_PREFIX_PATTERN = Pattern.compile("(.+)/(.+)");
+
+ public static RepositorySelector parse(String expr, String accessToken) throws IOException {
+ Matcher m = SELECTOR_EXPRESSION_PATTERN.matcher(expr);
+ if (!m.find()) throw new IllegalArgumentException("Invalid selector expression. Should be \"selector-type[:expression]\".");
+ String slug = m.group(1);
+ String content = m.groupCount() > 1 ? m.group(2) : null;
+ return switch (slug) {
+ case "org-repo-prefix" -> parseOrgRepoPrefix(content, accessToken);
+ case "stdin" -> stdinSelector();
+ case "file" -> fileSelector(content);
+ default -> throw new IllegalArgumentException("Unsupported selector type: " + slug);
+ };
+ }
+
+ private static RepositorySelector parseOrgRepoPrefix(String content, String accessToken) throws IOException {
+ if (content == null) throw new IllegalArgumentException("Missing required selector expression.");
+ if (accessToken == null) throw new IllegalArgumentException("Missing required access-token for GitHub org-repo-prefix selector.");
+ Matcher m = ORG_REPO_PREFIX_PATTERN.matcher(content);
+ if (!m.find()) throw new IllegalArgumentException("Invalid content for org-repo-prefix select. Should be \"orgName/prefix\"");
+ return GitHubSelectorBuilder.fromPersonalAccessToken(accessToken).orgAndPrefix(m.group(1), m.group(2));
+ }
+
+ private static RepositorySelector stdinSelector() {
+ return () -> {
+ List uris = new ArrayList<>();
+ String line;
+ try (var reader = new BufferedReader(new InputStreamReader(System.in))) {
+ while ((line = reader.readLine()) != null) {
+ if (!line.isBlank()) {
+ uris.add(line.trim());
+ }
+ }
+ }
+ return uris;
+ };
+ }
+
+ private static RepositorySelector fileSelector(String content) {
+ if (content == null) throw new IllegalArgumentException("No file paths were given.");
+ String[] filePaths = content.split(";");
+ if (filePaths.length < 1) throw new IllegalArgumentException("No file paths were given.");
+ List paths = Arrays.stream(filePaths).map(Path::of).toList();
+ for (var path : paths) {
+ if (Files.notExists(path)) throw new IllegalArgumentException("File " + path + " does not exist.");
+ if (!Files.isRegularFile(path)) throw new IllegalArgumentException("File " + path + " is not a regular file.");
+ if (!Files.isReadable(path)) throw new IllegalArgumentException("File " + path + " is not readable.");
+ }
+ return () -> {
+ List uris = new ArrayList<>();
+ for (var path : paths) {
+ try (var s = Files.lines(path)) {
+ uris.addAll(s.filter(str -> !str.isBlank()).toList());
+ }
+ }
+ return uris;
+ };
+ }
+}
diff --git a/src/test/java/nl/andrewl/distribugit/DistribuGitTest.java b/src/test/java/nl/andrewl/distribugit/DistribuGitTest.java
new file mode 100644
index 0000000..d1c414b
--- /dev/null
+++ b/src/test/java/nl/andrewl/distribugit/DistribuGitTest.java
@@ -0,0 +1,79 @@
+package nl.andrewl.distribugit;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class DistribuGitTest {
+ @Test
+ public void testBasicActionOperation() throws IOException {
+ AtomicInteger count = new AtomicInteger(0);
+ RepositoryAction countAction = git -> count.incrementAndGet();
+ new DistribuGit.Builder()
+ .selector(RepositorySelector.from(
+ "https://github.com/andrewlalis/record-net.git",
+ "https://github.com/andrewlalis/distribugit.git",
+ "https://github.com/andrewlalis/RandomHotbar.git"
+ ))
+ .action(countAction)
+ .statusListener(noOpListener())
+ .strictFail(true)
+ .cleanup(true)
+ .workingDir(Path.of(".", ".distribugit_test_tmp"))
+ .build().doActions();
+ assertEquals(3, count.get());
+ }
+
+ @Test
+ public void testStrictFailFalse() throws IOException {
+ AtomicInteger count = new AtomicInteger(0);
+ RepositoryAction countAction = git -> count.incrementAndGet();
+ new DistribuGit.Builder()
+ .selector(RepositorySelector.from(
+ "https://github.com/andrewlalis/record-net.git",
+ "https://github.com/andrewlalis/distribugit.git",
+ "https://github.com/andrewlalis/somerandomgitrepositorythatdoesntexist.git"
+ ))
+ .action(countAction)
+ .statusListener(noOpListener())
+ .strictFail(false)
+ .cleanup(true)
+ .workingDir(Path.of(".", ".distribugit_test_tmp"))
+ .build().doActions();
+ assertEquals(2, count.get());
+ }
+
+ @Test
+ public void testStrictFailTrue() {
+ AtomicInteger count = new AtomicInteger(0);
+ RepositoryAction countAction = git -> count.incrementAndGet();
+ DistribuGit d = new DistribuGit.Builder()
+ .selector(RepositorySelector.from(
+ "https://github.com/andrewlalis/record-net.git",
+ "https://github.com/andrewlalis/distribugit.git",
+ "https://github.com/andrewlalis/somerandomgitrepositorythatdoesntexist.git"
+ ))
+ .action(countAction)
+ .statusListener(noOpListener())
+ .strictFail(true)
+ .cleanup(true)
+ .workingDir(Path.of(".", ".distribugit_test_tmp"))
+ .build();
+ assertThrows(IOException.class, d::doActions);
+ }
+
+ private static StatusListener noOpListener() {
+ return new StatusListener() {
+ @Override
+ public void progressUpdated(float percentage) {}
+
+ @Override
+ public void messageReceived(String message) {}
+ };
+ }
+}