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
69 changes: 69 additions & 0 deletions lib/git.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1834,4 +1834,73 @@ defmodule Git do
command = struct!(Git.Commands.Am, rest)
Git.Command.run(Git.Commands.Am, command, config)
end

@doc """
Runs `git interpret-trailers` to add or parse trailers in commit messages.

Trailers are key-value metadata lines at the end of commit messages, such
as "Signed-off-by:" or "Co-authored-by:".

## Options

* `:config` - a `Git.Config` struct (default: `Git.Config.new()`)
* `:file` - path to a file containing the commit message
* `:parse` - only output the trailers (`--only-trailers`)
* `:trailers` - list of trailers to add, each as `"Key: Value"` (`--trailer`)
* `:in_place` - edit the file in place (`--in-place`)
* `:trim_empty` - trim empty trailers (`--trim-empty`)
* `:where` - where to place new trailers: `"after"`, `"before"`, `"end"`, `"start"` (`--where`)
* `:if_exists` - action if trailer exists: `"addIfDifferentNeighbor"`, `"addIfDifferent"`, `"add"`, `"replace"`, `"doNothing"` (`--if-exists`)
* `:if_missing` - action if trailer missing: `"add"`, `"doNothing"` (`--if-missing`)
* `:unfold` - unfold multi-line trailers (`--unfold`)
* `:no_divider` - do not treat `---` as divider (`--no-divider`)

## Examples

Git.interpret_trailers(file: "msg.txt", trailers: ["Signed-off-by: Name <email>"])
Git.interpret_trailers(file: "msg.txt", parse: true)
Git.interpret_trailers(file: "msg.txt", trailers: ["Acked-by: Name"], in_place: true)

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

@doc """
Runs `git maintenance` to manage repository maintenance tasks.

Supports running, starting, stopping, registering, and unregistering
maintenance tasks such as garbage collection and commit-graph updates.

## Options

* `:config` - a `Git.Config` struct (default: `Git.Config.new()`)
* `:run` - run maintenance tasks (`run` subcommand)
* `:start` - start background maintenance (`start` subcommand)
* `:stop` - stop background maintenance (`stop` subcommand)
* `:register_` - register repo for maintenance (`register` subcommand)
* `:unregister` - unregister repo from maintenance (`unregister` subcommand)
* `:task` - specific task to run (`--task`)
* `:auto` - only run if needed (`--auto`)
* `:quiet` - suppress output (`--quiet`)
* `:schedule` - maintenance schedule: `"hourly"`, `"daily"`, `"weekly"` (`--schedule`)

## Examples

Git.maintenance(run: true)
Git.maintenance(run: true, task: "gc")
Git.maintenance(run: true, auto: true)
Git.maintenance(start: true)
Git.maintenance(stop: true)

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

Adds or parses trailers in commit messages. Trailers are key-value pairs
that appear at the end of a commit message, such as "Signed-off-by:" or
"Co-authored-by:".
"""

@behaviour Git.Command

@type t :: %__MODULE__{
file: String.t() | nil,
parse: boolean(),
trailers: [String.t()],
in_place: boolean(),
trim_empty: boolean(),
where: String.t() | nil,
if_exists: String.t() | nil,
if_missing: String.t() | nil,
unfold: boolean(),
no_divider: boolean()
}

defstruct file: nil,
parse: false,
trailers: [],
in_place: false,
trim_empty: false,
where: nil,
if_exists: nil,
if_missing: nil,
unfold: false,
no_divider: false

@doc """
Returns the argument list for `git interpret-trailers`.

## Examples

iex> Git.Commands.InterpretTrailers.args(%Git.Commands.InterpretTrailers{})
["interpret-trailers"]

iex> Git.Commands.InterpretTrailers.args(%Git.Commands.InterpretTrailers{parse: true})
["interpret-trailers", "--only-trailers"]

iex> Git.Commands.InterpretTrailers.args(%Git.Commands.InterpretTrailers{trailers: ["Signed-off-by: A"]})
["interpret-trailers", "--trailer", "Signed-off-by: A"]

iex> Git.Commands.InterpretTrailers.args(%Git.Commands.InterpretTrailers{in_place: true, file: "msg.txt"})
["interpret-trailers", "--in-place", "msg.txt"]

iex> Git.Commands.InterpretTrailers.args(%Git.Commands.InterpretTrailers{where: "end"})
["interpret-trailers", "--where", "end"]

iex> Git.Commands.InterpretTrailers.args(%Git.Commands.InterpretTrailers{if_exists: "replace"})
["interpret-trailers", "--if-exists", "replace"]

iex> Git.Commands.InterpretTrailers.args(%Git.Commands.InterpretTrailers{if_missing: "doNothing"})
["interpret-trailers", "--if-missing", "doNothing"]

"""
@spec args(t()) :: [String.t()]
@impl true
def args(%__MODULE__{} = command) do
["interpret-trailers"]
|> maybe_add_flag(command.parse, "--only-trailers")
|> maybe_add_flag(command.in_place, "--in-place")
|> maybe_add_flag(command.trim_empty, "--trim-empty")
|> maybe_add_flag(command.unfold, "--unfold")
|> maybe_add_flag(command.no_divider, "--no-divider")
|> maybe_add_option(command.where, "--where")
|> maybe_add_option(command.if_exists, "--if-exists")
|> maybe_add_option(command.if_missing, "--if-missing")
|> maybe_add_trailers(command.trailers)
|> maybe_add_file(command.file)
end

@doc """
Parses the output of `git interpret-trailers`.

On success (exit code 0), returns `{:ok, output}` with the processed
message or trailer text. On failure, returns `{:error, {stdout, exit_code}}`.
"""
@spec parse_output(String.t(), non_neg_integer()) ::
{:ok, String.t()} | {:error, {String.t(), non_neg_integer()}}
@impl true
def parse_output(stdout, 0), do: {:ok, stdout}
def parse_output(stdout, exit_code), do: {:error, {stdout, exit_code}}

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

defp maybe_add_option(args, nil, _flag), do: args
defp maybe_add_option(args, value, flag), do: args ++ [flag, value]

defp maybe_add_trailers(args, []), do: args

defp maybe_add_trailers(args, trailers) do
Enum.reduce(trailers, args, fn trailer, acc ->
acc ++ ["--trailer", trailer]
end)
end

defp maybe_add_file(args, nil), do: args
defp maybe_add_file(args, file), do: args ++ [file]
end
101 changes: 101 additions & 0 deletions lib/git/commands/maintenance.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
defmodule Git.Commands.Maintenance do
@moduledoc """
Implements the `Git.Command` behaviour for `git maintenance`.

Runs, starts, stops, registers, or unregisters repository maintenance tasks
such as garbage collection, commit-graph updates, and prefetching.
"""

@behaviour Git.Command

@type t :: %__MODULE__{
run: boolean(),
start: boolean(),
stop: boolean(),
register_: boolean(),
unregister: boolean(),
task: String.t() | nil,
auto: boolean(),
quiet: boolean(),
schedule: String.t() | nil
}

defstruct run: false,
start: false,
stop: false,
register_: false,
unregister: false,
task: nil,
auto: false,
quiet: false,
schedule: nil

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

## Examples

iex> Git.Commands.Maintenance.args(%Git.Commands.Maintenance{run: true})
["maintenance", "run"]

iex> Git.Commands.Maintenance.args(%Git.Commands.Maintenance{start: true})
["maintenance", "start"]

iex> Git.Commands.Maintenance.args(%Git.Commands.Maintenance{stop: true})
["maintenance", "stop"]

iex> Git.Commands.Maintenance.args(%Git.Commands.Maintenance{register_: true})
["maintenance", "register"]

iex> Git.Commands.Maintenance.args(%Git.Commands.Maintenance{unregister: true})
["maintenance", "unregister"]

iex> Git.Commands.Maintenance.args(%Git.Commands.Maintenance{run: true, task: "gc"})
["maintenance", "run", "--task", "gc"]

iex> Git.Commands.Maintenance.args(%Git.Commands.Maintenance{run: true, auto: true})
["maintenance", "run", "--auto"]

iex> Git.Commands.Maintenance.args(%Git.Commands.Maintenance{run: true, quiet: true})
["maintenance", "run", "--quiet"]

iex> Git.Commands.Maintenance.args(%Git.Commands.Maintenance{run: true, schedule: "daily"})
["maintenance", "run", "--schedule", "daily"]

"""
@spec args(t()) :: [String.t()]
@impl true
def args(%__MODULE__{} = command) do
["maintenance"]
|> add_subcommand(command)
|> maybe_add_option(command.task, "--task")
|> maybe_add_flag(command.auto, "--auto")
|> maybe_add_flag(command.quiet, "--quiet")
|> maybe_add_option(command.schedule, "--schedule")
end

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

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 add_subcommand(args, %{run: true}), do: args ++ ["run"]
defp add_subcommand(args, %{start: true}), do: args ++ ["start"]
defp add_subcommand(args, %{stop: true}), do: args ++ ["stop"]
defp add_subcommand(args, %{register_: true}), do: args ++ ["register"]
defp add_subcommand(args, %{unregister: true}), do: args ++ ["unregister"]
defp add_subcommand(args, _command), do: args

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

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

@doc """
Runs `git interpret-trailers` on the repository.

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

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

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