Compare commits

..

No commits in common. "main" and "1.2.0" have entirely different histories.
main ... 1.2.0

8 changed files with 37 additions and 238 deletions

5
.gitignore vendored
View File

@ -1,5 +0,0 @@
.distribugit_tmp/
.distribugit_test_tmp/
.idea/
target/
*.iml

19
pom.xml
View File

@ -6,7 +6,7 @@
<groupId>nl.andrewl</groupId>
<artifactId>distribugit</artifactId>
<version>1.3.1</version>
<version>1.2.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
@ -38,7 +38,7 @@
<dependency>
<groupId>org.kohsuke</groupId>
<artifactId>github-api</artifactId>
<version>1.306</version>
<version>1.303</version>
</dependency>
<!-- https://mvnrepository.com/artifact/info.picocli/picocli -->
<dependency>
@ -46,21 +46,6 @@
<artifactId>picocli</artifactId>
<version>4.6.3</version>
</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>
<build>

View File

@ -9,7 +9,6 @@ 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;
@ -121,7 +120,7 @@ public class DistribuGit {
if (finalizationAction != null) {
applyActionToRepositories(repos, finalizationAction);
}
repos.values().stream().filter(Objects::nonNull).forEach(Git::close);
repos.values().forEach(Git::close);
} catch (Exception e) {
throw new IOException(e);
} finally {

View File

@ -17,16 +17,6 @@ public interface RepositoryAction {
* An action which executes a system command, as handled by
* {@link ProcessBuilder}. Note that the working directory of the command
* is set to the directory of the repository.
* <p>
* Commands invoked via this method have access to the following
* environment variables:
* </p>
* <ul>
* <li>DISTRIBUGIT_INVOKE_DIR - The directory in which distribugit
* was invoked.</li>
* <li>DISTRIBUGIT_WORKING_DIR - The working directory of distribugit,
* which contains all repositories.</li>
* </ul>
* @param command The command to run.
* @return The command action.
*/
@ -34,8 +24,6 @@ public interface RepositoryAction {
return git -> {
ProcessBuilder pb = new ProcessBuilder(command);
pb.directory(git.getRepository().getWorkTree());
pb.environment().put("DISTRIBUGIT_INVOKE_DIR", pb.directory().getParentFile().getParentFile().getAbsolutePath());
pb.environment().put("DISTRIBUGIT_WORKING_DIR", pb.directory().getParentFile().getAbsolutePath());
pb.inheritIO();
Process p = pb.start();
int result = p.waitFor();

View File

@ -23,12 +23,4 @@ public interface RepositorySelector {
static RepositorySelector from(String... 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;
};
}
}

View File

@ -1,58 +1,40 @@
package nl.andrewl.distribugit.cli;
import nl.andrewl.distribugit.DistribuGit;
import nl.andrewl.distribugit.GitCredentials;
import nl.andrewl.distribugit.RepositoryAction;
import nl.andrewl.distribugit.StatusListener;
import nl.andrewl.distribugit.*;
import nl.andrewl.distribugit.selectors.GitHubSelectorBuilder;
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", description = "DistribuGit command-line tool for performing distributed git operations.", mixinStandardHelpOptions = true)
@CommandLine.Command(name = "distribugit")
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")
public Path workingDir;
private Path workingDir;
@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 = {"-s", "--selector"}, description = "The repository selector to use. Format: \"slug:content\"", required = true)
private String selectorExpression;
@CommandLine.Option(
names = {"-a", "--action"},
description = """
The command to run on each repository.
Each command has access to the following environment variables:
- DISTRIBUGIT_INVOKE_DIR - The directory in which distribugit was invoked.
- DISTRIBUGIT_WORKING_DIR - The working directory of distribugit.
""",
required = true
)
public String actionCommand;
@CommandLine.Option(names = {"-a", "--action"}, description = "The command to run on each repository.", required = true)
private String actionCommand;
@CommandLine.Option(
names = {"-fa", "--finalization-action"},
description = "A command to run on each repository after all normal actions. Has access to the same environment variables as --action."
)
public String finalizationActionCommand;
@CommandLine.Option(names = {"-fa", "--finalization-action"}, description = "A command to run on each repository after all normal actions.")
private String finalizationActionCommand;
@CommandLine.Option(names = {"-t", "--access-token"}, description = "The access token to use to perform operations.")
public String accessToken;
private String accessToken;
@CommandLine.Option(names = {"-sf", "--strict-fail"}, description = "Whether to preemptively fail if any error occurs.", defaultValue = "true")
public boolean strictFail;
private boolean strictFail;
@CommandLine.Option(names = {"-cl", "--cleanup"}, description = "Whether to remove all repository files when done.", defaultValue = "false")
public boolean cleanup;
private boolean cleanup;
@Override
public Integer call() throws Exception {
@ -60,7 +42,7 @@ public class DistribuGitCommand implements Callable<Integer> {
.workingDir(workingDir)
.strictFail(strictFail)
.cleanup(cleanup)
.selector(SelectorExpressionParser.parse(selectorExpression, accessToken))
.selector(parseSelectorExpression(selectorExpression))
.action(RepositoryAction.ofCommand(actionCommand.split("\\s+")));
if (finalizationActionCommand != null) {
builder.finalizationAction(RepositoryAction.ofCommand(finalizationActionCommand.split("\\s+")));
@ -83,6 +65,20 @@ public class DistribuGitCommand implements Callable<Integer> {
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);
}

View File

@ -1,77 +0,0 @@
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;
};
}
}

View File

@ -1,79 +0,0 @@
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) {}
};
}
}