Fixed some small bugs, added test.
This commit is contained in:
parent
76099ddc99
commit
540616db42
|
@ -0,0 +1,5 @@
|
||||||
|
.distribugit_tmp/
|
||||||
|
.distribugit_test_tmp/
|
||||||
|
.idea/
|
||||||
|
target/
|
||||||
|
*.iml
|
17
pom.xml
17
pom.xml
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<groupId>nl.andrewl</groupId>
|
<groupId>nl.andrewl</groupId>
|
||||||
<artifactId>distribugit</artifactId>
|
<artifactId>distribugit</artifactId>
|
||||||
<version>1.2.0</version>
|
<version>1.3.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
@ -46,6 +46,21 @@
|
||||||
<artifactId>picocli</artifactId>
|
<artifactId>picocli</artifactId>
|
||||||
<version>4.6.3</version>
|
<version>4.6.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<version>5.8.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<version>5.8.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import java.nio.file.Path;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ForkJoinPool;
|
import java.util.concurrent.ForkJoinPool;
|
||||||
|
|
||||||
|
@ -120,7 +121,7 @@ public class DistribuGit {
|
||||||
if (finalizationAction != null) {
|
if (finalizationAction != null) {
|
||||||
applyActionToRepositories(repos, finalizationAction);
|
applyActionToRepositories(repos, finalizationAction);
|
||||||
}
|
}
|
||||||
repos.values().forEach(Git::close);
|
repos.values().stream().filter(Objects::nonNull).forEach(Git::close);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -23,4 +23,12 @@ public interface RepositorySelector {
|
||||||
static RepositorySelector from(String... uris) {
|
static RepositorySelector from(String... uris) {
|
||||||
return () -> List.of(uris);
|
return () -> List.of(uris);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static RepositorySelector from(RepositorySelector... selectors) {
|
||||||
|
return () -> {
|
||||||
|
List<String> uris = new ArrayList<>();
|
||||||
|
for (var selector : selectors) uris.addAll(selector.getURIs());
|
||||||
|
return uris;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,46 @@
|
||||||
package nl.andrewl.distribugit.cli;
|
package nl.andrewl.distribugit.cli;
|
||||||
|
|
||||||
import nl.andrewl.distribugit.*;
|
import nl.andrewl.distribugit.DistribuGit;
|
||||||
import nl.andrewl.distribugit.selectors.GitHubSelectorBuilder;
|
import nl.andrewl.distribugit.GitCredentials;
|
||||||
|
import nl.andrewl.distribugit.RepositoryAction;
|
||||||
|
import nl.andrewl.distribugit.StatusListener;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.concurrent.Callable;
|
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<Integer> {
|
public class DistribuGitCommand implements Callable<Integer> {
|
||||||
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")
|
@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)
|
@CommandLine.Option(
|
||||||
private String selectorExpression;
|
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)
|
@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.")
|
@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.")
|
@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")
|
@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")
|
@CommandLine.Option(names = {"-cl", "--cleanup"}, description = "Whether to remove all repository files when done.", defaultValue = "false")
|
||||||
private boolean cleanup;
|
public boolean cleanup;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
|
@ -42,7 +48,7 @@ public class DistribuGitCommand implements Callable<Integer> {
|
||||||
.workingDir(workingDir)
|
.workingDir(workingDir)
|
||||||
.strictFail(strictFail)
|
.strictFail(strictFail)
|
||||||
.cleanup(cleanup)
|
.cleanup(cleanup)
|
||||||
.selector(parseSelectorExpression(selectorExpression))
|
.selector(SelectorExpressionParser.parse(selectorExpression, accessToken))
|
||||||
.action(RepositoryAction.ofCommand(actionCommand.split("\\s+")));
|
.action(RepositoryAction.ofCommand(actionCommand.split("\\s+")));
|
||||||
if (finalizationActionCommand != null) {
|
if (finalizationActionCommand != null) {
|
||||||
builder.finalizationAction(RepositoryAction.ofCommand(finalizationActionCommand.split("\\s+")));
|
builder.finalizationAction(RepositoryAction.ofCommand(finalizationActionCommand.split("\\s+")));
|
||||||
|
@ -65,20 +71,6 @@ public class DistribuGitCommand implements Callable<Integer> {
|
||||||
return 0;
|
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) {
|
public static void main(String[] args) {
|
||||||
new CommandLine(new DistribuGitCommand()).execute(args);
|
new CommandLine(new DistribuGitCommand()).execute(args);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<String> 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<Path> 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<String> uris = new ArrayList<>();
|
||||||
|
for (var path : paths) {
|
||||||
|
try (var s = Files.lines(path)) {
|
||||||
|
uris.addAll(s.filter(str -> !str.isBlank()).toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uris;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue