Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions lib/git.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1772,4 +1772,66 @@ defmodule Git do
command = struct!(Git.Commands.Restore, rest)
Git.Command.run(Git.Commands.Restore, command, config)
end

@doc """
Runs `git apply` to apply a patch to files and/or the index.

The function is named `apply_patch` to avoid conflicting with `Kernel.apply/2`.

## Options

* `:config` - a `Git.Config` struct (default: `Git.Config.new()`)
* `:patch` - path to the patch file (required)
* `:check` - check if patch applies cleanly without applying (`--check`)
* `:stat` - show diffstat (`--stat`)
* `:summary` - show summary (`--summary`)
* `:cached` - apply to index only (`--cached`)
* `:index` - apply to index and working tree (`--index`)
* `:reverse` - apply in reverse (`--reverse`)
* `:three_way` - attempt 3-way merge (`--3way`)
* `:verbose` - verbose output (`--verbose`)

## Examples

Git.apply_patch(patch: "fix.patch")
Git.apply_patch(patch: "fix.patch", check: true)
Git.apply_patch(patch: "fix.patch", stat: true)

"""
@spec apply_patch(keyword()) :: {:ok, :done} | {:ok, String.t()} | {:error, term()}
def apply_patch(opts \\ []) do
{config, rest} = Keyword.pop(opts, :config, Config.new())
command = struct!(Git.Commands.Apply, rest)
Git.Command.run(Git.Commands.Apply, command, config)
end

@doc """
Runs `git am` to apply patches from mailbox-formatted files.

## Options

* `:config` - a `Git.Config` struct (default: `Git.Config.new()`)
* `:patches` - list of paths to mailbox patch files
* `:directory` - path to directory of patches
* `:three_way` - 3-way merge on conflict (`--3way`)
* `:keep` - keep subject prefix (`--keep`)
* `:signoff` - add Signed-off-by line (`--signoff`)
* `:abort` - abort current am session (`--abort`)
* `:continue_` - continue after resolving conflict (`--continue`)
* `:skip` - skip current patch (`--skip`)
* `:quiet` - quiet output (`--quiet`)

## Examples

Git.am(patches: ["0001-fix.patch"])
Git.am(patches: ["0001-fix.patch"], three_way: true)
Git.am(abort: true)

"""
@spec am(keyword()) :: {:ok, :done} | {:error, term()}
def am(opts \\ []) do
{config, rest} = Keyword.pop(opts, :config, Config.new())
command = struct!(Git.Commands.Am, rest)
Git.Command.run(Git.Commands.Am, command, config)
end
end
95 changes: 95 additions & 0 deletions lib/git/commands/am.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
defmodule Git.Commands.Am do
@moduledoc """
Implements the `Git.Command` behaviour for `git am`.

Applies a series of patches from mailbox-formatted files. Supports three-way
merges, keeping subject prefixes, adding sign-off lines, and controlling
in-progress am sessions (abort, continue, skip).
"""

@behaviour Git.Command

@type t :: %__MODULE__{
patches: [String.t()],
directory: String.t() | nil,
three_way: boolean(),
keep: boolean(),
signoff: boolean(),
abort: boolean(),
continue_: boolean(),
skip: boolean(),
quiet: boolean()
}

defstruct patches: [],
directory: nil,
three_way: false,
keep: false,
signoff: false,
abort: false,
continue_: false,
skip: false,
quiet: false

@doc """
Returns the argument list for `git am`.

When `:abort`, `:continue_`, or `:skip` is `true`, builds the corresponding
control command. Otherwise builds the full am command with all applicable
flags and patch file paths.

## Examples

iex> Git.Commands.Am.args(%Git.Commands.Am{abort: true})
["am", "--abort"]

iex> Git.Commands.Am.args(%Git.Commands.Am{continue_: true})
["am", "--continue"]

iex> Git.Commands.Am.args(%Git.Commands.Am{skip: true})
["am", "--skip"]

iex> Git.Commands.Am.args(%Git.Commands.Am{patches: ["0001-fix.patch"]})
["am", "0001-fix.patch"]

iex> Git.Commands.Am.args(%Git.Commands.Am{patches: ["0001-fix.patch"], three_way: true, signoff: true})
["am", "--3way", "--signoff", "0001-fix.patch"]

iex> Git.Commands.Am.args(%Git.Commands.Am{directory: "/tmp/patches"})
["am", "/tmp/patches"]

"""
@spec args(t()) :: [String.t()]
@impl true
def args(%__MODULE__{abort: true}), do: ["am", "--abort"]
def args(%__MODULE__{continue_: true}), do: ["am", "--continue"]
def args(%__MODULE__{skip: true}), do: ["am", "--skip"]

def args(%__MODULE__{} = command) do
["am"]
|> maybe_add(command.three_way, "--3way")
|> maybe_add(command.keep, "--keep")
|> maybe_add(command.signoff, "--signoff")
|> maybe_add(command.quiet, "--quiet")
|> maybe_add_patches(command.patches, command.directory)
end

@doc """
Parses the output of `git am`.

On success (exit code 0), returns `{:ok, :done}`.
On failure, returns `{:error, {stdout, exit_code}}`.
"""
@spec parse_output(String.t(), non_neg_integer()) ::
{:ok, :done} | {:error, {String.t(), non_neg_integer()}}
@impl true
def parse_output(_stdout, 0), do: {:ok, :done}
def parse_output(stdout, exit_code), do: {:error, {stdout, exit_code}}

defp maybe_add(args, true, flag), do: args ++ [flag]
defp maybe_add(args, false, _flag), do: args

defp maybe_add_patches(args, [], nil), do: args
defp maybe_add_patches(args, [], dir) when is_binary(dir), do: args ++ [dir]
defp maybe_add_patches(args, patches, _dir), do: args ++ patches
end
103 changes: 103 additions & 0 deletions lib/git/commands/apply.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
defmodule Git.Commands.Apply do
@moduledoc """
Implements the `Git.Command` behaviour for `git apply`.

Applies a patch to files and/or to the index. Supports checking whether a
patch applies cleanly, showing diffstat/summary, applying to the index or
working tree, reverse application, and three-way merges.
"""

@behaviour Git.Command

@mode_key :__git_apply_mode__

@type t :: %__MODULE__{
patch: String.t() | nil,
check: boolean(),
stat: boolean(),
summary: boolean(),
cached: boolean(),
index: boolean(),
reverse: boolean(),
three_way: boolean(),
verbose: boolean()
}

defstruct patch: nil,
check: false,
stat: false,
summary: false,
cached: false,
index: false,
reverse: false,
three_way: false,
verbose: false

@doc """
Returns the argument list for `git apply`.

## Examples

iex> Git.Commands.Apply.args(%Git.Commands.Apply{patch: "fix.patch"})
["apply", "fix.patch"]

iex> Git.Commands.Apply.args(%Git.Commands.Apply{patch: "fix.patch", check: true})
["apply", "--check", "fix.patch"]

iex> Git.Commands.Apply.args(%Git.Commands.Apply{patch: "fix.patch", stat: true, summary: true})
["apply", "--stat", "--summary", "fix.patch"]

iex> Git.Commands.Apply.args(%Git.Commands.Apply{patch: "fix.patch", cached: true})
["apply", "--cached", "fix.patch"]

iex> Git.Commands.Apply.args(%Git.Commands.Apply{patch: "fix.patch", reverse: true})
["apply", "--reverse", "fix.patch"]

iex> Git.Commands.Apply.args(%Git.Commands.Apply{patch: "fix.patch", three_way: true})
["apply", "--3way", "fix.patch"]

"""
@spec args(t()) :: [String.t()]
@impl true
def args(%__MODULE__{} = command) do
info_mode = command.stat or command.summary or command.check
Process.put(@mode_key, info_mode)

["apply"]
|> maybe_add(command.check, "--check")
|> maybe_add(command.stat, "--stat")
|> maybe_add(command.summary, "--summary")
|> maybe_add(command.cached, "--cached")
|> maybe_add(command.index, "--index")
|> maybe_add(command.reverse, "--reverse")
|> maybe_add(command.three_way, "--3way")
|> maybe_add(command.verbose, "--verbose")
|> maybe_add_patch(command.patch)
end

@doc """
Parses the output of `git apply`.

For stat, summary, or check modes, returns `{:ok, output}` with the
informational text. For normal apply operations, returns `{:ok, :done}`.
On failure, returns `{:error, {stdout, exit_code}}`.
"""
@spec parse_output(String.t(), non_neg_integer()) ::
{:ok, :done} | {:ok, String.t()} | {:error, {String.t(), non_neg_integer()}}
@impl true
def parse_output(stdout, 0) do
if Process.get(@mode_key, false) do
{:ok, stdout}
else
{:ok, :done}
end
end

def parse_output(stdout, exit_code), do: {:error, {stdout, exit_code}}

defp maybe_add(args, true, flag), do: args ++ [flag]
defp maybe_add(args, false, _flag), do: args

defp maybe_add_patch(args, nil), do: args
defp maybe_add_patch(args, patch), do: args ++ [patch]
end
20 changes: 20 additions & 0 deletions lib/git/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -770,4 +770,24 @@ defmodule Git.Repo do
def restore(%__MODULE__{} = repo, opts \\ []) do
Git.restore(Keyword.put(opts, :config, repo.config))
end

@doc """
Runs `git apply` on the repository.

See `Git.apply_patch/1` for available options.
"""
@spec apply_patch(t(), keyword()) :: {:ok, :done} | {:ok, String.t()} | {:error, term()}
def apply_patch(%__MODULE__{} = repo, opts \\ []) do
Git.apply_patch(Keyword.put(opts, :config, repo.config))
end

@doc """
Runs `git am` on the repository.

See `Git.am/1` for available options.
"""
@spec am(t(), keyword()) :: {:ok, :done} | {:error, term()}
def am(%__MODULE__{} = repo, opts \\ []) do
Git.am(Keyword.put(opts, :config, repo.config))
end
end
Loading