hare

The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

commit b57de9e4a5519acf30834ec6b2e54f4784640074
parent 78bc6a70ba77525024063e41543e29e76b6d6761
Author: Drew DeVault <sir@cmpwn.com>
Date:   Sun, 21 Nov 2021 09:30:17 +0100

hare release: new subcmd (WIP)

Signed-off-by: Drew DeVault <sir@cmpwn.com>

Diffstat:
MMakefile | 5+++--
Mcmd/hare/main.ha | 2++
Acmd/hare/release.ha | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcmd/hare/subcmds.ha | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 229 insertions(+), 2 deletions(-)

diff --git a/Makefile b/Makefile @@ -23,10 +23,11 @@ all: include stdlib.mk hare_srcs=\ + ./cmd/hare/main.ha \ ./cmd/hare/plan.ha \ - ./cmd/hare/subcmds.ha \ + ./cmd/hare/release.ha \ ./cmd/hare/schedule.ha \ - ./cmd/hare/main.ha + ./cmd/hare/subcmds.ha harec_srcs=\ ./cmd/harec/main.ha \ diff --git a/cmd/hare/main.ha b/cmd/hare/main.ha @@ -24,6 +24,8 @@ export fn main() void = { yield &cache; case "deps" => yield &deps; + case "release" => + yield &release; case "run" => yield &run; case "test" => diff --git a/cmd/hare/release.ha b/cmd/hare/release.ha @@ -0,0 +1,170 @@ +use errors; +use fmt; +use io; +use os::exec; +use os; +use strconv; +use strings; +use unix; + +type increment = enum { + MAJOR, + MINOR, + PATCH, +}; + +type modversion = (uint, uint, uint); +type git_error = !exec::exit_status; +type badversion = !void; +type release_error = !(exec::error | io::error | errors::error + | badversion | git_error); + +fn parseversion(in: str) (modversion | badversion) = { + const items = strings::split(in, "."); + defer free(items); + if (len(items) != 3) { + return badversion; + }; + let major = 0u, minor = 0u, patch = 0u; + let ptrs = [&major, &minor, &patch]; + for (let i = 0z; i < len(items); i += 1) { + *ptrs[i] = match (strconv::stou(items[i])) { + case u: uint => + yield u; + case => + return badversion; + }; + }; + return (major, minor, patch); +}; + +fn do_release(incr: increment, dryrun: bool) (void | release_error) = { + // XXX: If we were feeling REALLY fancy we could run the diff and + // automatically detect new functions/types/etc (minor bump), breaking + // changes (major bump), or neither (patch bump). I don't feel that + // fancy, however. + + // TODO: + // - Run hare test + // - Run git tag -a with release notes pre-filled + // - Generate & sign tarballs as git notes + checkbranch()?; + checkstatus()?; + git_runcmd("fetch")?; + checkbehind()?; + + // TODO: Detect if distance from the last tag is zero commits + const lasttag = match (git_readcmd("describe", "--abbrev=0")) { + case git_error => + return do_initial_release(); + case err: release_error => + return err; + case s: str => + yield strings::rtrim(s); + }; + defer free(lasttag); + + const current = parseversion(lasttag)?; + fmt::printfln("current version: {}.{}.{}", + current.0, current.1, current.2)!; + const new: modversion = switch (incr) { + case increment::MAJOR => + yield (current.0 + 1, 0, 0); + case increment::MINOR => + yield (current.0, current.1 + 1, 0); + case increment::PATCH => + yield (current.0, current.1, current.2 + 1); + }; + fmt::printfln("new version: {}.{}.{}", new.0, new.1, new.2)!; + const range = fmt::asprintf("{}..HEAD", lasttag); + defer free(range); + shortlog(os::stdout_file, range)?; +}; + +fn do_initial_release() (void | release_error) = { + fmt::fatal("TODO: Tag initial release"); +}; + +fn checkbranch() (void | release_error) = { + const default_branch = get_defaultbranch()?; + defer free(default_branch); + const current_branch = get_currentbranch()?; + defer free(current_branch); + if (default_branch != current_branch) { + fmt::errorfln( + "Warning! You do not have the {} branch checked out.", + default_branch)!; + }; +}; + +fn checkstatus() (void | release_error) = { + const status = strings::rtrim(git_readcmd("status", "-zuno")?); + defer free(status); + if (len(status) != 0) { + fmt::errorln("Warning! You have uncommitted changes.")!; + }; +}; + +fn checkbehind() (void | release_error) = { + const upstream = match (git_readcmd("rev-parse", "HEAD@{upstream}")) { + case git_error => + // Fails if there is no upstream, in which case we don't need to + // bother checking. + return; + case err: release_error => + return err; + case s: str => + yield s; + }; + defer free(upstream); + const head = git_readcmd("rev-parse", "HEAD")?; + defer free(head); + if (upstream == head) { + return; + }; + match (git_runcmd("merge-base", "--is-ancestor", "HEAD@{upstream}", "HEAD")) { + case git_error => + fmt::errorln("Warning! Your local branch is behind the upstream branch.")!; + case err: release_error => + return err; + case => void; + }; +}; + +fn shortlog(out: io::file, what: str) (void | release_error) = { + const cmd = exec::cmd("git", "shortlog", "--no-merges", what)?; + exec::addfile(&cmd, out, os::stdout_file); + const proc = exec::start(&cmd)?; + const status = exec::wait(&proc)?; + exec::check(&status)?; +}; + +fn git_runcmd(args: str...) (void | release_error) = { + const cmd = exec::cmd("git", args...)?; + const proc = exec::start(&cmd)?; + const status = exec::wait(&proc)?; + return exec::check(&status)?; +}; + +fn git_readcmd(args: str...) (str | release_error) = { + const pipe = unix::pipe()?; + defer io::close(pipe.0); + const cmd = exec::cmd("git", args...)?; + exec::addfile(&cmd, pipe.1, os::stdout_file); + const proc = exec::start(&cmd)?; + io::close(pipe.1); + const result = io::drain(pipe.0)?; + const status = exec::wait(&proc)?; + exec::check(&status)?; + return strings::fromutf8(result); +}; + +fn get_defaultbranch() (str | release_error) = { + const branch = git_readcmd("config", + "--default", "master", "init.defaultBranch")?; + return strings::rtrim(branch); +}; + +fn get_currentbranch() (str | release_error) = { + return strings::rtrim(git_readcmd("branch", "--show-current")?); +}; diff --git a/cmd/hare/subcmds.ha b/cmd/hare/subcmds.ha @@ -1,11 +1,13 @@ use ascii; use bufio; use encoding::utf8; +use errors; use fmt; use fs; use getopt; use hare::ast; use hare::module; +use io; use os::exec; use os; use path; @@ -202,6 +204,58 @@ fn deps(args: []str) void = { abort(); // TODO }; +fn release(args: []str) void = { + const help: []getopt::help = [ + "prepares a new release for a program or library", + ('d', "enable dry-run mode; do not perform any changes"), + "<major|minor|patch|x.y.z>", + ]; + const cmd = getopt::parse(args, help...); + defer getopt::finish(&cmd); + + let dryrun = false; + for (let i = 0z; i < len(cmd.opts); i += 1) { + let opt = cmd.opts[i]; + switch (opt.0) { + case 'd' => + dryrun = true; + case => abort(); + }; + }; + + if (len(cmd.args) == 0) { + getopt::printusage(os::stderr, "release", help); + os::exit(1); + }; + + const increment = switch (cmd.args[0]) { + case "major" => + yield increment::MAJOR; + case "minor" => + yield increment::MINOR; + case "patch" => + yield increment::PATCH; + case => + // TODO: Manually parse version x.y.z + getopt::printusage(os::stderr, "release", help); + os::exit(1); + }; + + match (do_release(increment, dryrun)) { + case void => void; + case err: exec::error => + fmt::fatal(exec::strerror(err)); + case err: errors::error => + fmt::fatal(errors::strerror(err)); + case err: io::error => + fmt::fatal(io::strerror(err)); + case err: git_error => + fmt::fatal("git: {}", exec::exitstr(err)); + case badversion => + fmt::fatal("Error: invalid format string. Hare uses semantic versioning, in the form major.minor.patch."); + }; +}; + fn run(args: []str) void = { const help: []getopt::help = [ "compiles and runs the Hare program at <path>",