Skip to content

Commit

Permalink
Feature: Add --probSpecsDir option
Browse files Browse the repository at this point in the history
The user can now tell the program to use an existing
`problem-specifications` directory, in which case it doesn't make a
temporary shallow clone.

This saves a significant amount of time, and is probably the biggest
possible speed optimisation given that the program running time in
non-interactive mode is essentially the time it takes to establish an
up-to-date `problem-specifications` directory. But we could add an
"offline" option in the future.

This commit also makes it easier to add tests.

Decisions:
- Show an error if the given `problem-specifications` working directory
  is not clean, rather than asking the user or stashing changes.
- Try to be strict about the user's `problem-specifications` directory,
  and do not silently `checkout` or `merge`.
- Don't assume the user has a remote named 'upstream'.
- Allow the given `problem-specifications` repo to be a shallow clone:
  don't check that it's actually a `problem-specifications` repo by
  requiring it to contain some specified SHA-1.

Closes: exercism#47
  • Loading branch information
ee7 committed Oct 21, 2020
1 parent 787b22d commit 7e242e6
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 10 deletions.
11 changes: 9 additions & 2 deletions src/cli.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ type
exercise*: Option[string]
mode*: Mode
verbosity*: Verbosity
probSpecsDir*: Option[string]

Opt = enum
optExercise, optCheck, optMode, optVerbosity, optHelp, optVersion
optExercise, optCheck, optMode, optVerbosity, optProbSpecsDir,
optHelp, optVersion

OptKey = tuple
short: string
Expand All @@ -32,6 +34,7 @@ const
("c", "check"),
("m", "mode"),
("o", "verbosity"),
("p", "probSpecsDir"),
("h", "help"),
("v", "version"),
]
Expand All @@ -54,6 +57,7 @@ Options:
-{optCheck.short}, --{optCheck.long} Check if there are missing tests. Doesn't update the tests. Terminates with a non-zero exit code if one or more tests are missing
-{optMode.short}, --{optMode.long} <mode> What to do with missing test cases. Allowed values: c[hoose], i[nclude], e[xclude]
-{optVerbosity.short}, --{optVerbosity.long} <verbosity> The verbosity of output. Allowed values: q[uiet], n[ormal], d[etailed]
-{optProbSpecsDir.short}, --{optProbSpecsDir.long} <dir> Use this `problem-specifications` directory, rather than cloning temporarily
-{optHelp.short}, --{optHelp.long} Show this help message and exit
-{optVersion.short}, --{optVersion.long} Show this tool's version information and exit"""

Expand All @@ -63,7 +67,7 @@ proc showVersion =
echo &"Canonical Data Syncer v{NimblePkgVersion}"
quit(0)

proc showError(s: string) =
proc showError*(s: string) =
stdout.styledWrite(fgRed, "Error: ")
stdout.write(s)
stdout.write("\n\n")
Expand Down Expand Up @@ -134,6 +138,9 @@ proc processCmdLine*: Conf =
of optVerbosity.short, optVerbosity.long:
showErrorForMissingVal(kind, key, val)
result.verbosity = parseVerbosity(kind, key, val)
of optProbSpecsDir.short, optProbSpecsDir.long:
showErrorForMissingVal(kind, key, val)
result.probSpecsDir = some(val)
of optHelp.short, optHelp.long:
showHelp()
of optVersion.short, optVersion.long:
Expand Down
79 changes: 71 additions & 8 deletions src/probspecs.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import std/[json, options, os, osproc, sequtils, strformat, strutils]
import std/[json, options, os, osproc, sequtils, strformat, strscans, strutils]
import cli, logger

type
Expand Down Expand Up @@ -102,12 +102,75 @@ proc findProbSpecsExercises(repo: ProbSpecsRepo, conf: Conf): seq[ProbSpecsExerc
if conf.exercise.isNone or conf.exercise.get() == repoExercise.slug:
result.add(initProbSpecsExercise(repoExercise))

proc findProbSpecsExercises*(conf: Conf): seq[ProbSpecsExercise] =
let probSpecsRepo = initProbSpecsRepo()

template withDir(dir: string; body: untyped): untyped =
## Changes the current directory to `dir` temporarily.
let startDir = getCurrentDir()
try:
probSpecsRepo.remove()
probSpecsRepo.clone()
probSpecsRepo.findProbSpecsExercises(conf)
setCurrentDir(dir)
body
finally:
probSpecsRepo.remove()
setCurrentDir(startDir)

proc findProbSpecsExercises*(conf: Conf): seq[ProbSpecsExercise] =
const mainBranchName = "master"

if conf.probSpecsDir.isSome():
let probSpecsDir = conf.probSpecsDir.get()
logDetailed(&"Using user-provided problem-specifications dir: {probSpecsDir}")

if not dirExists(probSpecsDir):
showError(&"the given problem-specifications directory does not exist: '{probSpecsDir}'")

withDir probSpecsDir:
if execCmd("git rev-parse") != 0:
showError(&"the given problem-specifications directory is not a git repository: '{probSpecsDir}'")

# Exit if the working directory is not clean.
if execCmd("git diff-index --quiet HEAD") != 0: # Ignores untracked files.
echo &"\nUnstaged changes in {probSpecsDir}:"
discard execCmd("git status --short --untracked=no")
showError(&"the given problem-specifications working directory is not clean: '{probSpecsDir}'")

# Exit if HEAD is detached.
let (_, errDetached) = execCmdEx("git symbolic-ref --quiet HEAD")
if errDetached != 0:
showError(&"there is a detached HEAD in the given problem-specifications directory: '{probSpecsDir}'")

# Find the name of the user's remote that points to upstream (don't assume
# it's named 'upstream').
# Exit if user has no remote that points to the correct location.
let (remotes, errRemotes) = execCmdEx("git remote -v")
if errRemotes != 0:
showError(&"could not run `git remote -v` in the given problem-specifications directory: '{probSpecsDir}'")
var remoteName, remoteUrl: string
var foundCorrectLocation = false
const upstreamLocation = "github.com/exercism/problem-specifications"
for line in remotes.splitLines():
discard line.scanf("$s$w$s$+fetch)$.", remoteName, remoteUrl)
if remoteUrl.contains(upstreamLocation):
foundCorrectLocation = true
break
if not foundCorrectLocation:
showError(&"there is no remote that points to '{upstreamLocation}' in the given problem-specifications directory: '{probSpecsDir}'")

# For now, just exit with an error if the HEAD is not up-to-date with
# upstream, even if it's possible to do a fast-forward merge.
if execCmd(&"git fetch --quiet {remoteName} {mainBranchName}") != 0:
showError(&"failed to fetch `{mainBranchName}` in problem-specifications directory: '{probSpecsDir}'")

# Allow HEAD being on a non-`master` branch, as long as it's up-to-date
# with `upstream/master`.
let (revHead, _) = execCmdEx("git rev-parse HEAD")
let (revUpstream, _) = execCmdEx(&"git rev-parse {remoteName}/{mainBranchName}")
if revHead != revUpstream:
showError(&"the given problem-specifications directory is not up-to-date: '{probSpecsDir}'")

result = ProbSpecsRepo(dir: probSpecsDir).findProbSpecsExercises(conf)
else:
let probSpecsRepo = initProbSpecsRepo()
try:
probSpecsRepo.remove()
probSpecsRepo.clone()
result = probSpecsRepo.findProbSpecsExercises(conf)
finally:
probSpecsRepo.remove()

0 comments on commit 7e242e6

Please sign in to comment.