SetupMojo.java
package network.ike.plugin;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.List;
import java.util.Set;
/**
* Install VCS bridge git hooks to {@code ~/.git-hooks/}.
*
* <p>Installs the pre-commit, post-commit, and pre-push hooks that
* coordinate git state across Syncthing-paired machines. Only the
* VCS bridge hooks are written — existing hooks (prepare-commit-msg,
* commit-msg, post-checkout) are never touched.
*
* <p>After installation, verifies that {@code core.hooksPath} is
* configured to point to {@code ~/.git-hooks/}.
*
* <p>Usage: {@code mvnw ike:setup}
*/
@Mojo(name = "setup", projectRequired = false)
public class SetupMojo implements org.apache.maven.api.plugin.Mojo {
@org.apache.maven.api.di.Inject
private org.apache.maven.api.plugin.Log log;
/**
* Access the Maven logger.
*
* @return the logger
*/
protected org.apache.maven.api.plugin.Log getLog() { return log; }
/** Creates this goal instance. */
public SetupMojo() {}
/** VCS bridge hook filenames — only these are written. */
private static final List<String> VCS_HOOKS =
List.of("pre-commit", "post-commit", "pre-push");
/**
* Force overwrite of existing VCS bridge hooks without prompting.
*/
@Parameter(property = "force", defaultValue = "false")
boolean force;
@Override
public void execute() throws MojoException {
getLog().info("");
getLog().info("IKE VCS Bridge — Setup");
getLog().info("══════════════════════════════════════════════════════════════");
Path hooksDir = Path.of(System.getProperty("user.home"), ".git-hooks");
// Create hooks directory if it doesn't exist
try {
Files.createDirectories(hooksDir);
} catch (IOException e) {
throw new MojoException(
"Failed to create hooks directory: " + hooksDir, e);
}
// Install each VCS bridge hook
int installed = 0;
for (String hookName : VCS_HOOKS) {
Path target = hooksDir.resolve(hookName);
if (Files.exists(target) && !force) {
getLog().info(" " + hookName + ": already exists (use -Dforce=true to overwrite)");
continue;
}
String content = readResource("/hooks/" + hookName);
try {
Files.writeString(target, content, StandardCharsets.UTF_8);
setExecutable(target);
getLog().info(" " + hookName + ": installed");
installed++;
} catch (IOException e) {
throw new MojoException(
"Failed to write hook: " + target, e);
}
}
getLog().info("");
if (installed > 0) {
getLog().info(" Installed " + installed + " hook(s) to " + hooksDir);
} else {
getLog().info(" All hooks already present.");
}
// Ensure global gitignore entries
getLog().info("");
ensureGlobalGitignore();
// Verify core.hooksPath
getLog().info("");
checkHooksPath(hooksDir);
getLog().info("");
}
private String readResource(String path) throws MojoException {
try (InputStream is = getClass().getResourceAsStream(path)) {
if (is == null) {
throw new MojoException(
"Hook resource not found on classpath: " + path);
}
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new MojoException(
"Failed to read hook resource: " + path, e);
}
}
private void setExecutable(Path file) {
try {
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file);
perms.add(PosixFilePermission.OWNER_EXECUTE);
perms.add(PosixFilePermission.GROUP_EXECUTE);
perms.add(PosixFilePermission.OTHERS_EXECUTE);
Files.setPosixFilePermissions(file, perms);
} catch (UnsupportedOperationException e) {
// Windows — Git Bash handles execute permission via git config
getLog().debug("POSIX permissions not supported (Windows); skipping chmod.");
} catch (IOException e) {
getLog().warn("Could not set execute permission on " + file
+ ": " + e.getMessage());
}
}
/**
* Ensure standard entries exist in the global gitignore file.
* Creates the file if it doesn't exist.
*/
private void ensureGlobalGitignore() {
Path globalIgnore = Path.of(System.getProperty("user.home"), ".gitignore_global");
try {
String content = Files.exists(globalIgnore)
? Files.readString(globalIgnore, StandardCharsets.UTF_8) : "";
StringBuilder additions = new StringBuilder();
if (!content.contains("_git-init")) {
additions.append("_git-init*\n");
}
if (!content.contains("vcs-state")) {
additions.append(".ike/vcs-state\n");
}
if (additions.isEmpty()) {
getLog().info(" Global gitignore: already current ✓");
return;
}
String updated = content + (content.endsWith("\n") ? "" : "\n") + additions;
Files.writeString(globalIgnore, updated, StandardCharsets.UTF_8);
getLog().info(" Global gitignore: updated (" + globalIgnore + ")");
} catch (IOException e) {
getLog().warn(" Could not update global gitignore: " + e.getMessage());
}
}
private void checkHooksPath(Path expectedDir) throws MojoException {
try {
Process proc = new ProcessBuilder(
"git", "config", "--global", "core.hooksPath")
.redirectErrorStream(false)
.start();
String output;
try (var reader = new java.io.BufferedReader(
new java.io.InputStreamReader(proc.getInputStream(),
StandardCharsets.UTF_8))) {
output = reader.lines()
.collect(java.util.stream.Collectors.joining())
.trim();
}
int exit = proc.waitFor();
if (exit != 0 || output.isEmpty()) {
getLog().warn(" core.hooksPath is not set.");
getLog().warn(" Run: git config --global core.hooksPath "
+ expectedDir);
} else {
Path actual = Path.of(output).toAbsolutePath().normalize();
Path expected = expectedDir.toAbsolutePath().normalize();
if (actual.equals(expected)) {
getLog().info(" core.hooksPath: " + output + " ✓");
} else {
getLog().warn(" core.hooksPath is set to: " + output);
getLog().warn(" Expected: " + expectedDir);
getLog().warn(" Hooks may not activate. Update with:");
getLog().warn(" git config --global core.hooksPath "
+ expectedDir);
}
}
} catch (IOException | InterruptedException e) {
getLog().warn(" Could not check core.hooksPath: " + e.getMessage());
}
}
}