Skip to content

v2.0.0

Latest

Choose a tag to compare

@StevenTCramer StevenTCramer released this 14 Oct 01:54
· 10 commits to master since this release
0d18a20

First stable 2.x release. TimeWarp.Amuru and TimeWarp.Amuru.Tools ship together at 2.0.0 (one <Version> in source/Directory.Build.props). Install either package without --prerelease. The baseline is 1.1.1. This section consolidates everything that shipped in 2.0.0-beta.1 and 2.0.0-beta.2, plus the concurrency contract for CommandResult.

Breaking changes

TimeWarp.Amuru

  • WithStandardInput("") attaches immediate EOF. null still means the caller owns stdin. Callers who passed an empty string and expected "no stdin configured" now close the child's stdin immediately.
  • TtyPassthroughAsync throws InvalidOperationException (TtyPassthroughAsync requires console stdin; configured standard input cannot be used with a TTY) when standard input was configured, including the empty string. It also throws TtyPassthroughAsync cannot run a pipeline; the upstream stage would be dropped when the command is a pipeline. The throw happens before the process starts.

TimeWarp.Amuru.Tools

dotnet builders:

  • DotNetPackBuilder.WithFramework is removed. dotnet pack rejects --framework.
  • DotNetWatchBuilder.WithInclude, WithExclude, and WithProperty are removed.
  • DotNetNuGetDeleteBuilder.WithConfigFile is removed. dotnet nuget delete no longer accepts --configfile. WithNonInteractive() emits --non-interactive.
  • DotNetTestBuilder.WithCollect() now requires a collector name: WithCollect(string dataCollector).
  • DotNetDevCertsHttpsBuilder.WithExport() without WithExportPath throws.
  • DotNetRunBuilder throws when WithProject and WithFile are both set.

Git:

  • Git.GetMasterWorktreePathAsync is removed. Use Git.GetDefaultWorktreePathAsync.
  • Git.UpdateMasterWorktreeAsync is removed. Use Git.UpdateDefaultWorktreeAsync.
  • Git.GetWorktreePathAsync, Git.UpdateBranchAsync, and Git.UpdateWorktreeAsync no longer default branchName to "master". Pass the branch name. Working-directory overloads remain: (string branchName, CancellationToken cancellationToken = default).
  • Git.FetchAsync returns GitFetchResult instead of bool.
  • Git.BranchExistsAsync returns GitBranchExistsResult (Success, Exists, ErrorMessage) instead of bool. It runs git show-ref --verify --quiet. Exit code 1 (missing ref) is Success true and Exists false. Other failures set Success false and keep the git message.
  • Git.ConfigureFetchRefspecAsync returns GitConfigureFetchRefspecResult instead of bool.
  • Git.SetRemoteHeadAutoAsync returns GitSetRemoteHeadResult instead of bool.
  • Git.WorktreeListPorcelainAsync returns GitWorktreeListResult (Success, Porcelain, ErrorMessage) instead of string. Failure is Success false and Porcelain null, not string.Empty.
  • GetCommitsAheadAsync(string branchName = "master", ...) still defaults the comparison branch to "master". GetCommitsAheadOfDefaultBranchAsync remains the default-branch entry point.

Naming:

  • DotNetListPackagesBuilder.WithConfig(string) is renamed to WithConfigFile(string).
  • DotNetWatchBuilder.WithTargetFramework(string) is renamed to WithFramework(string).

Behavior changes

  • PassthroughAsync feeds a configured standard-input string to the child. Console stdin is opened only when no string was configured. Callers who never set stdin keep the previous behavior.
  • UpdateBranchAsync fast-forwards a branch that is checked out in any worktree with git pull --ff-only (pull.rebase=false). A branch that is not checked out still uses git fetch origin <branch>:<branch>. BranchPath is set for a normal repository.
  • UpdateWorktreeAsync uses the same fast-forward-only pull. It no longer runs a plain git pull that could merge or rebase from config.
  • GetDefaultBranchAsync strips only a leading origin/ prefix.
  • SelectWithFzf forwards the builder's option flags to fzf. FromInput writes each item as its own stdin line. FromFiles walks the working directory. FromCommand keeps quoted text, including spaces, as one argument.
  • WithPreviewLabelPos and WithBorderLabelPos each accept fzf's N[:top|bottom] string and a column int.

New

TimeWarp.Amuru

  • CommandResult.Pipe(string executable, CommandOptions options, params string[] arguments) and the matching ShellBuilder.Pipe overload apply working directory, environment, and validation to the downstream stage.
  • CommandMock.GetConfiguredStandardInput returns the standard-input string recorded for the latest matching call. Null means the caller owns stdin.

Concurrency and output contract for CommandResult:

  • Execution methods may be called concurrently on the same instance. Each call returns its own CommandOutput, exit code, or streamed lines.
  • LastOutput is the most recent execution on that instance. The last writer wins, so it is not meaningful under concurrent use. Prefer the returned CommandOutput.
  • CaptureAsync and RunAndCaptureAsync retain every line plus the combined strings.
  • StreamStdoutAsync, StreamStderrAsync, and StreamCombinedAsync keep one line at a time.
  • RunAsync and PassthroughAsync do not buffer output.
  • There is no size limit. The limit is process memory.

Native text search and in-place replace (TimeWarp.Amuru.Native.Text). Commands returns CommandOutput and does not throw. Direct returns typed results and throws.

SelectString (Commands.SelectString, Direct.SelectString, and Bash Grep / GrepDirect):

  • Inputs are a path, an IEnumerable<string> of paths, a root plus glob, or a TextReader / Stream. There is no string-content overload; pass a StringReader. A reader or stream with no path is labeled -.
  • Direct streams TextMatch (Path, LineNumber, Line, Match, ContextBefore, ContextAfter) one line at a time.
  • Commands writes path:line:text. Exit codes match grep: 0 when a line matched, 1 when none matched, 2 on error.
  • A file whose first 8000 bytes contain a NUL byte and have no UTF-16 or UTF-32 BOM is binary and yields no matches.

ReplaceInFiles (Commands.ReplaceInFiles, Direct.ReplaceInFiles, and Bash Sed / SedDirect):

  • Inputs are a pattern, a replacement ($1 and ${name} expand to captures), and the same file inputs as SelectString except a reader or stream.
  • Direct yields one ReplaceResult per file (Path, ReplacementCount, Changed, Preview, BackupPath), including files that did not change.
  • Commands writes path: N replacement(s) for each changed file. Exit 0 when the walk finishes, including when nothing matched. Exit 1 on a regex or I/O error.

Options (SelectStringOptions, ReplaceInFilesOptions): literal match (SimpleMatch), case, invert, context lines, per-file limits (MaxMatches counts matching lines, the same as grep -m; MaxReplacements), include and exclude globs, dry run, .bak backup, multiline, and singleline. Dry run fills Preview with a unified diff and writes nothing.

Write safety for ReplaceInFiles:

  • A BOM is kept. A file with no BOM is written as UTF-8 without one.
  • The first newline style is kept: CRLF when the first line feed follows a carriage return, CR when the file has no line feed, otherwise LF. A trailing newline is kept. A lone CR in an LF or CRLF file stays a literal character.
  • A file whose text does not change is not rewritten, so its bytes and mtime stay.
  • The write is a temp file in the target directory, then a move on that volume.
  • A UTF-8 or no-BOM file that contains a NUL byte is skipped as binary, which includes UTF-16 and UTF-32 files without a BOM. Invalid bytes in the detected encoding throw InvalidDataException and are not rewritten as U+FFFD.
  • A symlink is followed and the target is rewritten, so the link stays a link. The .bak is written beside the path that was passed in, only after the temp write succeeds and only when the text changes.

Per-command timeout. There is no process-wide default. CommandOptions.Timeout null means no limit. TimeoutGracePeriod defaults to CommandOptions.DefaultTimeoutGracePeriod (5 seconds).

  • CommandOptions.Timeout, TimeoutGracePeriod, WithTimeout(TimeSpan), WithTimeoutGracePeriod(TimeSpan), and DefaultTimeoutGracePeriod
  • ShellBuilder.WithTimeout and ShellBuilder.WithTimeoutGracePeriod
  • CommandOutput.TimedOut
  • CommandResult.TimeoutExitCode is 124
  • CommandResult.LastOutput is set when an execution finishes, including a timeout, and is left unchanged when the caller cancels
  • MockSetup.TimesOut()

On expiry the graceful signal is sent (SIGINT on Linux and macOS, Ctrl+C on Windows). The process tree is killed after the grace period. TtyPassthroughAsync sends SIGINT on Linux and macOS, then kills the tree after the grace period. Windows non-console children do not receive that signal, so the grace period ends in the kill.

Pipe uses one window: the shorter timeout, and the grace period from that shorter side. Equal timeouts keep the smaller grace. The window applies to the last stage. Earlier stages are cancelled when the pipe stops.

Outcome Caller sees
Timeout, default validation TimedOut true, exit 124, Success false, no throw. The child's own exit status is not preserved
Timeout, WithZeroExitCodeValidation() TimeoutException (Command timed out after {n} second(s) and was terminated.). LastOutput is set first
TimesOut() with no timeout configured, strict validation TimeoutException (Command timed out and was terminated.)
Caller cancels OperationCanceledException. LastOutput is unchanged
TtyPassthroughAsync timeout Exit 124, including under strict validation. This method does not throw

TimeWarp.Amuru.Tools

  • Every command builder exposes WithNoValidation() and WithZeroExitCodeValidation() with the same semantics as ShellBuilder. Default validation stays None. DotNetBuilder gained WithZeroExitCodeValidation() so derived builders inherit it.
  • DotNetBuilder.WithTimeout(TimeSpan) and DotNetBuilder.WithTimeoutGracePeriod(TimeSpan). Derived builders inherit them. A builder that already holds a CommandOptions snapshot keeps the timeout stored on that snapshot. The result contract is the core contract above.
  • Fzf.FromCommand and SelectWithFzf pass the builder's CommandOptions through to the fzf pipe stage.
  • Repository-path overloads (null path means the process working directory) for GetDefaultBranchAsync, UpdateDefaultBranchAsync, GetCommitsAheadAsync, GetCommitsAheadOfDefaultBranchAsync, GetWorktreePathAsync, GetDefaultWorktreePathAsync, UpdateBranchAsync, UpdateDefaultBranchAsync, UpdateWorktreeAsync, and UpdateDefaultWorktreeAsync. Existing zero-argument and (CancellationToken) calls of the default-branch helpers still compile.

Fixed

  • dotnet builders emit flags SDK 10.0.x accepts. WithTerminalLogger emits one argument, --tl:<mode>, on build, restore, run, test, publish, and pack. nuget why takes the project as a positional argument before the package id.
  • SelectWithFzf no longer drops configured fzf options. FromInput no longer feeds items through echo (so -n, -e, and -E stay data, including on Windows). FromFiles no longer shells out to Unix find. FromCommand no longer splits the command on spaces.
  • UpdateBranchAsync updates the currently checked-out branch. git fetch origin <branch>:<branch> refuses that case; the method pulls with --ff-only instead.
  • FindMainRepositoryFromWorktree resolves a relative gitdir: against the worktree directory and returns the repository directory, not <repo>/.git.
  • TimeWarp.Amuru.Tools ships lib/net10.0/timewarp-amuru-tools.xml. IntelliSense covers every public Tools member.

Upgrade guide

One line per removed or renamed member, or per behavior that changes an existing call.

  • Git.GetMasterWorktreePathAsync → Git.GetDefaultWorktreePathAsync
  • Git.UpdateMasterWorktreeAsync → Git.UpdateDefaultWorktreeAsync
  • Git.GetWorktreePathAsync(branch) / Git.UpdateBranchAsync(branch) / Git.UpdateWorktreeAsync(branch) → pass branch; the "master" default is gone
  • Git.FetchAsync → read GitFetchResult instead of bool
  • Git.BranchExistsAsync → read GitBranchExistsResult (Success, Exists, ErrorMessage) instead of bool
  • Git.ConfigureFetchRefspecAsync → read GitConfigureFetchRefspecResult instead of bool
  • Git.SetRemoteHeadAutoAsync → read GitSetRemoteHeadResult instead of bool
  • Git.WorktreeListPorcelainAsync → read GitWorktreeListResult; failure is Porcelain == null, not string.Empty
  • DotNetListPackagesBuilder.WithConfig → WithConfigFile
  • DotNetWatchBuilder.WithTargetFramework → WithFramework
  • DotNetPackBuilder.WithFramework → removed (dotnet pack has no --framework)
  • DotNetWatchBuilder.WithInclude → removed
  • DotNetWatchBuilder.WithExclude → removed
  • DotNetWatchBuilder.WithProperty → removed
  • DotNetNuGetDeleteBuilder.WithConfigFile → removed; use WithNonInteractive() for --non-interactive
  • DotNetTestBuilder.WithCollect() → WithCollect(string dataCollector)
  • DotNetDevCertsHttpsBuilder.WithExport() → call WithExport().WithExportPath(path); export without a path throws
  • DotNetRunBuilder.WithProject together with WithFile → set only one; both together throws
  • WithStandardInput("") → immediate EOF (null still means the caller owns stdin)
  • TtyPassthroughAsync with configured stdin → throws InvalidOperationException; leave stdin unconfigured for a real TTY
  • TtyPassthroughAsync on a pipeline → throws; use PassthroughAsync when an upstream stage must feed the child