Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
*.a
*.so
*.dylib
ver
ver-gui
/ver
/ver.exe
/ver-gui
/ver-gui.exe
bin/
build/
*.lst
Expand Down
58 changes: 58 additions & 0 deletions source/ver/detect.d
Original file line number Diff line number Diff line change
@@ -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";
}
}
43 changes: 43 additions & 0 deletions source/ver/dispatch.d
Original file line number Diff line number Diff line change
@@ -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");
}
}
127 changes: 127 additions & 0 deletions source/ver/handlers.d
Original file line number Diff line number Diff line change
@@ -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 <command> [options]

Commands:
init Initialize a repository (git/hg)
save <msg> 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 <branch> Merge a branch (git)
help Show this help
version Show version

Provider-specific tooling is selected from the nearest VCS root.
EOS".strip;
}
9 changes: 9 additions & 0 deletions source/ver/package.d
Original file line number Diff line number Diff line change
@@ -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";
20 changes: 20 additions & 0 deletions source/ver/types.d
Original file line number Diff line number Diff line change
@@ -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;
}
Binary file added ver.pdb
Binary file not shown.
Loading