-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: GooeyCLI - replacement utility scripting system based on PicoCLI.
Still a proof of concept, with a limited amount of old functionality converted to showcase the new approach. Relies on a groovyw tweak, renames the scripts, and lets PicoCLI worry about all the CLI structuring
- Loading branch information
Showing
20 changed files
with
423 additions
and
1,188 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// Copyright 2020 The Terasology Foundation | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import picocli.CommandLine.Command | ||
|
||
/** | ||
* Simple super class for commands with global style options | ||
*/ | ||
@Command( | ||
synopsisHeading = "%n@|green Usage:|@%n%n", | ||
descriptionHeading = "%n@|green Description:|@%n%n", | ||
parameterListHeading = "%n@|green Parameters:|@%n%n", | ||
optionListHeading = "%n@|green Options:|@%n%n", | ||
commandListHeading = "%n@|green Commands:|@%n%n") | ||
class BaseCommand { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Copyright 2020 The Terasology Foundation | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import picocli.CommandLine.ParentCommand | ||
import picocli.CommandLine.Command | ||
import picocli.CommandLine.Mixin // Actually in use, annotation below may show syntax error due to Groovy's annotation by the same name. Works fine | ||
import picocli.CommandLine.Parameters | ||
|
||
/** | ||
* Sub-sub-command that works on item-oriented sub-commands. | ||
* Mixes-in GitOptions to vary the origin if indicated by the user | ||
* Distinct from the sibling command add-remote which does *not* mix in GitOptions since the command itself involves git remote | ||
*/ | ||
@Command(name = "get", description = "Gets one or more items directly") | ||
class Get extends BaseCommand implements Runnable { | ||
|
||
/** Reference to the parent item command so we can figure out what type it is */ | ||
@ParentCommand | ||
ItemCommand parent | ||
|
||
/** Mix in a variety of supported Git extras */ | ||
@Mixin | ||
GitOptions gitOptions | ||
|
||
@Parameters(paramLabel = "items", arity = "1", description = "Target item(s) to get") | ||
List<String> items | ||
|
||
void run() { | ||
println "Going to get $items! And from origin: " + gitOptions.origin | ||
|
||
// The parent should be a ManagedItem. Make an instace including the possible git origin option | ||
ManagedItem mi = parent.getManager(gitOptions.origin) | ||
|
||
// Having prepared an instance of the logic class we call it to actually retrieve stuff | ||
mi.retrieve(items, false) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Copyright 2020 The Terasology Foundation | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import picocli.CommandLine.Option | ||
|
||
/** | ||
* A mix-in meant to supply some general Git-related options | ||
*/ | ||
class GitOptions { | ||
@Option(names = [ "-o", "--origin"], description = "Which Git origin (account) to target") | ||
String origin | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// Grab the Groovy extensions for PicoCLI - in IntelliJ Alt-ENTER on a `@Grab` to register contents for syntax highlighting | ||
@Grab('info.picocli:picocli-groovy:4.3.2') | ||
// TODO: Actually exists inside the Gradle Wrapper - gradle-6.4.1\lib\groovy-all-1.3-2.5.10.jar\groovyjarjarpicocli\ | ||
|
||
// TODO: Unsure if this helps or should be included - don't really need this since we execute via Groovy Wrapper anyway | ||
@GrabExclude('org.codehaus.groovy:groovy-all') | ||
|
||
// Needed for colors to work on Windows, along with a mode toggle at the start and end of execution in main | ||
@Grab('org.fusesource.jansi:jansi:1.18') // TODO: Exists at 1.17 inside the Gradle Wrapper lib - can use that one? | ||
import org.fusesource.jansi.AnsiConsole | ||
|
||
import picocli.CommandLine | ||
import picocli.CommandLine.Command | ||
import picocli.CommandLine.HelpCommand | ||
|
||
// If using local groovy files the subcommands section may highlight as bad syntax in IntelliJ - that's OK | ||
@Command(name = "gw", | ||
synopsisSubcommandLabel = "COMMAND", // Default is [COMMAND] indicating optional, but sub command here is required | ||
subcommands = [ | ||
HelpCommand.class, // Adds standard help options (help as a subcommand, -h, and --help) | ||
Module.class, | ||
Init.class], // Note that these Groovy classes *must* start with a capital letter for some reason | ||
description = "Utility system for interacting with a Terasology developer workspace") | ||
class GooeyCLI extends BaseCommand { | ||
static void main(String[] args) { | ||
AnsiConsole.systemInstall() // enable colors on Windows - TODO: Test on not-so-Windows systems, should those not run this? | ||
CommandLine cmd = new CommandLine(new GooeyCLI()) | ||
if (args.length == 0) { | ||
cmd.usage(System.out) | ||
} | ||
else { | ||
cmd.execute(args) | ||
} | ||
AnsiConsole.systemUninstall() // cleanup when done | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// Copyright 2020 The Terasology Foundation | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import picocli.CommandLine.Command | ||
import picocli.CommandLine.Help.Ansi | ||
import picocli.CommandLine.Mixin // Is in use, IDE may think the Groovy-supplied is in use below and mark this unused | ||
import picocli.CommandLine.Parameters | ||
|
||
@Command(name = "init", description = "Initializes a workspace with some useful things") | ||
class Init extends BaseCommand implements Runnable { | ||
|
||
/** The name of the distro, if given. Optional parameter (the arity = 0..1 bit) */ | ||
@Parameters(paramLabel = "distro", arity = "0..1", defaultValue = "sample", description = "Target module distro to prepare locally") | ||
String distro | ||
|
||
/** Mix in a variety of supported Git extras */ | ||
@Mixin | ||
GitOptions gitOptions | ||
|
||
void run() { | ||
String str = Ansi.AUTO.string("@|bold,green,underline Time to initialize $distro !|@") | ||
System.out.println(str) | ||
println "Do we have a Git origin override? " + gitOptions.origin | ||
println "Can has desired global prop? " + PropHelper.getGlobalProp("alternativeGithubHome") | ||
// Call logic elsewhere | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// Copyright 2020 The Terasology Foundation | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
/** | ||
* Simple command type class that indicates the command deals with "items" - nested Git roots representing application elements | ||
*/ | ||
abstract class ItemCommand extends BaseCommand { | ||
/** | ||
* Return a manager class for interacting with the specific type of item | ||
* @param optionGitOrigin if the user indicated an alternative Git origin it will be used to vary some URLs | ||
* @return the instantiated item type manager class | ||
*/ | ||
abstract ManagedItem getManager(String optionGitOrigin) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
// Copyright 2020 The Terasology Foundation | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
@GrabResolver(name = 'jcenter', root = 'http://jcenter.bintray.com/') | ||
@Grab(group = 'org.ajoberstar', module = 'grgit', version = '1.9.3') | ||
import org.ajoberstar.grgit.Grgit | ||
|
||
/** | ||
* Utility class for dealing with items managed in a developer workspace. | ||
* | ||
* Primarily assists with the retrieval, creation, and updating of nested Git roots representing application elements. | ||
*/ | ||
abstract class ManagedItem { | ||
|
||
/** For keeping a list of items retrieved so far (retrieval calls may be recursive) */ | ||
def itemsRetrieved = [] | ||
|
||
/** The default name of a git remote we might want to work on or keep handy */ | ||
String defaultRemote = "origin" // TODO: Consider always naming remotes after their origin / account name? | ||
|
||
String displayName | ||
abstract String getDisplayName() | ||
|
||
File targetDirectory | ||
abstract File getTargetDirectory() | ||
|
||
String githubTargetHome | ||
abstract String getDefaultItemGitOrigin() | ||
|
||
ManagedItem() { | ||
displayName = getDisplayName() | ||
targetDirectory = getTargetDirectory() | ||
githubTargetHome = calculateGitOrigin(null) | ||
} | ||
|
||
ManagedItem(String optionGitOrigin) { | ||
displayName = getDisplayName() | ||
targetDirectory = getTargetDirectory() | ||
githubTargetHome = calculateGitOrigin(optionGitOrigin) | ||
} | ||
|
||
String calculateGitOrigin(String optionOrigin) { | ||
// If the user indicated a target Git origin via option parameter then use that (primary choice) | ||
if (optionOrigin != null) { | ||
println "We have an option set for Git origin so using that: " + optionOrigin | ||
return optionOrigin | ||
} | ||
|
||
// Alternatively if the user has a global override set for Git origin then use that (secondary choice) | ||
String altOrigin = PropHelper.getGlobalProp("alternativeGithubHome") | ||
if (altOrigin != null) { | ||
println "There was no option set but we have a global proper override for Git origin: " + altOrigin | ||
return altOrigin | ||
} | ||
|
||
// And finally if neither override is set fall back on the default defined by the item type | ||
println "No option nor global override set for Git origin so using default for the type: " + getDefaultItemGitOrigin() | ||
return getDefaultItemGitOrigin() | ||
} | ||
|
||
// TODO: Likely everything below should just delegate to more specific classes to keep things tidy | ||
// TODO: That would allow these methods to later just figure out exact required operations then delegate | ||
// TODO: Should make it easier to hide the logic of (for instance) different Git adapters behind the next step | ||
|
||
/** | ||
* Tests a URL via a HEAD request (no body) to see if it is valid | ||
* @param url the URL to test | ||
* @return boolean indicating whether the URL is valid (code 200) or not | ||
*/ | ||
boolean isUrlValid(String url) { | ||
def code = new URL(url).openConnection().with { | ||
requestMethod = 'HEAD' | ||
connect() | ||
responseCode | ||
} | ||
return code.toString() == "200" | ||
} | ||
|
||
/** | ||
* Primary entry point for retrieving items, kicks off recursively if needed. | ||
* @param items the items we want to retrieve | ||
* @param recurse whether to also retrieve dependencies of the desired items (only really for modules ...) | ||
*/ | ||
def retrieve(List<String> items, boolean recurse) { | ||
println "Now inside retrieve, user (recursively? $recurse) wants: $items" | ||
for (String itemName : items) { | ||
println "Starting retrieval for $displayName $itemName, are we recursing? $recurse" | ||
println "Retrieved so far: $itemsRetrieved" | ||
retrieveItem(itemName, recurse) | ||
} | ||
} | ||
|
||
/** | ||
* Retrieves a single item via Git Clone. Considers whether it exists locally first or if it has already been retrieved this execution. | ||
* @param itemName the target item to retrieve | ||
* @param recurse whether to also retrieve its dependencies (if so then recurse back into retrieve) | ||
*/ | ||
def retrieveItem(String itemName, boolean recurse) { | ||
File itemDir = new File(targetDirectory, itemName) | ||
println "Request to retrieve $displayName $itemName would store it at $itemDir - exists? " + itemDir.exists() | ||
if (itemDir.exists()) { | ||
println "That $displayName already had an existing directory locally. If something is wrong with it please delete and try again" | ||
itemsRetrieved << itemName | ||
} else if (itemsRetrieved.contains(itemName)) { | ||
println "We already retrieved $itemName - skipping" | ||
} else { | ||
itemsRetrieved << itemName | ||
def targetUrl = "https://github.com/${githubTargetHome}/${itemName}" | ||
if (!isUrlValid(targetUrl)) { | ||
println "Can't retrieve $displayName from $targetUrl - URL appears invalid. Typo? Not created yet?" | ||
return | ||
} | ||
println "Retrieving $displayName $itemName from $targetUrl" | ||
if (githubTargetHome != getDefaultItemGitOrigin()) { | ||
println "Doing a retrieve from a custom remote: $githubTargetHome - will name it as such plus add the ${getDefaultItemGitOrigin()} remote as '$defaultRemote'" | ||
Grgit.clone dir: itemDir, uri: targetUrl, remote: githubTargetHome | ||
println "Primary clone operation complete, about to add the '$defaultRemote' remote for the ${getDefaultItemGitOrigin()} org address" | ||
//addRemote(itemName, defaultRemote, "https://github.com/${getDefaultItemGitOrigin()}/${itemName}") //TODO: Add me :p | ||
} else { | ||
println "Cloning $targetUrl to $itemDir" | ||
Grgit.clone dir: itemDir, uri: targetUrl | ||
} | ||
/* | ||
// This step allows the item type to check the newly cloned item and add in extra template stuff - TODO? | ||
//itemTypeScript.copyInTemplateFiles(itemDir) | ||
// Handle also retrieving dependencies if the item type cares about that | ||
if (recurse) { | ||
def foundDependencies = itemTypeScript.findDependencies(itemDir) | ||
if (foundDependencies.length == 0) { | ||
println "The $itemType $itemName did not appear to have any dependencies we need to worry about" | ||
} else { | ||
println "The $itemType $itemName has the following $itemType dependencies we care about: $foundDependencies" | ||
String[] uniqueDependencies = foundDependencies - itemsRetrieved | ||
println "After removing dupes already retrieved we have the remaining dependencies left: $uniqueDependencies" | ||
if (uniqueDependencies.length > 0) { | ||
retrieve(uniqueDependencies, true) | ||
} | ||
} | ||
}*/ | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// Copyright 2020 The Terasology Foundation | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
class ManagedModule extends ManagedItem { | ||
ManagedModule() { | ||
super() | ||
} | ||
|
||
ManagedModule(String optionGitOrigin) { | ||
super(optionGitOrigin) | ||
} | ||
|
||
@Override | ||
String getDisplayName() { | ||
return "module" | ||
} | ||
|
||
@Override | ||
File getTargetDirectory() { | ||
return new File("modules") | ||
} | ||
|
||
@Override | ||
String getDefaultItemGitOrigin() { | ||
return "Terasology" | ||
} | ||
|
||
/** | ||
* Copies in a fresh copy of build.gradle for all modules (in case changes are made and need to be propagated) | ||
*/ | ||
void refreshGradle() { | ||
targetDirectory.eachDir() { dir -> | ||
File targetDir = new File(targetDirectory, dir.name) | ||
|
||
// Copy in the template build.gradle for modules | ||
if (!new File(targetDir, "module.txt").exists()) { | ||
println "$targetDir has no module.txt, it must not want a fresh build.gradle" | ||
return | ||
} | ||
println "In refreshGradle for module $targetDir - copying in a fresh build.gradle" | ||
File targetBuildGradle = new File(targetDir, 'build.gradle') | ||
targetBuildGradle.delete() | ||
targetBuildGradle << new File('templates/build.gradle').text | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// Copyright 2020 The Terasology Foundation | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
/** | ||
* Convenience class for pulling properties out of a 'gradle.properties' if present, for various overrides | ||
* // TODO: YAML variant that can be added for more complex use cases? | ||
*/ | ||
class PropHelper { | ||
|
||
static String getGlobalProp(String key) { | ||
Properties extraProps = new Properties() | ||
File gradlePropsFile = new File("gradle.properties") | ||
if (gradlePropsFile.exists()) { | ||
gradlePropsFile.withInputStream { | ||
extraProps.load(it) | ||
} | ||
//println "Found a 'gradle.properties' file, loaded in global overrides: " + extraProps | ||
|
||
if (extraProps.containsKey(key)) { | ||
println "Returning found global prop for $key" | ||
return extraProps.get(key) | ||
} | ||
println "Didn't find a global prop for key $key" | ||
|
||
} else { | ||
println "No 'gradle.properties' file found, not supplying global overrides" | ||
} | ||
return null | ||
} | ||
} |
Oops, something went wrong.