defmodule OpenAgents.Stacks.MergeTest do
@moduledoc """
Contiguous-prefix stack merge (#51): the merge-commit, squash, and rebase
methods with exact resulting histories, prefix enforcement, upper-layer
restacking after a partial merge, preflight failures, and injected crash
recovery through the persisted plan.
"""
use OpenAgents.DataCase, async: false
alias OpenAgents.Forge.Repos
alias OpenAgents.Issues.Issue
alias OpenAgents.PullRequests.PullRequest
alias OpenAgents.Repo
alias OpenAgents.Stacks
alias OpenAgents.Stacks.Merge
alias OpenAgents.Stacks.Operation
alias OpenAgents.Stacks.OperationWorker
alias OpenAgents.Stacks.Stack
alias OpenAgents.Stacks.StackEntry
alias OpenAgents.Stacks.StackEvent
import Ecto.Query
import OpenAgents.AccountsFixtures
import OpenAgents.IssuesFixtures
setup do
base = Path.join(System.tmp_dir!(), "stack-merge-#{System.unique_integer([:positive])}")
previous_data = Application.get_env(:openagents, :forge_data_dir)
previous_wal = Application.get_env(:openagents, :forge_wal_dir)
Application.put_env(:openagents, :forge_data_dir, Path.join(base, "data"))
Application.put_env(:openagents, :forge_wal_dir, Path.join(base, "wal"))
on_exit(fn ->
restore_env(:forge_data_dir, previous_data)
restore_env(:forge_wal_dir, previous_wal)
File.rm_rf(base)
end)
actor = repository_user_fixture("merge-actor")
repository = repository_with_member_fixture(actor)
%{actor: actor, repository: repository}
end
describe "the merge-commit method" do
test "lands one group merge commit whose tree is the selected top head's", context do
%{repository: repository, actor: actor} = context
%{path: path, oids: oids, stack: stack, pull_requests: [pr_1, pr_2]} =
seed_stack(repository, actor)
{:ok, {operation, :created}} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => pr_2.issue.number, "merge_method" => "merge"},
actor,
"merge-1"
)
assert operation.state == "pending"
assert operation.target_position == 2
assert reload(Stack, stack.id).health == "operation_in_progress"
assert :processed = OperationWorker.run_once()
operation = reload(Operation, operation.id)
assert operation.state == "succeeded"
merge_commit = show(path, ["rev-parse", "refs/heads/main"])
parents = show(path, ["show", "-s", "--format=%P", merge_commit])
assert parents == "#{oids["main"]} #{oids["layer-2"]}"
assert show(path, ["rev-parse", merge_commit <> "^{tree}"]) ==
show(path, ["rev-parse", oids["layer-2"] <> "^{tree}"])
# Both selected pull requests merged with the group merge commit.
for pr <- [pr_1, pr_2] do
merged = Repo.one!(from p in PullRequest, where: p.id == ^pr.id)
assert merged.state == "closed"
assert merged.merge_commit_sha == merge_commit
assert merged.merged_by_user_id == actor.id
refute is_nil(merged.merged_at)
issue = Repo.one!(from i in Issue, where: i.id == ^pr.issue_id)
assert issue.state == "closed"
assert issue.state_reason == "completed"
end
# The merged branches did not move and were not deleted.
assert show(path, ["rev-parse", "refs/heads/layer-1"]) == oids["layer-1"]
assert show(path, ["rev-parse", "refs/heads/layer-2"]) == oids["layer-2"]
stack = reload(Stack, stack.id)
assert stack.state == "completed"
assert stack.health == "healthy"
assert stack.version == 2
assert entries(stack.id) == []
assert Repo.exists?(
from event in StackEvent,
where:
event.event_type == "pull_request_stack.merge_completed" and
event.stack_id == ^stack.id
)
{internal, 0} = Repos.git(path, ["for-each-ref", "refs/internal/"])
assert internal == ""
end
end
describe "the squash method" do
test "chains one commit per pull request so each trunk commit is one layer", context do
%{repository: repository, actor: actor} = context
%{path: path, oids: oids, stack: stack, pull_requests: [pr_1, pr_2]} =
seed_stack(repository, actor)
{:ok, {_operation, :created}} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => pr_2.issue.number, "merge_method" => "squash"},
actor,
"squash-1"
)
assert :processed = OperationWorker.run_once()
squash_2 = show(path, ["rev-parse", "refs/heads/main"])
squash_1 = show(path, ["rev-parse", squash_2 <> "^"])
assert show(path, ["rev-parse", squash_1 <> "^"]) == oids["main"]
# Each squash commit's tree is exactly its layer head's tree.
assert show(path, ["rev-parse", squash_1 <> "^{tree}"]) ==
show(path, ["rev-parse", oids["layer-1"] <> "^{tree}"])
assert show(path, ["rev-parse", squash_2 <> "^{tree}"]) ==
show(path, ["rev-parse", oids["layer-2"] <> "^{tree}"])
# Messages name the pull request and the original author survives.
assert show(path, ["show", "-s", "--format=%s", squash_1]) ==
"PR layer-1 (##{pr_1.issue.number})"
assert show(path, ["show", "-s", "--format=%an <%ae>", squash_1]) ==
"Test Author <author@example.test>"
merged_1 = Repo.one!(from p in PullRequest, where: p.id == ^pr_1.id)
merged_2 = Repo.one!(from p in PullRequest, where: p.id == ^pr_2.id)
assert merged_1.merge_commit_sha == squash_1
assert merged_2.merge_commit_sha == squash_2
end
end
describe "the rebase method" do
test "replays each layer's commits in order onto the trunk", context do
%{repository: repository, actor: actor} = context
%{path: path, oids: oids, stack: stack, pull_requests: [_pr_1, pr_2]} =
seed_stack(repository, actor)
{:ok, {_operation, :created}} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => pr_2.issue.number, "merge_method" => "rebase"},
actor,
"rebase-1"
)
assert :processed = OperationWorker.run_once()
new_tip = show(path, ["rev-parse", "refs/heads/main"])
new_mid = show(path, ["rev-parse", new_tip <> "^"])
assert show(path, ["rev-parse", new_mid <> "^"]) == oids["main"]
# The final tree equals the selected top head's tree, and the replayed
# commits keep their messages.
assert show(path, ["rev-parse", new_tip <> "^{tree}"]) ==
show(path, ["rev-parse", oids["layer-2"] <> "^{tree}"])
assert show(path, ["show", "-s", "--format=%s", new_mid]) == "Layer layer-1"
assert show(path, ["show", "-s", "--format=%s", new_tip]) == "Layer layer-2"
end
end
describe "prefix enforcement" do
test "selecting the bottom layer merges only it and restacks the layers above", context do
%{repository: repository, actor: actor} = context
%{path: path, oids: oids, stack: stack, pull_requests: [pr_1, pr_2]} =
seed_stack(repository, actor)
{:ok, {_operation, :created}} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => pr_1.issue.number, "merge_method" => "merge"},
actor,
"prefix-1"
)
assert :processed = OperationWorker.run_once()
trunk_new = show(path, ["rev-parse", "refs/heads/main"])
merged_1 = Repo.one!(from p in PullRequest, where: p.id == ^pr_1.id)
assert merged_1.state == "closed"
assert merged_1.merge_commit_sha == trunk_new
# The upper layer stays open, restacked onto the merge result, and
# retargets the trunk.
open_2 = Repo.one!(from p in PullRequest, where: p.id == ^pr_2.id)
assert open_2.state == "open"
assert open_2.base_ref == "main"
assert open_2.base_sha == trunk_new
new_layer_2 = show(path, ["rev-parse", "refs/heads/layer-2"])
refute new_layer_2 == oids["layer-2"]
assert show(path, ["rev-parse", new_layer_2 <> "^"]) == trunk_new
assert open_2.head_sha == new_layer_2
stack = reload(Stack, stack.id)
assert stack.state == "open"
assert stack.health == "healthy"
[entry_2] = entries(stack.id)
assert entry_2.boundary_oid == trunk_new
assert entry_2.observed_head_oid == new_layer_2
assert Repo.exists?(
from event in StackEvent,
where:
event.event_type == "pull_request.synchronize" and
event.stack_id == ^stack.id
)
end
test "a lower-prefix merge restacks every upper layer in order", context do
%{repository: repository, actor: actor} = context
%{path: path, oids: oids, stack: stack, pull_requests: [pr_1, pr_2, pr_3, pr_4]} =
seed_stack(repository, actor, ["layer-1", "layer-2", "layer-3", "layer-4"])
{:ok, {_operation, :created}} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => pr_2.issue.number, "merge_method" => "squash"},
actor,
"prefix-multi-1"
)
assert :processed = OperationWorker.run_once()
trunk_new = show(path, ["rev-parse", "refs/heads/main"])
for pr <- [pr_1, pr_2] do
assert Repo.one!(from p in PullRequest, where: p.id == ^pr.id).state == "closed"
end
# Layer 3 restacked onto the squash result, layer 4 onto the new
# layer 3, and only the lowest remaining layer retargets the trunk.
new_layer_3 = show(path, ["rev-parse", "refs/heads/layer-3"])
new_layer_4 = show(path, ["rev-parse", "refs/heads/layer-4"])
refute new_layer_3 == oids["layer-3"]
refute new_layer_4 == oids["layer-4"]
assert show(path, ["rev-parse", new_layer_3 <> "^"]) == trunk_new
assert show(path, ["rev-parse", new_layer_4 <> "^"]) == new_layer_3
open_3 = Repo.one!(from p in PullRequest, where: p.id == ^pr_3.id)
assert open_3.state == "open"
assert open_3.base_ref == "main"
assert open_3.base_sha == trunk_new
assert open_3.head_sha == new_layer_3
open_4 = Repo.one!(from p in PullRequest, where: p.id == ^pr_4.id)
assert open_4.state == "open"
assert open_4.base_ref == "layer-3"
assert open_4.base_sha == new_layer_3
assert open_4.head_sha == new_layer_4
stack = reload(Stack, stack.id)
assert stack.state == "open"
assert stack.health == "healthy"
[entry_3, entry_4] = entries(stack.id)
assert entry_3.boundary_oid == trunk_new
assert entry_3.observed_head_oid == new_layer_3
assert entry_4.boundary_oid == new_layer_3
assert entry_4.observed_head_oid == new_layer_4
end
test "a pull request outside the stack cannot be selected", context do
%{repository: repository, actor: actor} = context
%{stack: stack} = seed_stack(repository, actor)
assert {:error, :pull_request_not_in_stack} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => 999_999, "merge_method" => "merge"},
actor,
"prefix-missing-1"
)
end
end
describe "preflight" do
test "a stack behind its trunk fails with needs_rebase before any ref moves", context do
%{repository: repository, actor: actor} = context
%{path: path, oids: oids, stack: stack, pull_requests: [_pr_1, pr_2]} =
seed_stack(repository, actor)
trunk_tip = commit(path, oids["main"], "Trunk advance", %{"trunk.md" => "trunk\n"})
{_, 0} = Repos.git(path, ["update-ref", "refs/heads/main", trunk_tip])
{:ok, {operation, :created}} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => pr_2.issue.number, "merge_method" => "merge"},
actor,
"behind-1"
)
assert :processed = OperationWorker.run_once()
operation = reload(Operation, operation.id)
assert operation.state == "failed"
assert operation.error["code"] == "needs_rebase"
assert reload(Stack, stack.id).health == "needs_rebase"
assert show(path, ["rev-parse", "refs/heads/main"]) == trunk_tip
assert Repo.one!(from p in PullRequest, where: p.id == ^pr_2.id).state == "open"
end
test "a boundary the branch does not contain fails instead of reverting the trunk",
context do
%{repository: repository, actor: actor} = context
%{path: path, oids: oids, stack: stack, pull_requests: [pr_1, pr_2]} =
seed_stack(repository, actor)
# The trunk gains a commit the stack branches never built on, and the
# bottom entry records that tip as its boundary — the shape a stack
# takes when its boundary was snapshotted from a moved trunk rather
# than the branch's real fork point. The chain check alone passes, so
# only boundary ancestry stands between this merge and a trunk whose
# tree silently loses trunk.md.
trunk_tip = commit(path, oids["main"], "Trunk advance", %{"trunk.md" => "trunk\n"})
{_, 0} = Repos.git(path, ["update-ref", "refs/heads/main", trunk_tip])
{1, _rows} =
Repo.update_all(
from(entry in StackEntry, where: entry.stack_id == ^stack.id and entry.position == 1),
set: [boundary_oid: trunk_tip]
)
{:ok, {operation, :created}} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => pr_2.issue.number, "merge_method" => "merge"},
actor,
"bad-boundary-1"
)
assert :processed = OperationWorker.run_once()
operation = reload(Operation, operation.id)
assert operation.state == "failed"
assert operation.error["code"] == "needs_rebase"
assert reload(Stack, stack.id).health == "needs_rebase"
# The trunk did not move and still carries the advance commit.
assert show(path, ["rev-parse", "refs/heads/main"]) == trunk_tip
assert show(path, ["rev-parse", "refs/heads/main:trunk.md"]) != ""
for pr <- [pr_1, pr_2] do
assert Repo.one!(from p in PullRequest, where: p.id == ^pr.id).state == "open"
end
end
test "a concurrent branch push fails the merge and preserves the branch", context do
%{repository: repository, actor: actor} = context
%{path: path, oids: oids, stack: stack, pull_requests: [_pr_1, pr_2]} =
seed_stack(repository, actor)
{:ok, {operation, :created}} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => pr_2.issue.number, "merge_method" => "merge"},
actor,
"race-1"
)
pushed = commit(path, oids["layer-1"], "User push", %{"extra.md" => "extra\n"})
{_, 0} = Repos.git(path, ["update-ref", "refs/heads/layer-1", pushed])
assert :processed = OperationWorker.run_once()
operation = reload(Operation, operation.id)
assert operation.state == "failed"
assert operation.error["code"] == "head_changed"
assert show(path, ["rev-parse", "refs/heads/layer-1"]) == pushed
assert show(path, ["rev-parse", "refs/heads/main"]) == oids["main"]
end
test "an expected head mismatch fails before any ref moves", context do
%{repository: repository, actor: actor} = context
%{path: path, oids: oids, stack: stack, pull_requests: [pr_1, pr_2]} =
seed_stack(repository, actor)
wrong = String.duplicate("ab", 20)
{:ok, {operation, :created}} =
Merge.request_from_api(
repository,
stack.number,
%{
"pull_request_number" => pr_2.issue.number,
"merge_method" => "merge",
"expected_heads" => %{"#{pr_1.issue.number}" => wrong}
},
actor,
"expected-1"
)
assert :processed = OperationWorker.run_once()
operation = reload(Operation, operation.id)
assert operation.state == "failed"
assert operation.error["code"] == "expected_head_mismatch"
assert show(path, ["rev-parse", "refs/heads/main"]) == oids["main"]
end
test "a stale expected stack version is rejected at request time", context do
%{repository: repository, actor: actor} = context
%{stack: stack, pull_requests: [_pr_1, pr_2]} = seed_stack(repository, actor)
assert {:error, :stale_stack_version} =
Merge.request_from_api(
repository,
stack.number,
%{
"pull_request_number" => pr_2.issue.number,
"merge_method" => "merge",
"expected_stack_version" => 42
},
actor,
"stale-1"
)
end
test "queueing is not yet available", context do
%{repository: repository, actor: actor} = context
%{stack: stack, pull_requests: [_pr_1, pr_2]} = seed_stack(repository, actor)
assert {:error, :merge_queue_unavailable} =
Merge.request_from_api(
repository,
stack.number,
%{
"pull_request_number" => pr_2.issue.number,
"merge_method" => "merge",
"merge_action" => "queue"
},
actor,
"queue-1"
)
end
test "a reader cannot request a merge", context do
%{repository: repository, actor: actor} = context
%{stack: stack, pull_requests: [_pr_1, pr_2]} = seed_stack(repository, actor)
reader = repository_user_fixture("merge-reader")
assert {:error, :forbidden} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => pr_2.issue.number, "merge_method" => "merge"},
reader,
"reader-1"
)
end
end
describe "idempotency" do
test "a retried key replays the original operation", context do
%{repository: repository, actor: actor} = context
%{stack: stack, pull_requests: [_pr_1, pr_2]} = seed_stack(repository, actor)
request = %{"pull_request_number" => pr_2.issue.number, "merge_method" => "merge"}
{:ok, {operation, :created}} =
Merge.request_from_api(repository, stack.number, request, actor, "idem-1")
assert {:ok, {replayed, :replayed}} =
Merge.request_from_api(repository, stack.number, request, actor, "idem-1")
assert replayed.id == operation.id
assert {:error, :idempotency_conflict} =
Merge.request_from_api(
repository,
stack.number,
Map.put(request, "merge_method", "squash"),
actor,
"idem-1"
)
end
end
describe "crash recovery" do
test "a crash between the git refs and the metadata reconciles to success", context do
%{repository: repository, actor: actor} = context
%{path: path, stack: stack, pull_requests: [pr_1, pr_2]} = seed_stack(repository, actor)
{:ok, {operation, :created}} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => pr_2.issue.number, "merge_method" => "merge"},
actor,
"crash-1"
)
# A worker claims the row, moves the refs, and dies before the
# metadata transaction.
{1, _rows} =
Repo.update_all(
from(o in Operation, where: o.id == ^operation.id),
set: [state: "running", claimed_at: DateTime.utc_now(), attempt_count: 1]
)
claimed = reload(Operation, operation.id)
assert_raise RuntimeError, "injected crash", fn ->
Merge.execute(claimed, after_refs: fn -> raise "injected crash" end)
end
# The refs landed but the metadata did not: the pull requests still
# read open and the plan is marked applied.
crashed = reload(Operation, operation.id)
assert crashed.state == "running"
assert crashed.planned_result["refs_applied"] == true
assert show(path, ["rev-parse", "refs/heads/main"]) == crashed.planned_result["trunk_new"]
assert Repo.one!(from p in PullRequest, where: p.id == ^pr_1.id).state == "open"
# The lease expires and another worker reconciles instead of
# re-merging.
stale = DateTime.add(DateTime.utc_now(), -600, :second)
{1, _rows} =
Repo.update_all(
from(o in Operation, where: o.id == ^operation.id),
set: [claimed_at: stale]
)
assert :processed = OperationWorker.run_once()
operation = reload(Operation, operation.id)
assert operation.state == "succeeded"
assert operation.attempt_count == 2
trunk_new = show(path, ["rev-parse", "refs/heads/main"])
assert trunk_new == operation.planned_result["trunk_new"]
for pr <- [pr_1, pr_2] do
merged = Repo.one!(from p in PullRequest, where: p.id == ^pr.id)
assert merged.state == "closed"
assert merged.merge_commit_sha == trunk_new
end
assert reload(Stack, stack.id).state == "completed"
end
test "a crash after a diverged repository marks the operation partial", context do
%{repository: repository, actor: actor} = context
%{path: path, oids: oids, stack: stack, pull_requests: [_pr_1, pr_2]} =
seed_stack(repository, actor)
{:ok, {operation, :created}} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => pr_2.issue.number, "merge_method" => "merge"},
actor,
"diverge-1"
)
{1, _rows} =
Repo.update_all(
from(o in Operation, where: o.id == ^operation.id),
set: [state: "running", claimed_at: DateTime.utc_now(), attempt_count: 1]
)
claimed = reload(Operation, operation.id)
assert_raise RuntimeError, "injected crash", fn ->
Merge.execute(claimed, after_refs: fn -> raise "injected crash" end)
end
# A trunk push lands after the crash, so the live refs no longer match
# the plan.
trunk_new = show(path, ["rev-parse", "refs/heads/main"])
diverged = commit(path, trunk_new, "Post-crash push", %{"post.md" => "post\n"})
{:ok, _result} =
OpenAgents.Forge.GitPlane.batch_update_refs(
repository.storage_key,
[%{ref: "refs/heads/main", expected_old: trunk_new, new: diverged}],
"test-push"
)
stale = DateTime.add(DateTime.utc_now(), -600, :second)
{1, _rows} =
Repo.update_all(
from(o in Operation, where: o.id == ^operation.id),
set: [claimed_at: stale]
)
assert :processed = OperationWorker.run_once()
operation = reload(Operation, operation.id)
assert operation.state == "partially_succeeded"
assert operation.error["code"] == "refs_diverged"
assert show(path, ["rev-parse", "refs/heads/main"]) == diverged
# The merged prefix stays landed underneath the diverged push.
assert oids["layer-2"] ==
show(path, ["rev-parse", operation.planned_result["trunk_new"] <> "^2"])
end
end
describe "the merged review context" do
test "a merged pull request resolves its historical stack placement", context do
%{repository: repository, actor: actor} = context
%{stack: stack, pull_requests: [pr_1, pr_2]} = seed_stack(repository, actor)
{:ok, {_operation, :created}} =
Merge.request_from_api(
repository,
stack.number,
%{"pull_request_number" => pr_2.issue.number, "merge_method" => "merge"},
actor,
"merged-context-1"
)
assert :processed = OperationWorker.run_once()
for pr <- [pr_1, pr_2] do
merged = Repo.one!(from p in PullRequest, where: p.id == ^pr.id)
assert {:ok, merged_context} = Stacks.merged_context(merged)
assert merged_context.stack.id == stack.id
assert merged_context.size == 2
assert merged_context.entry.pull_request_id == pr.id
assert Enum.map(merged_context.stack.entries, & &1.position) == [1, 2]
assert Enum.map(merged_context.stack.entries, & &1.pull_request.issue.number) ==
[pr_1.issue.number, pr_2.issue.number]
end
end
test "an unmerged stacked pull request has no merged context", context do
%{repository: repository, actor: actor} = context
%{pull_requests: [pr_1, _pr_2]} = seed_stack(repository, actor)
assert {:error, :not_stacked} = Stacks.merged_context(pr_1)
end
end
## Fixtures
# main ── layer-1 ── layer-2 ── …, each layer adding one file.
defp seed_stack(repository, actor, branches \\ ["layer-1", "layer-2"]) do
path = Repos.ensure_repo!(repository.storage_key, repository.default_branch)
main = commit(path, nil, "Seed repository", %{"README.md" => "readme\n"})
{_, 0} = Repos.git(path, ["update-ref", "refs/heads/main", main])
{oids, _parent} =
Enum.reduce(branches, {%{"main" => main}, main}, fn branch, {oids, parent} ->
oid = commit(path, parent, "Layer #{branch}", %{"#{branch}.md" => "#{branch}\n"})
{_, 0} = Repos.git(path, ["update-ref", "refs/heads/#{branch}", oid])
{Map.put(oids, branch, oid), oid}
end)
pull_requests =
branches
|> Enum.with_index()
|> Enum.map(fn {branch, index} ->
base = Enum.at(["main" | branches], index)
pull_request(repository, branch, base, oids[base], oids[branch])
end)
{:ok, stack} = Stacks.create(repository, pull_requests, actor)
%{path: path, oids: oids, stack: stack, pull_requests: pull_requests}
end
defp pull_request(repository, head_ref, base_ref, base_sha, head_sha) do
issue = issue_fixture(repository, %{title: "PR #{head_ref}"})
{:ok, pull_request} =
%PullRequest{}
|> PullRequest.changeset(%{
repository_id: repository.id,
issue_id: issue.id,
head_repository_id: repository.id,
head_ref: head_ref,
head_sha: head_sha,
base_ref: base_ref,
base_sha: base_sha,
state: "open"
})
|> Repo.insert()
Repo.preload(pull_request, :issue)
end
# Commits a tree that layers the given files over the parent's tree.
defp commit(path, parent, message, files) do
parent_entries =
if parent do
{listing, 0} = Repos.git(path, ["ls-tree", parent])
listing
|> String.split("\n", trim: true)
|> Map.new(fn line ->
[meta, name] = String.split(line, "\t", parts: 2)
{name, meta <> "\t" <> name}
end)
else
%{}
end
new_entries =
Map.new(files, fn {name, content} ->
blob = git!(path, ["hash-object", "-w", "--stdin"], content)
{name, "100644 blob #{blob}\t#{name}"}
end)
listing =
parent_entries
|> Map.merge(new_entries)
|> Map.values()
|> Enum.map_join("", &(&1 <> "\n"))
tree = git!(path, ["mktree"], listing)
parent_args = if parent, do: ["-p", parent], else: []
git!(path, ["commit-tree", tree] ++ parent_args ++ ["-m", message], "",
env: [
{"GIT_AUTHOR_NAME", "Test Author"},
{"GIT_AUTHOR_EMAIL", "author@example.test"},
{"GIT_COMMITTER_NAME", "Test Author"},
{"GIT_COMMITTER_EMAIL", "author@example.test"}
]
)
end
defp entries(stack_id) do
Repo.all(
from entry in StackEntry,
where: entry.stack_id == ^stack_id and is_nil(entry.removed_at),
order_by: [asc: entry.position]
)
end
defp reload(schema, id), do: Repo.get!(schema, id)
defp show(path, args) do
{output, 0} = Repos.git(path, args)
String.trim(output)
end
defp git!(git_dir, args, input, options \\ []) do
input_path = Path.join(System.tmp_dir!(), "merge-input-#{System.unique_integer([:positive])}")
File.write!(input_path, input)
try do
{output, 0} =
System.cmd(
"sh",
["-c", ~s(exec git --git-dir "$GIT_DIR" "$@" < "$INPUT"), "sh"] ++ args,
env: [{"GIT_DIR", git_dir}, {"INPUT", input_path}] ++ Keyword.get(options, :env, [])
)
String.trim(output)
after
File.rm(input_path)
end
end
defp restore_env(key, nil), do: Application.delete_env(:openagents, key)
defp restore_env(key, value), do: Application.put_env(:openagents, key, value)
end