# `Git.Workflow`
[🔗](https://github.com/joshrotenberg/git_wrapper_ex/blob/main/lib/git/workflow.ex#L1)

Composable helpers for common multi-step git workflows.

Each function orchestrates several lower-level `Git` commands into a
single logical operation. All functions accept a keyword list where the
`:config` key, when present, must be a `Git.Config` struct and is
forwarded to every underlying git invocation.

## Examples

    # Stage everything and commit in one call
    {:ok, result} = Git.Workflow.commit_all("feat: ship it", config: cfg)

    # Work on a feature branch, then return to the original branch
    {:ok, result} = Git.Workflow.feature_branch("feat/cool", fn opts ->
      File.write!("cool.txt", "cool")
      {:ok, :done} = Git.add(files: ["cool.txt"], config: opts[:config])
      {:ok, _} = Git.commit("feat: cool", Keyword.take(opts, [:config]))
      {:ok, :worked}
    end, merge: true, config: cfg)

# `labeled_step`

```elixir
@type labeled_step() :: step() | {atom(), step()}
```

A step, optionally labeled so a failure can name it.

# `step`

```elixir
@type step() :: (keyword() -&gt; {:ok, term()} | {:error, term()})
```

A workflow step: a 1-arity function receiving the config keyword list.

# `amend`

```elixir
@spec amend(keyword()) :: {:ok, Git.CommitResult.t()} | {:error, term()}
```

Amends the last commit.

When no `:message` is provided, the existing commit message is reused.
When `:all` is `true`, all changes are staged before amending.

## Options

  * `:message` - new commit message (default: reuse existing message)
  * `:all` - stage all changes before amending (default `false`)
  * `:config` - a `Git.Config` struct

Returns `{:ok, commit_result}` on success.

# `backport`

```elixir
@spec backport(
  String.t() | [String.t()],
  keyword()
) :: {:ok, Git.CherryPickResult.t()} | {:error, term()}
```

Cherry-picks commits onto another branch, then returns to where you started.

`commits` is a single sha/ref or a list of them, applied in order. The current
branch is remembered, `:target` is checked out (created from `:base` when
given), and the commits are cherry-picked. On a conflict the cherry-pick is
aborted; the original branch is always restored afterward, even if a step
raises. With `:push`, the target is pushed to `:remote` after a clean pick.

## Options

  * `:target` - branch to apply the commits on (required)
  * `:base` - create `:target` from this ref first (default: `:target` must exist)
  * `:push` - push `:target` to `:remote` after a clean pick (default `false`)
  * `:remote` - remote to push to (default `"origin"`)
  * `:config` - a `Git.Config` struct

Returns `{:ok, %Git.CherryPickResult{}}` on success, or the error.

# `chain`

```elixir
@spec chain(
  [labeled_step()],
  keyword()
) :: {:ok, term()} | {:error, term()}
```

Runs a list of steps in order, threading `:config` into each and
short-circuiting on the first `{:error, _}`.

Each step is a 1-arity function receiving the config keyword list (the same
contract as `feature_branch/3`'s function), or a `{name, fun}` tuple. With a
tuple, a failure is reported as `{:error, {name, reason}}`; otherwise the raw
`{:error, reason}` is returned. Returns the last step's `{:ok, result}`; an
empty list returns `{:ok, nil}`.

Steps do not receive each other's results. Prefer a plain `with` for a fixed
sequence or when a later step needs an earlier step's output; reach for
`chain/2` when the step list is built at runtime.

## Examples

    [
      {:stage, fn o -> Git.add(Keyword.merge(o, all: true)) end},
      {:commit, fn o -> Git.commit("chore: release", o) end}
    ]
    |> Git.Workflow.chain(config: cfg)

# `commit_all`

```elixir
@spec commit_all(
  String.t(),
  keyword()
) :: {:ok, Git.CommitResult.t()} | {:error, term()}
```

Stages all changes and commits with the given message.

Any additional keyword options (e.g., `:allow_empty`) are forwarded to
`Git.commit/2`.

## Options

  * `:config` - a `Git.Config` struct
  * All other options are passed to `Git.commit/2`.

Returns `{:ok, commit_result}` on success.

# `discard_all`

```elixir
@spec discard_all(keyword()) ::
  {:ok, :discarded} | {:ok, {:dry_run, [String.t()]}} | {:error, term()}
```

Returns the working tree to a pristine committed state.

Hard-resets tracked changes and removes untracked files and directories, so
both halves of a dirty tree are cleared (a `reset --hard` leaves untracked
files, a `clean` leaves tracked modifications; you need both).

Destructive: discarded untracked and ignored files are not recoverable.
Preview first with `dry_run: true`.

## Options

  * `:ignored` - also remove ignored files (`clean -x`, default `false`)
  * `:dry_run` - do not change anything; return the untracked/ignored files
    that would be removed (default `false`)
  * `:config` - a `Git.Config` struct

Returns `{:ok, :discarded}` after clearing the tree, or
`{:ok, {:dry_run, paths}}` when `:dry_run` is set.

# `feature_branch`

```elixir
@spec feature_branch(
  String.t(),
  (keyword() -&gt; {:ok, term()} | {:error, term()}),
  keyword()
) ::
  {:ok, term()} | {:error, term()}
```

Creates a feature branch, runs a function on it, and returns to the original
branch.

The function `fun` receives a keyword list containing the `:config` key
(when one was provided in `opts`). It must return `{:ok, result}` or
`{:error, reason}`.

After `fun` completes (successfully or not), the original branch is checked
out to ensure cleanup.

## Options

  * `:merge` - when `true`, merge the feature branch back into the original
    branch after `fun` succeeds (default `false`)
  * `:delete` - when `true`, delete the feature branch after a successful
    merge (default `false`; requires `:merge` to be `true`)
  * `:config` - a `Git.Config` struct

Returns `{:ok, result}` where `result` is the return value of `fun`, or
the merge result when `:merge` is `true`.

# `publish`

```elixir
@spec publish(keyword()) :: {:ok, :done} | {:error, term()}
```

Pushes the current branch to a remote, setting its upstream.

Runs `git push -u <remote> <current-branch>`. With `:force`, uses
`--force-with-lease` rather than a plain push, so a remote that moved on is not
overwritten blindly.

## Options

  * `:remote` - remote to push to (default `"origin"`)
  * `:force` - force with lease (default `false`)
  * `:config` - a `Git.Config` struct

Returns `{:ok, :done}` on success.

# `release`

```elixir
@spec release(
  String.t(),
  keyword()
) :: {:ok, String.t()} | {:error, term()}
```

Creates an annotated release tag and, optionally, pushes it.

Refuses to clobber an existing tag: when `version` already exists the tag is
left untouched and `{:error, {:tag_exists, version}}` is returned. Publishing
is opt-in, so nothing is pushed unless `:push` is `true`.

## Options

  * `:message` - annotation message (default: `version`)
  * `:sign` - create a GPG-signed tag (`-s`, default `false`)
  * `:ref` - commit to tag (default `"HEAD"`)
  * `:push` - push the tag after creating it (default `false`)
  * `:remote` - remote to push the tag to (default `"origin"`)
  * `:config` - a `Git.Config` struct

Returns `{:ok, version}` once the tag exists (and, with `:push`, is pushed).

# `restore_branch`

```elixir
@spec restore_branch(
  String.t(),
  keyword()
) :: {:ok, String.t()} | {:error, term()}
```

Recreates a deleted branch.

With `:sha`, simply creates `name` at that commit. Otherwise scans the reflog
for the last commit `name` pointed at (the value HEAD held just before the
branch was last left) and recreates it there, or returns
`{:error, :not_found}` when the reflog has no record of the branch. Pass
`checkout: true` to switch to the restored branch.

## Options

  * `:sha` - recreate the branch at this commit instead of consulting the reflog
  * `:checkout` - check the branch out after recreating it (default `false`)
  * `:config` - a `Git.Config` struct

Returns `{:ok, sha}` with the commit the branch was recreated at.

# `safe_rebase`

```elixir
@spec safe_rebase(keyword()) :: {:ok, Git.RebaseResult.t()} | {:error, term()}
```

Rebases onto an upstream, cleaning up on conflict.

Runs `Git.rebase/1` with `:upstream` (and optional `:onto`) taken from `opts`.
A conflict (or any rebase failure) leaves the rebase in progress, so this
aborts it with `Git.rebase(abort: true, ...)` before returning the original
`{:error, reason}`. The abort is best-effort.

## Options

  * `:upstream` - the upstream ref to rebase onto
  * `:onto` - rebase onto this ref instead of the upstream's base
  * `:config` - a `Git.Config` struct

Returns `{:ok, %Git.RebaseResult{}}` on a clean rebase.

# `squash_last`

```elixir
@spec squash_last(pos_integer(), String.t(), keyword()) ::
  {:ok, Git.CommitResult.t()} | {:error, term()}
```

Collapses the last `count` commits on the current branch into a single new
commit with `message`.

Soft-resets to `HEAD~count` (keeping all their changes staged) and commits
them as one. This is the only in-place squash in the library, since
interactive rebase is intentionally not wrapped. `count` must be at least 2.

Rewrites current-branch history; recover the originals from the reflog if
needed.

## Options

  * `:config` - a `Git.Config` struct
  * All other options are forwarded to `Git.commit/2`.

Returns `{:ok, commit_result}`, or `{:error, :cannot_undo_root}` when there
are not enough commits to squash that many.

# `squash_merge`

```elixir
@spec squash_merge(
  String.t(),
  keyword()
) :: {:ok, Git.CommitResult.t()} | {:error, term()}
```

Merges a branch with `--squash` and commits with the given message.

## Options

  * `:message` - commit message (required)
  * `:delete` - when `true`, delete the source branch after merge
    (default `false`)
  * `:config` - a `Git.Config` struct

Returns `{:ok, commit_result}` on success. When `:delete` is requested but
the source branch cannot be deleted (for example it is still checked out in a
worktree), returns `{:error, {:branch_not_deleted, reason}}` rather than
reporting success over a branch that still exists. The squash commit itself
has already landed at that point.

# `sync`

```elixir
@spec sync(keyword()) :: {:ok, :synced} | {:error, term()}
```

Fetches from a remote and integrates changes using rebase or merge.

## Options

  * `:strategy` - `:rebase` (default) or `:merge`
  * `:autostash` - stash uncommitted changes before syncing and pop after
    (default `true`). Like git's own `rebase.autostash`, only tracked changes
    are stashed; an untracked-only working tree is not stashed.
  * `:remote` - remote name (default `"origin"`)
  * `:branch` - branch to sync with (defaults to the upstream tracking branch)
  * `:config` - a `Git.Config` struct

Returns `{:ok, :synced}` on success.

When autostash is active and integration succeeds but popping the stash
conflicts with the freshly integrated changes, returns
`{:error, {:autostash_pop_failed, reason}}` (the working tree is left with the
popped stash and its conflict markers) rather than reporting success over it.

When integration itself fails (for example a rebase or merge conflict), the
autostash is left in place; recover it with `git stash pop` after resolving
the integration, since popping onto a mid-conflict tree would compound it.

# `sync_fork`

```elixir
@spec sync_fork(keyword()) :: {:ok, :synced} | {:error, term()}
```

Syncs a fork: fetches the upstream and integrates it into the current branch.

Fetches `:upstream`, then fast-forwards the current branch onto
`<upstream>/<branch>` (a `--ff-only` merge, the default) or rebases onto it
when `strategy: :rebase`. With `:push`, the result is pushed to `:origin`.

## Options

  * `:upstream` - remote to sync from (default `"upstream"`)
  * `:origin` - remote to push to when `:push` (default `"origin"`)
  * `:branch` - branch to track on the upstream (default: the current branch)
  * `:strategy` - `:merge` (fast-forward only, default) or `:rebase`
  * `:push` - push to `:origin` after integrating (default `false`)
  * `:config` - a `Git.Config` struct

Returns `{:ok, :synced}` on success.

# `try_merge`

```elixir
@spec try_merge(
  String.t(),
  keyword()
) :: {:ok, Git.MergeResult.t()} | {:error, term()}
```

Merges `branch` into the current branch, cleaning up on conflict.

Runs `Git.merge/2`. A conflict (or any merge failure) leaves the working tree
mid-merge, so this aborts it with `Git.merge(:abort, ...)` before returning the
original `{:error, reason}`. The abort is best-effort: when the failure left no
merge to abort, the abort's own error is ignored and the original merge error
is still returned.

Extra options (for example `:no_ff` or `:strategy`) are forwarded to the merge.

## Options

  * `:config` - a `Git.Config` struct
  * All other options are forwarded to `Git.merge/2`.

Returns `{:ok, %Git.MergeResult{}}` on a clean merge.

# `undo_last_commit`

```elixir
@spec undo_last_commit(keyword()) :: {:ok, [Git.Commit.t()]} | {:error, term()}
```

Moves `HEAD` back by `:count` commits, returning the commits that were undone.

This is the "I did not mean to commit that" helper. It captures the commits
being undone first, then resets. `:mode` controls what happens to their
changes:

  * `:soft` (default) - keep the changes staged
  * `:mixed` - keep the changes in the working tree, unstaged
  * `:hard` - discard the changes (irreversible beyond the reflog)

## Options

  * `:count` - number of commits to undo (default `1`)
  * `:mode` - `:soft` (default), `:mixed`, or `:hard`
  * `:config` - a `Git.Config` struct

Returns `{:ok, undone_commits}` where `undone_commits` is the list of
`Git.Commit` structs that were on top of the new `HEAD`, or
`{:error, :cannot_undo_root}` when there is not enough history to move back
that far (you cannot reset past the root commit).

# `with_branch`

```elixir
@spec with_branch(String.t(), step(), keyword()) :: {:ok, term()} | {:error, term()}
```

Checks out `ref`, runs `fun` on it, then restores the original branch.

The original branch is restored even if `fun` raises. Pass `create: true` to
create the branch first. Returns `fun`'s result. This is the reusable bracket
underneath `feature_branch/3`.

## Options

  * `:create` - create `ref` as a new branch before running (default `false`)
  * `:config` - a `Git.Config` struct

# `with_stash`

```elixir
@spec with_stash(
  step(),
  keyword()
) :: {:ok, term()} | {:error, term()}
```

Stashes uncommitted tracked changes (only if the tree is dirty), runs `fun`,
then pops the stash.

The stash is popped even if `fun` raises. Returns `fun`'s result. A pop that
fails (for example a conflict) is surfaced as the error rather than hidden.
This is the `:autostash` behavior of `sync/1`, made reusable.

## Options

  * `:config` - a `Git.Config` struct

---

*Consult [api-reference.md](api-reference.md) for complete listing*
