diff --git a/.gitignore b/.gitignore index 20e5484..9e98776 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,10 @@ *.a *.so *.dylib -ver -ver-gui +/ver +/ver.exe +/ver-gui +/ver-gui.exe bin/ build/ *.lst diff --git a/source/ver/detect.d b/source/ver/detect.d new file mode 100644 index 0000000..c02b87a --- /dev/null +++ b/source/ver/detect.d @@ -0,0 +1,58 @@ +module ver.detect; + +import std.file : exists, isDir; +import std.path : buildPath; +import ver.types; + +/** Walk upward from `startDir` looking for a VCS root. */ +string findVcsRoot(string startDir) +{ + import std.path : absolutePath, dirName; + + auto dir = absolutePath(startDir); + while (true) + { + if (exists(buildPath(dir, ".git"))) + return dir; + if (exists(buildPath(dir, ".svn")) && isDir(buildPath(dir, ".svn"))) + return dir; + if (exists(buildPath(dir, ".hg")) && isDir(buildPath(dir, ".hg"))) + return dir; + auto parent = dirName(dir); + if (parent == dir) + return null; + dir = parent; + } +} + +/** Detect VCS kind for `dir` (or its nearest ancestor root). */ +VcsKind detectVcs(string dir = ".") +{ + auto root = findVcsRoot(dir); + if (root is null) + return VcsKind.none; + if (exists(buildPath(root, ".git"))) + return VcsKind.git; + if (exists(buildPath(root, ".svn"))) + return VcsKind.svn; + if (exists(buildPath(root, ".hg"))) + return VcsKind.hg; + return VcsKind.unknown; +} + +string vcsKindName(VcsKind kind) +{ + final switch (kind) + { + case VcsKind.none: + return "none"; + case VcsKind.git: + return "git"; + case VcsKind.svn: + return "svn"; + case VcsKind.hg: + return "hg"; + case VcsKind.unknown: + return "unknown"; + } +} diff --git a/source/ver/dispatch.d b/source/ver/dispatch.d new file mode 100644 index 0000000..e522a20 --- /dev/null +++ b/source/ver/dispatch.d @@ -0,0 +1,43 @@ +module ver.dispatch; + +import ver.detect; +import ver.handlers; +import ver.types; + +/** Dispatch a Ver command using the detected VCS under `workDir`. */ +CommandResult dispatch(string command, string[] args, string workDir = ".") +{ + if (command == "help" || command == "--help") + return CommandResult(0, helpText(), "", "ver"); + + if (command == "version" || command == "--version") + { + import ver : verVersion; + return CommandResult(0, "Ver " ~ verVersion ~ "\n", "", "ver"); + } + + if (command == "init") + { + auto kind = detectVcs(workDir); + if (kind == VcsKind.none || kind == VcsKind.git) + return gitHandle("init", args, workDir); + if (kind == VcsKind.hg) + return hgHandle("init", args, workDir); + return CommandResult(2, "", "cannot init: existing non-git/hg VCS", "ver"); + } + + auto kind = detectVcs(workDir); + final switch (kind) + { + case VcsKind.git: + return gitHandle(command, args, workDir); + case VcsKind.svn: + return svnHandle(command, args, workDir); + case VcsKind.hg: + return hgHandle(command, args, workDir); + case VcsKind.none: + return CommandResult(1, "", "No version control system detected. Use 'ver init'.", "ver"); + case VcsKind.unknown: + return CommandResult(1, "", "Unknown VCS detected.", "ver"); + } +} diff --git a/source/ver/handlers.d b/source/ver/handlers.d new file mode 100644 index 0000000..fe1bbb3 --- /dev/null +++ b/source/ver/handlers.d @@ -0,0 +1,127 @@ +module ver.handlers; + +import std.array : join; +import std.process : Config, execute; +import std.string : strip; +import ver.types; + +private CommandResult runTool(string name, string[] args, string workDir) +{ + auto result = execute([name] ~ args, null, Config.none, size_t.max, workDir); + return CommandResult(result.status, result.output, "", name); +} + +/** Git-backed handler for the Ver command vocabulary. */ +CommandResult gitHandle(string command, string[] args, string workDir) +{ + switch (command) + { + case "status": + return runTool("git", ["status", "--short", "--branch"], workDir); + case "history": + return runTool("git", ["log", "--oneline", "-n", "20"], workDir); + case "save": + if (args.length == 0) + return CommandResult(2, "", "save requires a message", "git"); + auto msg = args.join(" "); + auto add = runTool("git", ["add", "-A"], workDir); + if (add.exitCode != 0) + return add; + return runTool("git", ["commit", "-m", msg], workDir); + case "stage": + if (args.length == 0) + return runTool("git", ["add", "-A"], workDir); + return runTool("git", ["add"] ~ args, workDir); + case "sync": + auto pull = runTool("git", ["pull", "--rebase", "--autostash"], workDir); + if (pull.exitCode != 0) + return pull; + return runTool("git", ["push"], workDir); + case "branch": + if (args.length == 0) + return runTool("git", ["branch", "-vv"], workDir); + if (args[0] == "switch" && args.length > 1) + return runTool("git", ["switch", args[1]], workDir); + return runTool("git", ["branch"] ~ args, workDir); + case "init": + return runTool("git", ["init"], workDir); + case "merge": + if (args.length == 0) + return CommandResult(2, "", "merge requires a branch name", "git"); + return runTool("git", ["merge"] ~ args, workDir); + default: + return CommandResult(2, "", "unsupported command for git: " ~ command, "git"); + } +} + +CommandResult svnHandle(string command, string[] args, string workDir) +{ + switch (command) + { + case "status": + return runTool("svn", ["status"], workDir); + case "history": + return runTool("svn", ["log", "-l", "20"], workDir); + case "save": + if (args.length == 0) + return CommandResult(2, "", "save requires a message", "svn"); + return runTool("svn", ["commit", "-m", args.join(" ")], workDir); + case "stage": + return CommandResult(0, "svn has no separate stage step", "", "svn"); + case "sync": + return runTool("svn", ["update"], workDir); + case "init": + return CommandResult(2, "", "use svnadmin create / svn checkout for svn repos", "svn"); + default: + return CommandResult(2, "", "unsupported command for svn: " ~ command, "svn"); + } +} + +CommandResult hgHandle(string command, string[] args, string workDir) +{ + switch (command) + { + case "status": + return runTool("hg", ["status"], workDir); + case "history": + return runTool("hg", ["log", "-l", "20"], workDir); + case "save": + if (args.length == 0) + return CommandResult(2, "", "save requires a message", "hg"); + return runTool("hg", ["commit", "-m", args.join(" ")], workDir); + case "stage": + return CommandResult(0, "hg stages at commit time", "", "hg"); + case "sync": + auto pull = runTool("hg", ["pull", "-u"], workDir); + if (pull.exitCode != 0) + return pull; + return runTool("hg", ["push"], workDir); + case "init": + return runTool("hg", ["init"], workDir); + default: + return CommandResult(2, "", "unsupported command for hg: " ~ command, "hg"); + } +} + +string helpText() +{ + return q"EOS +Ver - Universal Version Control Interface + +Usage: ver [options] + +Commands: + init Initialize a repository (git/hg) + save Commit / save changes with a message + stage [paths...] Stage files (git) or no-op elsewhere + status Show working-tree status + history Show recent history + sync Pull then push (or svn update) + branch [...] List or manage branches + merge Merge a branch (git) + help Show this help + version Show version + +Provider-specific tooling is selected from the nearest VCS root. +EOS".strip; +} diff --git a/source/ver/package.d b/source/ver/package.d new file mode 100644 index 0000000..565d345 --- /dev/null +++ b/source/ver/package.d @@ -0,0 +1,9 @@ +/** Ver — universal version-control interface (library API). */ +module ver; + +public import ver.detect; +public import ver.dispatch; +public import ver.handlers; +public import ver.types; + +enum string verVersion = "0.2.0"; diff --git a/source/ver/types.d b/source/ver/types.d new file mode 100644 index 0000000..1b0d0b7 --- /dev/null +++ b/source/ver/types.d @@ -0,0 +1,20 @@ +module ver.types; + +/** Identifies which VCS backend is active for a working tree. */ +enum VcsKind +{ + none, + git, + svn, + hg, + unknown, +} + +/** Result of running a mapped Ver command. */ +struct CommandResult +{ + int exitCode; + string stdout; + string stderr; + string handlerName; +} diff --git a/ver.pdb b/ver.pdb new file mode 100644 index 0000000..08320b2 Binary files /dev/null and b/ver.pdb differ