Coding - arxanas/git-branchless GitHub Wiki

Printing messages and progress reporting

The Effects type is used to manage printing output and displaying progress to the user. It can be used to show the progress of nested tasks, including with spinners or progress meters.

It's not safe to call println! or eprintln! directly, because the output may clobber or be clobbered by progress reporting mechanisms. Instead, call one of the following, as appropriate:

writeln!(effects.get_output_stream(), "message {}", param1, ...);
writeln!(effects.get_error_stream(), "message {}", param1, ...);

as appropriate.

Logging

For logging, the project uses the tracing library. You can generate logs using macros like tracing::warn!. These macros support structured logging, so you can directly include variables which you want to log, without having to convert them to strings first:

// Includes the contents of `buffer` in the log message.
warn!(?buffer, "WriteProgress dropped while buffer was not empty");

To display logs at runtime, set the RUST_LOG environment variable. For example:

$ RUST_LOG=info cargo run smartlog

Error-handling

Most of the project uses the eyre library for error-handling. The eyre::Result type allows you to use the ? operator with nearly any error type, rather than having to wrap every third-party library Result into a project- or function-specific Result type.

Span traces

Stack traces are displayed with the help of the color-eyre and tracing-error libraries. Technically, span traces are recorded, not stack traces. These correspond to functions which have been explicitly annotated using the tracing library, such as with #[tracing::instrument]. Other function calls are not included in the span trace.

Example span trace

When the program exits with error, a span trace will appear:

$ cargo run smartlog
   Compiling git-branchless v0.3.4 (/Users/wkhan/workspace/git-branchless)
    Finished dev [unoptimized + debuginfo] target(s) in 5.74s
     Running `target/debug/git-branchless smartlog`
Error:
   0: Could not find repository main branch

Location:
   src/git/repo.rs:302

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

   0: branchless::commands::smartlog::smartlog with effects=<Output fancy=true>
      at src/commands/smartlog.rs:269

The span trace includes branchless::commands::smartlog::smartlog because it is wrapped with #[tracing::instrument]. Also notice that it includes the parameter value effects, along with its Debug representation.

Adding functions to the span trace

When you want a function to appear in the span trace, you must do one of two things:

  • Wrap the function definition with #[tracing::instrument]. This is best for first-party code:
    • Only the definition needs to be wrapped with #[instrument]. Callers don't have to remember to call wrap_err_with every time they call the function.
    • Parameter values are included in the span trace as well.
  • Call wrap_err_with on a Result to turn it into an eyre::Result. This is best for third-party libraries which don't use eyre::Result, or when additional information needs to be added to an operation.

Including parameter values in the span trace

By default, #[tracing::instrument]-annotated functions will include all of their parameters in the span trace by getting their Debug representations, which is helpful for debugging.

However, parameters should not be included when the Debug representation is very large. A parameter whose type doesn't implement Debug can't be included at all.

To skip printing a certain parameter in the span trace: add a skip to tracing::instrument:

#[instrument(skip(value))]
fn set_config(
    effects: &Effects,
    config: &mut Config,
    name: &str,
    value: impl Into<ConfigValue>,
) -> eyre::Result<()> {

vs. anyhow

The project also uses the similar anyhow library in a few cases:

  • When an error needs to be sent between threads. The eyre::ErrReport type is not Send, so we use the anyhow::Error type instead.
  • When interfacing with another library that uses anyhow::Error.

vs. Result

When specific error values need to be consumed by the rest of the program, rather than reported to the user, the project uses the standard library Result instead (or in addition). If necessary, the thiserror library could also be used to compose these kinds of errors.

Compile times

To keep compile times down, heed the advice in Fast Rust Builds. In particular, you'll see some examples of reducing monomorphization in the codebase:

  • eyre::ErrContext::wrap_err is called in preference to eyre::ErrContext::wrap_err_with. The latter takes a function as its argument, which is slower to process by the compiler, and slightly increases code size.
    • If the containing function is marked with #[instrument], then its parameter values will be printed in the span-trace, so they don't need to be duplicated in the wrap_err/wrap_err_with message. Thus, wrap_err_with is only useful when there are other values which should be included in the error message, such as local variables.
  • Functions which take Into<Foo> or AsRef<Foo> immediately defer to a function which is specialized to take Foo/&Foo.
    • This also makes it easier to include the parameter values with #[instrument], as Into<Foo>/AsRef<Foo> won't be Debug, but Foo often is.

The git module

The project wraps Git operations into its own git module, rather than use the types from e.g. git2 directly. This is for a few reasons:

  • To be able to swap out the Git implementation.
    • We may use the gitoxide library in the future.
    • We may want to back some operations with the git executable rather than via git2.
    • We may want to integrate with e.g. Watchman in the future.
  • To improve API correctness.
    • The Oid type is split into MaybeZeroOid and NonZeroOid.
    • The git2 library returns a git2::Error for cases such as an object not being found. Our wrapper detects those cases and instead returns an Option.
  • To improve API performance.
    • The git2 library works with Indexes, which scale with the size of the repository, rather than the size of the change to the repository. This makes some operations very slow, such as cherry-picking changes. The project offers alternative implementations which don't use Indexes.

Types specific to git2, etc., should not escape the git module. Instead, our own wrapper types should be exposed.

⚠️ **GitHub.com Fallback** ⚠️