Added CLI tool, and more GitHub stuff.
This commit is contained in:
parent
4f0848c2d1
commit
4df5d82c08
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022 Andrew Lalis
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
35
pom.xml
35
pom.xml
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<groupId>nl.andrewl</groupId>
|
<groupId>nl.andrewl</groupId>
|
||||||
<artifactId>distribugit</artifactId>
|
<artifactId>distribugit</artifactId>
|
||||||
<version>1.0.0</version>
|
<version>1.1.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
@ -34,5 +34,38 @@
|
||||||
<version>1.7.36</version>
|
<version>1.7.36</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.kohsuke/github-api -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.kohsuke</groupId>
|
||||||
|
<artifactId>github-api</artifactId>
|
||||||
|
<version>1.303</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/info.picocli/picocli -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>info.picocli</groupId>
|
||||||
|
<artifactId>picocli</artifactId>
|
||||||
|
<version>4.6.3</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>nl.andrewl.distribugit.cli.DistribuGitCommand</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
<descriptorRefs>
|
||||||
|
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||||
|
</descriptorRefs>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
|
@ -9,10 +9,37 @@ 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.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ForkJoinPool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single DistribuGit instance is used to execute a single sequence of
|
||||||
|
* operations on a set of git repositories, as configured according to its
|
||||||
|
* various components.
|
||||||
|
* <p>
|
||||||
|
* A DistribuGit object, when invoking {@link DistribuGit#doActions()} or
|
||||||
|
* {@link DistribuGit#doActionsAsync()}, will perform the following series
|
||||||
|
* of operations:
|
||||||
|
* </p>
|
||||||
|
* <ol>
|
||||||
|
* <li>Call {@link RepositorySelector#getURIs()} to collect the list of
|
||||||
|
* repositories that it will operate on.</li>
|
||||||
|
* <li>Download each repository to the working directory.</li>
|
||||||
|
* <li>Applies the configured {@link RepositoryAction} to all of the
|
||||||
|
* repositories.</li>
|
||||||
|
* <li>If provided, applies the configured finalization action to all of
|
||||||
|
* the repositories.</li>
|
||||||
|
* <li>If needed, all repositories are deleted.</li>
|
||||||
|
* </ol>
|
||||||
|
* <p>
|
||||||
|
* Note that repositories are not guaranteed to be processed in any
|
||||||
|
* particular order.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
public class DistribuGit {
|
public class DistribuGit {
|
||||||
private final RepositorySelector selector;
|
private final RepositorySelector selector;
|
||||||
private final RepositoryAction action;
|
private final RepositoryAction action;
|
||||||
|
private final RepositoryAction finalizationAction;
|
||||||
private final GitCredentials credentials;
|
private final GitCredentials credentials;
|
||||||
private final StatusListener statusListener;
|
private final StatusListener statusListener;
|
||||||
private final Path workingDir;
|
private final Path workingDir;
|
||||||
|
@ -22,9 +49,27 @@ public class DistribuGit {
|
||||||
private int stepsComplete;
|
private int stepsComplete;
|
||||||
private int stepsTotal;
|
private int stepsTotal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a DistribuGit instance.
|
||||||
|
* @param selector A selector that provides a list of repository URIs.
|
||||||
|
* @param action An action to do for each repository.
|
||||||
|
* @param finalizationAction A final action to do for each repository,
|
||||||
|
* after all normal actions are done.
|
||||||
|
* @param credentials The credentials to use to operate on repositories.
|
||||||
|
* @param statusListener A listener that can be used to get information
|
||||||
|
* about the progress of the operations, and any
|
||||||
|
* messages that are emitted.
|
||||||
|
* @param workingDir The directory in which to do all git operations.
|
||||||
|
* @param strictFail Whether to fail instantly if any error occurs. If set
|
||||||
|
* to false, the program will continue even if actions
|
||||||
|
* fail for some repositories.
|
||||||
|
* @param cleanup Whether to perform cleanup after everything is done. This
|
||||||
|
* will remove the working directory once we're done.
|
||||||
|
*/
|
||||||
public DistribuGit(
|
public DistribuGit(
|
||||||
RepositorySelector selector,
|
RepositorySelector selector,
|
||||||
RepositoryAction action,
|
RepositoryAction action,
|
||||||
|
RepositoryAction finalizationAction,
|
||||||
GitCredentials credentials,
|
GitCredentials credentials,
|
||||||
StatusListener statusListener,
|
StatusListener statusListener,
|
||||||
Path workingDir,
|
Path workingDir,
|
||||||
|
@ -33,6 +78,7 @@ public class DistribuGit {
|
||||||
) {
|
) {
|
||||||
this.selector = selector;
|
this.selector = selector;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
|
this.finalizationAction = finalizationAction;
|
||||||
this.credentials = credentials;
|
this.credentials = credentials;
|
||||||
this.statusListener = statusListener;
|
this.statusListener = statusListener;
|
||||||
this.workingDir = workingDir;
|
this.workingDir = workingDir;
|
||||||
|
@ -40,16 +86,35 @@ public class DistribuGit {
|
||||||
this.cleanup = cleanup;
|
this.cleanup = cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void doActions() throws IOException {
|
/**
|
||||||
|
* Performs the configured actions on the selected git repositories.
|
||||||
|
* @throws IOException If an error occurs that requires us to quit early.
|
||||||
|
* This is only thrown if {@link DistribuGit#strictFail} is true.
|
||||||
|
*/
|
||||||
|
public synchronized void doActions() throws IOException {
|
||||||
stepsComplete = 0;
|
stepsComplete = 0;
|
||||||
|
if (Files.exists(workingDir)) {
|
||||||
|
try (var s = Files.list(workingDir)) {
|
||||||
|
if (s.findAny().isPresent()) throw new IOException("Working directory is not empty!");
|
||||||
|
}
|
||||||
|
}
|
||||||
Utils.delete(workingDir); // Delete the directory if it already exists.
|
Utils.delete(workingDir); // Delete the directory if it already exists.
|
||||||
Files.createDirectory(workingDir);
|
Files.createDirectory(workingDir);
|
||||||
statusListener.messageReceived("Prepared temporary directory for repositories.");
|
statusListener.messageReceived("Prepared temporary directory for repositories.");
|
||||||
|
List<String> repositoryURIs;
|
||||||
try {
|
try {
|
||||||
List<String> repositoryURIs = selector.getURIs();
|
repositoryURIs = selector.getURIs();
|
||||||
stepsTotal = 2 * repositoryURIs.size();
|
} catch (Exception e) {
|
||||||
Map<String, Path> repoDirs = downloadRepositories(repositoryURIs);
|
throw new IOException("Could not fetch repository URIs.", e);
|
||||||
applyActionToRepositories(repoDirs);
|
}
|
||||||
|
try {
|
||||||
|
stepsTotal = 3 * repositoryURIs.size();
|
||||||
|
Map<String, Git> repos = downloadRepositories(repositoryURIs);
|
||||||
|
applyActionToRepositories(repos, action);
|
||||||
|
if (finalizationAction != null) {
|
||||||
|
applyActionToRepositories(repos, finalizationAction);
|
||||||
|
}
|
||||||
|
repos.values().forEach(Git::close);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -60,53 +125,93 @@ public class DistribuGit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the configured git actions on all selected repositories in an
|
||||||
|
* asynchronous manner.
|
||||||
|
* @return A future that completes when all actions are complete, or if an
|
||||||
|
* error occurs and the operation quits early.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Void> doActionsAsync() {
|
||||||
|
final CompletableFuture<Void> cf = new CompletableFuture<>();
|
||||||
|
ForkJoinPool.commonPool().submit(() -> {
|
||||||
|
try {
|
||||||
|
doActions();
|
||||||
|
cf.complete(null);
|
||||||
|
} catch (IOException e) {
|
||||||
|
cf.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return cf;
|
||||||
|
}
|
||||||
|
|
||||||
private void completeStep() {
|
private void completeStep() {
|
||||||
stepsComplete++;
|
stepsComplete++;
|
||||||
statusListener.progressUpdated(stepsComplete / (float) stepsTotal * 100f);
|
statusListener.progressUpdated(stepsComplete / (float) stepsTotal * 100f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Path> downloadRepositories(List<String> uris) throws IOException {
|
/**
|
||||||
Map<String, Path> repositoryDirs = new HashMap<>();
|
* Downloads a set of repositories to working directories.
|
||||||
|
* @param uris The repositories to download.
|
||||||
|
* @return A map which maps each repository URI to its {@link Git} instance.
|
||||||
|
* @throws IOException If {@link DistribuGit#strictFail} is set to true,
|
||||||
|
* this will be thrown if an error occurs.
|
||||||
|
*/
|
||||||
|
private Map<String, Git> downloadRepositories(List<String> uris) throws IOException {
|
||||||
|
Map<String, Git> repositoryDirs = new HashMap<>();
|
||||||
int dirIdx = 1;
|
int dirIdx = 1;
|
||||||
for (String repositoryURI : uris) {
|
for (String repositoryURI : uris) {
|
||||||
Path repoDir = workingDir.resolve(Integer.toString(dirIdx++));
|
Path repoDir = workingDir.resolve(Integer.toString(dirIdx++));
|
||||||
|
statusListener.messageReceived("Cloning repository " + repositoryURI + " to " + repoDir);
|
||||||
|
CloneCommand clone = Git.cloneRepository();
|
||||||
try {
|
try {
|
||||||
statusListener.messageReceived("Cloning repository " + repositoryURI + " to " + repoDir);
|
|
||||||
CloneCommand clone = Git.cloneRepository();
|
|
||||||
credentials.addCredentials(clone);
|
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) {
|
} catch (Exception e) {
|
||||||
if (strictFail) {
|
if (strictFail) {
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
|
statusListener.messageReceived("Could not add credentials to repository: " + e.getMessage());
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
// Skip the rest of the logic since this failed. Just go to the next repository.
|
||||||
|
completeStep();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
clone.setDirectory(repoDir.toFile());
|
||||||
|
clone.setURI(repositoryURI);
|
||||||
|
try (var git = clone.call()) {
|
||||||
|
repositoryDirs.put(repositoryURI, git);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (strictFail) {
|
||||||
|
throw new IOException(e);
|
||||||
|
} else {
|
||||||
|
statusListener.messageReceived("Could not clone repository: " + e.getMessage());
|
||||||
|
repositoryDirs.put(repositoryURI, null);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
completeStep();
|
completeStep();
|
||||||
}
|
}
|
||||||
return repositoryDirs;
|
return repositoryDirs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyActionToRepositories(Map<String, Path> repoDirs) throws IOException {
|
/**
|
||||||
for (var entry : repoDirs.entrySet()) {
|
* Applies an action to all git repositories in the given map.
|
||||||
|
* @param repositories A map which maps URIs to {@link Git} instances.
|
||||||
|
* @param action The action to apply to each repository.
|
||||||
|
* @throws IOException If {@link DistribuGit#strictFail} is set to true,
|
||||||
|
* this will be thrown if an error occurs.
|
||||||
|
*/
|
||||||
|
private void applyActionToRepositories(Map<String, Git> repositories, RepositoryAction action) throws IOException {
|
||||||
|
for (var entry : repositories.entrySet()) {
|
||||||
if (entry.getValue() != null) {
|
if (entry.getValue() != null) {
|
||||||
try (Git git = Git.open(entry.getValue().toFile())) {
|
try {
|
||||||
|
Git git = entry.getValue();
|
||||||
statusListener.messageReceived("Applying action to repository " + entry.getKey());
|
statusListener.messageReceived("Applying action to repository " + entry.getKey());
|
||||||
action.doAction(git);
|
action.doAction(git);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (strictFail) {
|
if (strictFail) {
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
|
statusListener.messageReceived("Action could not be applied to repository: " + e.getMessage());
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -116,9 +221,14 @@ public class DistribuGit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder class to help with constructing {@link DistribuGit} instances
|
||||||
|
* with a fluent method interface.
|
||||||
|
*/
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
private RepositorySelector selector;
|
private RepositorySelector selector;
|
||||||
private RepositoryAction action;
|
private RepositoryAction action;
|
||||||
|
private RepositoryAction finalizationAction;
|
||||||
private GitCredentials credentials = cmd -> {};
|
private GitCredentials credentials = cmd -> {};
|
||||||
private StatusListener statusListener = new StatusListener() {
|
private StatusListener statusListener = new StatusListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -145,6 +255,11 @@ public class DistribuGit {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder finalizationAction(RepositoryAction finalizationAction) {
|
||||||
|
this.finalizationAction = finalizationAction;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder credentials(GitCredentials credentials) {
|
public Builder credentials(GitCredentials credentials) {
|
||||||
this.credentials = credentials;
|
this.credentials = credentials;
|
||||||
return this;
|
return this;
|
||||||
|
@ -171,7 +286,10 @@ public class DistribuGit {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DistribuGit build() {
|
public DistribuGit build() {
|
||||||
return new DistribuGit(selector, action, credentials, statusListener, workingDir, strictFail, cleanup);
|
if (selector == null || action == null) {
|
||||||
|
throw new IllegalStateException("Cannot build an instance of DistribuGit without a selector or action.");
|
||||||
|
}
|
||||||
|
return new DistribuGit(selector, action, finalizationAction, credentials, statusListener, workingDir, strictFail, cleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,17 @@ import org.eclipse.jgit.util.FS;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supplies credentials information to a git command, which is needed when
|
||||||
|
* interacting with private repositories, or when pushing changes to a
|
||||||
|
* repository.
|
||||||
|
*/
|
||||||
public interface GitCredentials {
|
public interface GitCredentials {
|
||||||
|
/**
|
||||||
|
* Adds credentials information to the given command.
|
||||||
|
* @param gitCommand The command to add credentials to.
|
||||||
|
* @throws Exception If an error occurs while adding credentials.
|
||||||
|
*/
|
||||||
void addCredentials(TransportCommand<?, ?> gitCommand) throws Exception;
|
void addCredentials(TransportCommand<?, ?> gitCommand) throws Exception;
|
||||||
|
|
||||||
static GitCredentials ofUsernamePassword(String username, String password) {
|
static GitCredentials ofUsernamePassword(String username, String password) {
|
||||||
|
|
|
@ -2,7 +2,15 @@ package nl.andrewl.distribugit;
|
||||||
|
|
||||||
import org.eclipse.jgit.api.Git;
|
import org.eclipse.jgit.api.Git;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that can be applied to a git repository.
|
||||||
|
*/
|
||||||
public interface RepositoryAction {
|
public interface RepositoryAction {
|
||||||
|
/**
|
||||||
|
* Performs the action on the given git repository.
|
||||||
|
* @param git A reference to the git repository.
|
||||||
|
* @throws Exception If an error occurs during the action.
|
||||||
|
*/
|
||||||
void doAction(Git git) throws Exception;
|
void doAction(Git git) throws Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,6 +9,11 @@ import java.util.List;
|
||||||
* Component which produces a list of repositories to operate on.
|
* Component which produces a list of repositories to operate on.
|
||||||
*/
|
*/
|
||||||
public interface RepositorySelector {
|
public interface RepositorySelector {
|
||||||
|
/**
|
||||||
|
* Gets a list of repository URIs to operate on.
|
||||||
|
* @return A list of repository URIs.
|
||||||
|
* @throws Exception If an error occurs while fetching the URIs.
|
||||||
|
*/
|
||||||
List<String> getURIs() throws Exception;
|
List<String> getURIs() throws Exception;
|
||||||
|
|
||||||
static RepositorySelector fromCollection(Collection<String> uris) {
|
static RepositorySelector fromCollection(Collection<String> uris) {
|
||||||
|
|
|
@ -1,6 +1,18 @@
|
||||||
package nl.andrewl.distribugit;
|
package nl.andrewl.distribugit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for updates during {@link DistribuGit#doActions()}.
|
||||||
|
*/
|
||||||
public interface StatusListener {
|
public interface StatusListener {
|
||||||
|
/**
|
||||||
|
* Called when the operation's progress is updated.
|
||||||
|
* @param percentage The percentage (0 - 100) complete.
|
||||||
|
*/
|
||||||
void progressUpdated(float percentage);
|
void progressUpdated(float percentage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the DistribuGit operation emits a message.
|
||||||
|
* @param message The message that was emitted.
|
||||||
|
*/
|
||||||
void messageReceived(String message);
|
void messageReceived(String message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
package nl.andrewl.distribugit.cli;
|
||||||
|
|
||||||
|
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")
|
||||||
|
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")
|
||||||
|
private Path workingDir;
|
||||||
|
|
||||||
|
@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.", required = true)
|
||||||
|
private String actionCommand;
|
||||||
|
|
||||||
|
@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.")
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"-sf", "--strict-fail"}, description = "Whether to preemptively fail if any error occurs.", defaultValue = "true")
|
||||||
|
private boolean strictFail;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"-cl", "--cleanup"}, description = "Whether to remove all repository files when done.", defaultValue = "false")
|
||||||
|
private boolean cleanup;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer call() throws Exception {
|
||||||
|
var builder = new DistribuGit.Builder()
|
||||||
|
.workingDir(workingDir)
|
||||||
|
.strictFail(strictFail)
|
||||||
|
.cleanup(cleanup)
|
||||||
|
.selector(parseSelectorExpression(selectorExpression))
|
||||||
|
.action(RepositoryAction.ofCommand(actionCommand.split("\\s+")));
|
||||||
|
if (finalizationActionCommand != null) {
|
||||||
|
builder.finalizationAction(RepositoryAction.ofCommand(finalizationActionCommand.split("\\s+")));
|
||||||
|
}
|
||||||
|
if (accessToken != null) {
|
||||||
|
builder.credentials(GitCredentials.ofUsernamePassword(accessToken, ""));
|
||||||
|
}
|
||||||
|
builder.statusListener(new StatusListener() {
|
||||||
|
@Override
|
||||||
|
public void progressUpdated(float percentage) {
|
||||||
|
System.out.printf("Progress: %.1f%%%n", percentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void messageReceived(String message) {
|
||||||
|
System.out.println(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.build().doActions();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package nl.andrewl.distribugit.selectors;
|
||||||
|
|
||||||
|
import org.kohsuke.github.GHRepository;
|
||||||
|
import org.kohsuke.github.GitHub;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface GitHubRepositorySelector {
|
||||||
|
List<GHRepository> getRepos(GitHub gh) throws Exception;
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package nl.andrewl.distribugit.selectors;
|
||||||
|
|
||||||
|
import nl.andrewl.distribugit.RepositorySelector;
|
||||||
|
import org.kohsuke.github.GHOrganization;
|
||||||
|
import org.kohsuke.github.GHRepository;
|
||||||
|
import org.kohsuke.github.GitHub;
|
||||||
|
import org.kohsuke.github.GitHubBuilder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that can be used to construct {@link RepositorySelector} instances
|
||||||
|
* that fetch repositories from GitHub via its API.
|
||||||
|
*/
|
||||||
|
public record GitHubSelectorBuilder(GitHub gh) {
|
||||||
|
public static GitHubSelectorBuilder fromPersonalAccessToken(String token) throws IOException {
|
||||||
|
return fromPersonalAccessToken(token, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GitHubSelectorBuilder fromPersonalAccessToken(String token, String userOrOrgName) throws IOException {
|
||||||
|
return new GitHubSelectorBuilder(new GitHubBuilder().withOAuthToken(token, userOrOrgName).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select repositories using an organization name, and a prefix to match
|
||||||
|
* against all repositories in that organization.
|
||||||
|
* @param orgName The name of the organization.
|
||||||
|
* @param prefix The prefix to use.
|
||||||
|
* @return A selector that selects matching repositories.
|
||||||
|
*/
|
||||||
|
public RepositorySelector orgAndPrefix(String orgName, String prefix) {
|
||||||
|
return () -> {
|
||||||
|
List<String> repoURIs = new ArrayList<>();
|
||||||
|
GHOrganization org = gh.getOrganization(orgName);
|
||||||
|
for (GHRepository repo : org.listRepositories()) {
|
||||||
|
if (repo.getName().startsWith(prefix)) {
|
||||||
|
repoURIs.add(repo.getHttpTransportUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return repoURIs;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom selector that can be used to perform some operations using the
|
||||||
|
* GitHub API and return a list of repositories.
|
||||||
|
* @param selector The GitHub selector to use.
|
||||||
|
* @return A selector that selects GitHub repositories.
|
||||||
|
*/
|
||||||
|
public RepositorySelector custom(GitHubRepositorySelector selector) {
|
||||||
|
return () -> selector.getRepos(gh).stream().map(GHRepository::getHttpTransportUrl).toList();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue