diff --git a/assets/css/overlays.css b/assets/css/overlays.css
index 901693f..5d27594 100644
--- a/assets/css/overlays.css
+++ b/assets/css/overlays.css
@@ -19,6 +19,7 @@
.language-picker-modal-backdrop,
.confirm-delete-modal-backdrop,
.confirm-dialog-overlay,
+.git-run-backdrop,
.gallery-overlay,
.lightbox-overlay {
position: fixed;
@@ -35,6 +36,7 @@
.language-picker-modal,
.confirm-delete-modal,
.confirm-dialog,
+.git-run-modal,
.gallery-overlay-content {
position: relative;
z-index: 1;
@@ -44,6 +46,76 @@
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
+.git-run-modal {
+ width: min(760px, calc(100vw - 32px));
+ max-height: calc(100vh - 48px);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.git-run-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 12px 16px;
+ border-bottom: 1px solid #3c3c3c;
+}
+
+.git-run-title {
+ margin: 0;
+ font-size: 14px;
+ font-family: var(--font-mono, monospace);
+}
+
+.git-run-status {
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.git-run-status.running {
+ color: #d7ba7d;
+}
+
+.git-run-status.ok {
+ color: #4ec9b0;
+}
+
+.git-run-status.fail {
+ color: #f48771;
+}
+
+.git-run-output {
+ margin: 0;
+ padding: 12px 16px;
+ overflow: auto;
+ flex: 1;
+ min-height: 160px;
+ max-height: 60vh;
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-family: var(--font-mono, monospace);
+ font-size: 12px;
+ line-height: 1.5;
+ background: #141414;
+}
+
+.git-run-stdout {
+ color: #d4d4d4;
+}
+
+.git-run-stderr {
+ color: #f48771;
+}
+
+.git-run-footer {
+ display: flex;
+ justify-content: flex-end;
+ padding: 12px 16px;
+ border-top: 1px solid #3c3c3c;
+}
+
.ai-suggestions-modal,
.language-picker-modal,
.confirm-delete-modal,
diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex
index cf0560e..474d551 100644
--- a/lib/bds/desktop/shell_live.ex
+++ b/lib/bds/desktop/shell_live.ex
@@ -14,6 +14,7 @@ defmodule BDS.Desktop.ShellLive do
ChatEditor,
GalleryImport,
GitHandler,
+ GitRun,
ImportEditor,
MediaEditor,
MenuEditor,
@@ -62,6 +63,7 @@ defmodule BDS.Desktop.ShellLive do
@refresh_interval 1_500
def refresh_interval, do: @refresh_interval
+
@sidebar_filter_events [
"toggle_sidebar_filters",
"toggle_sidebar_archive",
@@ -176,12 +178,13 @@ defmodule BDS.Desktop.ShellLive do
|> assign(:chat_editor_request_refs, %{})
|> assign(:file_picker_task, nil)
|> assign(:shell_overlay, nil)
+ |> assign(:git_run, nil)
|> assign(:output_entries, [])
|> assign(:panel_post_links, %{backlinks: [], outlinks: []})
|> assign(:panel_git_entries, [])
|> assign(:auto_save_timers, %{})
|> reload_shell(workbench)
- |> UrlState.apply_params(params)
+ |> UrlState.apply_params(params)
|> tap(&sync_menu_bar_locale/1)}
end
@@ -209,7 +212,7 @@ defmodule BDS.Desktop.ShellLive do
{:noreply,
socket
|> refresh_sidebar(workbench)
- |> UrlState.push()}
+ |> UrlState.push()}
end
def handle_event("select_panel_tab", %{"tab" => tab}, socket) do
@@ -242,9 +245,25 @@ defmodule BDS.Desktop.ShellLive do
end
def handle_event(event, _params, socket) when event in @git_action_events do
- {:noreply, GitHandler.run_action(socket, event)}
+ if GitRun.network_event?(event) do
+ {:noreply, GitRun.start(socket, event, current_project_id(socket))}
+ else
+ {:noreply, GitHandler.run_action(socket, event)}
+ end
end
+ def handle_event("git_run_close", _params, socket) do
+ socket = GitRun.close(socket)
+ {:noreply, refresh_sidebar(socket, socket.assigns.workbench)}
+ end
+
+ def handle_event("git_run_keydown", %{"key" => "Escape"}, socket) do
+ socket = GitRun.close(socket)
+ {:noreply, refresh_sidebar(socket, socket.assigns.workbench)}
+ end
+
+ def handle_event("git_run_keydown", _params, socket), do: {:noreply, socket}
+
def handle_event("git_commit", params, socket) do
message = params |> get_in(["git", "message"]) |> to_string() |> String.trim()
@@ -302,7 +321,7 @@ defmodule BDS.Desktop.ShellLive do
socket
|> assign(:tab_meta, tab_meta)
|> refresh_layout(workbench)
- |> UrlState.push()}
+ |> UrlState.push()}
end
def handle_event(
@@ -691,6 +710,15 @@ defmodule BDS.Desktop.ShellLive do
{:noreply, handle_blogmark_deep_link(socket, url)}
end
+ def handle_info({:git_output, ref, stream, chunk}, socket) do
+ {:noreply, GitRun.append(socket, ref, stream, chunk)}
+ end
+
+ def handle_info({:git_done, ref, status}, socket) do
+ socket = GitRun.done(socket, ref, status)
+ {:noreply, refresh_sidebar(socket, socket.assigns.workbench)}
+ end
+
def handle_info(message, socket) do
Bridges.handle_info(message, socket, bridges_callbacks())
end
@@ -987,7 +1015,8 @@ defmodule BDS.Desktop.ShellLive do
defp handle_socket_menu_action(socket, :save), do: TabActions.save_current_tab(socket)
- defp handle_socket_menu_action(socket, :publish_selected), do: TabActions.publish_current_tab(socket)
+ defp handle_socket_menu_action(socket, :publish_selected),
+ do: TabActions.publish_current_tab(socket)
defp handle_socket_menu_action(socket, :quit) do
Shutdown.request_quit()
diff --git a/lib/bds/desktop/shell_live/git_run.ex b/lib/bds/desktop/shell_live/git_run.ex
new file mode 100644
index 0000000..016e346
--- /dev/null
+++ b/lib/bds/desktop/shell_live/git_run.ex
@@ -0,0 +1,130 @@
+defmodule BDS.Desktop.ShellLive.GitRun do
+ @moduledoc """
+ Live modal for streaming network git commands (fetch/pull/push).
+
+ Starts a `BDS.Git.Stream`, appends its stdout/stderr chunks as they arrive
+ (stderr rendered in red), shows a spinner while running and a pass/fail banner
+ from the exit status. Closing the modal cancels a still-running command, which
+ kills the OS process.
+ """
+
+ use Phoenix.Component
+
+ use Gettext, backend: BDS.Gettext
+
+ alias BDS.Git
+
+ @operations %{"git_fetch" => :fetch, "git_pull" => :pull, "git_push" => :push}
+
+ @doc "True when the shell event is a streaming network git action."
+ def network_event?(event), do: Map.has_key?(@operations, event)
+
+ @doc "Open the modal and start streaming the operation."
+ def start(socket, event, project_id) do
+ operation = Map.fetch!(@operations, event)
+
+ run =
+ case start_stream(project_id, operation) do
+ {:ok, pid, ref} ->
+ %{operation: operation, pid: pid, ref: ref, lines: [], status: :running}
+
+ {:error, reason} ->
+ %{
+ operation: operation,
+ pid: nil,
+ ref: nil,
+ lines: [{:stderr, error_text(reason)}],
+ status: {:done, 1}
+ }
+ end
+
+ assign(socket, :git_run, run)
+ end
+
+ @doc "Append a streamed chunk to the modal (newest first internally)."
+ def append(socket, ref, stream, chunk) do
+ case socket.assigns[:git_run] do
+ %{ref: ^ref, lines: lines} = run ->
+ assign(socket, :git_run, %{run | lines: [{stream, chunk} | lines]})
+
+ _other ->
+ socket
+ end
+ end
+
+ @doc "Mark the run finished with its exit status."
+ def done(socket, ref, status) do
+ case socket.assigns[:git_run] do
+ %{ref: ^ref} = run ->
+ assign(socket, :git_run, %{run | status: {:done, status}, pid: nil})
+
+ _other ->
+ socket
+ end
+ end
+
+ @doc "Close the modal, cancelling (killing) a still-running command."
+ def close(socket) do
+ case socket.assigns[:git_run] do
+ %{status: :running, pid: pid} when is_pid(pid) -> Git.Stream.cancel(pid)
+ _other -> :ok
+ end
+
+ assign(socket, :git_run, nil)
+ end
+
+ defp start_stream(nil, _operation), do: {:error, :no_project}
+ defp start_stream("default", _operation), do: {:error, :no_project}
+
+ defp start_stream(project_id, operation) when is_binary(project_id),
+ do: Git.stream(project_id, operation, self())
+
+ defp error_text(reason) when reason in [:no_project, :not_found],
+ do: dgettext("ui", "No active project") <> "\n"
+
+ defp error_text(reason), do: inspect(reason) <> "\n"
+
+ def git_run_modal(assigns) do
+ ~H"""
+ <%= if @git_run do %>
+
+
+
+
+
<%= for {stream, text} <- Enum.reverse(@git_run.lines) do %>{text}<% end %>
+
+
+
+ <% end %>
+ """
+ end
+
+ defp title(:fetch), do: dgettext("ui", "git fetch")
+ defp title(:pull), do: dgettext("ui", "git pull")
+ defp title(:push), do: dgettext("ui", "git push")
+
+ defp line_class(:stderr), do: "git-run-line git-run-stderr"
+ defp line_class(:stdout), do: "git-run-line git-run-stdout"
+
+ defp status_class(:running), do: "running"
+ defp status_class({:done, 0}), do: "ok"
+ defp status_class({:done, _status}), do: "fail"
+
+ defp status_label(:running), do: dgettext("ui", "Running…")
+ defp status_label({:done, 0}), do: dgettext("ui", "Done")
+
+ defp status_label({:done, status}),
+ do: dgettext("ui", "Failed (exit %{status})", status: status)
+
+ defp close_label(:running), do: dgettext("ui", "Cancel")
+ defp close_label({:done, _status}), do: dgettext("ui", "Close")
+end
diff --git a/lib/bds/desktop/shell_live/index.html.heex b/lib/bds/desktop/shell_live/index.html.heex
index fe5c834..f40df65 100644
--- a/lib/bds/desktop/shell_live/index.html.heex
+++ b/lib/bds/desktop/shell_live/index.html.heex
@@ -678,4 +678,5 @@
+
diff --git a/lib/bds/git.ex b/lib/bds/git.ex
index 1e7ca99..0b11b38 100644
--- a/lib/bds/git.ex
+++ b/lib/bds/git.ex
@@ -133,7 +133,6 @@ defmodule BDS.Git do
opts
),
{:ok, remote_log} <- remote_history_log(project_dir, branch, opts) do
-
local_commits = parse_history_log(local_log)
remote_hashes = MapSet.new(parse_remote_history(remote_log))
local_hashes = MapSet.new(Enum.map(local_commits, & &1.hash))
@@ -194,6 +193,30 @@ defmodule BDS.Git do
end
end
+ @doc """
+ Start a streaming network git command (`:fetch | :pull | :push`), forwarding
+ output to `subscriber` (see `BDS.Git.Stream`) so the UI can show it live and
+ cancel a hanging run. Returns `{:ok, pid, ref}`, or `{:error, reason}` when the
+ project can't be resolved.
+ """
+ def stream(project_id, operation, subscriber, opts \\ [])
+ when is_binary(project_id) and operation in [:fetch, :pull, :push] and is_pid(subscriber) do
+ with {:ok, project_dir} <- project_dir(project_id) do
+ env = command_opts(project_dir, 0)[:env]
+
+ BDS.Git.Stream.start(
+ subscriber,
+ project_dir,
+ stream_args(operation),
+ Keyword.put(opts, :env, env)
+ )
+ end
+ end
+
+ defp stream_args(:fetch), do: ["fetch", "--all", "--prune", "--progress"]
+ defp stream_args(:pull), do: ["pull", "--ff-only", "--progress"]
+ defp stream_args(:push), do: ["push", "--progress"]
+
def commit_all(project_id, message, opts \\ [])
when is_binary(project_id) and is_binary(message) and is_list(opts) do
with {:ok, project_dir} <- project_dir(project_id),
diff --git a/lib/bds/git/stream.ex b/lib/bds/git/stream.ex
new file mode 100644
index 0000000..480ca00
--- /dev/null
+++ b/lib/bds/git/stream.ex
@@ -0,0 +1,142 @@
+defmodule BDS.Git.Stream do
+ @moduledoc """
+ Runs a git command and streams its output to a subscriber process as it
+ arrives, keeping stdout and stderr on separate channels.
+
+ A plain Erlang port merges stderr into stdout, so we redirect the command's
+ stderr through a FIFO (`sh -c 'exec "$0" "$@" 2>fifo'`) and read that FIFO with
+ a second `cat` port. `sh`, `cat` and `mkfifo` are system binaries, so no extra
+ dependency is bundled.
+
+ The subscriber receives:
+
+ * `{:git_output, ref, :stdout, chunk}`
+ * `{:git_output, ref, :stderr, chunk}`
+ * `{:git_done, ref, exit_status}` (`0` on success)
+
+ Send `cancel/1` to terminate a hanging command; the OS process is killed and a
+ `{:git_done, ref, status}` still follows.
+ """
+
+ @drain_ms 200
+
+ @spec start(pid, String.t(), [String.t()], keyword) :: {:ok, pid, reference}
+ def start(subscriber, cwd, args, opts \\ [])
+ when is_pid(subscriber) and is_binary(cwd) and is_list(args) do
+ ref = make_ref()
+ pid = spawn(fn -> init(subscriber, ref, cwd, args, opts) end)
+ {:ok, pid, ref}
+ end
+
+ @spec cancel(pid) :: :ok
+ def cancel(pid) when is_pid(pid) do
+ send(pid, :cancel)
+ :ok
+ end
+
+ defp init(subscriber, ref, cwd, args, opts) do
+ command = Keyword.get(opts, :command, "git")
+ env = Keyword.get(opts, :env, %{})
+
+ case System.find_executable(command) do
+ nil ->
+ send(subscriber, {:git_output, ref, :stderr, "missing executable: #{command}\n"})
+ send(subscriber, {:git_done, ref, 127})
+
+ executable ->
+ run(subscriber, ref, cwd, executable, args, env)
+ end
+ end
+
+ defp run(subscriber, ref, cwd, executable, args, env) do
+ fifo_dir = Path.join(System.tmp_dir!(), "bds-git-fifo-#{System.unique_integer([:positive])}")
+ File.mkdir_p!(fifo_dir)
+ fifo = Path.join(fifo_dir, "stderr")
+
+ try do
+ {_out, 0} = System.cmd("mkfifo", [fifo])
+
+ # exec replaces sh with the target binary, so the OS pid and exit status we
+ # observe on this port are the command's own.
+ script = ~s(exec "$0" "$@" 2>'#{fifo}')
+
+ out_port =
+ Port.open(
+ {:spawn_executable, System.find_executable("sh")},
+ [
+ :binary,
+ :exit_status,
+ :use_stdio,
+ :hide,
+ {:cd, cwd},
+ {:env, port_env(env)},
+ {:args, ["-c", script, executable | args]}
+ ]
+ )
+
+ err_port =
+ Port.open(
+ {:spawn_executable, System.find_executable("cat")},
+ [:binary, :exit_status, :use_stdio, :hide, {:args, [fifo]}]
+ )
+
+ loop(subscriber, ref, out_port, err_port)
+ after
+ File.rm_rf(fifo_dir)
+ end
+ end
+
+ defp loop(subscriber, ref, out_port, err_port) do
+ receive do
+ {^out_port, {:data, data}} ->
+ send(subscriber, {:git_output, ref, :stdout, data})
+ loop(subscriber, ref, out_port, err_port)
+
+ {^err_port, {:data, data}} ->
+ send(subscriber, {:git_output, ref, :stderr, data})
+ loop(subscriber, ref, out_port, err_port)
+
+ {^err_port, {:exit_status, _status}} ->
+ loop(subscriber, ref, out_port, nil)
+
+ {^out_port, {:exit_status, status}} ->
+ drain(subscriber, ref, err_port)
+ send(subscriber, {:git_done, ref, status})
+
+ :cancel ->
+ kill(out_port)
+ loop(subscriber, ref, out_port, err_port)
+ end
+ end
+
+ defp drain(_subscriber, _ref, nil), do: :ok
+
+ defp drain(subscriber, ref, err_port) do
+ receive do
+ {^err_port, {:data, data}} ->
+ send(subscriber, {:git_output, ref, :stderr, data})
+ drain(subscriber, ref, err_port)
+
+ {^err_port, {:exit_status, _status}} ->
+ :ok
+ after
+ @drain_ms -> :ok
+ end
+ end
+
+ defp kill(port) do
+ case Port.info(port, :os_pid) do
+ {:os_pid, os_pid} when is_integer(os_pid) ->
+ System.cmd("kill", ["-TERM", Integer.to_string(os_pid)], stderr_to_stdout: true)
+
+ _other ->
+ :ok
+ end
+ end
+
+ defp port_env(env) do
+ Enum.map(env, fn {key, value} ->
+ {String.to_charlist(to_string(key)), String.to_charlist(to_string(value))}
+ end)
+ end
+end
diff --git a/priv/gettext/de/LC_MESSAGES/ui.po b/priv/gettext/de/LC_MESSAGES/ui.po
index f14a0ba..3062338 100644
--- a/priv/gettext/de/LC_MESSAGES/ui.po
+++ b/priv/gettext/de/LC_MESSAGES/ui.po
@@ -336,7 +336,7 @@ msgstr "Autor"
msgid "Auto"
msgstr "Automatisch"
-#: lib/bds/desktop/shell_live.ex:424
+#: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
@@ -418,6 +418,7 @@ msgstr "Blogmark-Kategorie"
msgid "Bookmarklet copy support is wired through the desktop runtime and project public URL."
msgstr "Die Bookmarklet-Kopierfunktion ist über die Desktop-Laufzeit und die öffentliche Projekt-URL verdrahtet."
+#: lib/bds/desktop/shell_live/git_run.ex:126
#: lib/bds/desktop/shell_live/import_editor.ex:1366
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:298
#: lib/bds/desktop/shell_live/menu_editor.ex:335
@@ -485,7 +486,7 @@ msgstr "Kategorie-Standards, Render-Flags und Template-Zuordnung"
msgid "Category name is required"
msgstr "Kategoriename ist erforderlich"
-#: lib/bds/desktop/shell_live.ex:721
+#: lib/bds/desktop/shell_live.ex:748
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:233
#: lib/bds/desktop/shell_live/chat_editor.ex:323
@@ -1062,7 +1063,7 @@ msgstr "Galerie"
msgid "Generate Site"
msgstr "Website generieren"
-#: lib/bds/desktop/shell_live.ex:722
+#: lib/bds/desktop/shell_live.ex:749
#: lib/bds/desktop/shell_live/socket_state.ex:109
#: lib/bds/ui/sidebar.ex:789
#, elixir-autogen, elixir-format
@@ -1076,7 +1077,7 @@ msgid "Git Diff"
msgstr "Git-Diff"
#: lib/bds/desktop/shell_data.ex:226
-#: lib/bds/desktop/shell_live.ex:718
+#: lib/bds/desktop/shell_live.ex:745
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1199,9 +1200,9 @@ msgstr "Importdefinitionen"
msgid "Import failed: %{error}"
msgstr "Import fehlgeschlagen: %{error}"
-#: lib/bds/desktop/shell_live.ex:574
-#: lib/bds/desktop/shell_live.ex:834
-#: lib/bds/desktop/shell_live.ex:840
+#: lib/bds/desktop/shell_live.ex:592
+#: lib/bds/desktop/shell_live.ex:861
+#: lib/bds/desktop/shell_live.ex:867
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1783,7 +1784,7 @@ msgstr "Sonstige"
msgid "Other (%{count})"
msgstr "Andere (%{count})"
-#: lib/bds/desktop/shell_live.ex:717
+#: lib/bds/desktop/shell_live.ex:744
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -2537,7 +2538,7 @@ msgstr "Schlagwortname"
msgid "Tags"
msgstr "Tags"
-#: lib/bds/desktop/shell_live.ex:716
+#: lib/bds/desktop/shell_live.ex:743
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -3217,12 +3218,12 @@ msgstr "Willkommen beim KI-Assistenten"
msgid "Comparing database and filesystem metadata"
msgstr "Vergleicht Datenbank- und Dateisystem-Metadaten"
-#: lib/bds/desktop/shell_live.ex:650
+#: lib/bds/desktop/shell_live.ex:668
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "%{count} Bilder zum Beitrag hinzugefügt"
-#: lib/bds/desktop/shell_live.ex:618
+#: lib/bds/desktop/shell_live.ex:636
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "%{title} hinzugefügt"
@@ -3242,18 +3243,18 @@ msgstr "Endbenutzer-Anleitung für redaktionelle Arbeitsabläufe, Medien, Vorlag
msgid "Image Import Concurrency"
msgstr "Gleichzeitige Bildimporte"
-#: lib/bds/desktop/shell_live.ex:423
-#: lib/bds/desktop/shell_live.ex:436
-#: lib/bds/desktop/shell_live.ex:617
-#: lib/bds/desktop/shell_live.ex:649
-#: lib/bds/desktop/shell_live.ex:660
-#: lib/bds/desktop/shell_live.ex:671
+#: lib/bds/desktop/shell_live.ex:441
+#: lib/bds/desktop/shell_live.ex:454
+#: lib/bds/desktop/shell_live.ex:635
+#: lib/bds/desktop/shell_live.ex:667
+#: lib/bds/desktop/shell_live.ex:678
+#: lib/bds/desktop/shell_live.ex:689
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Galerie-Bilder hinzufügen"
-#: lib/bds/desktop/shell_live.ex:672
+#: lib/bds/desktop/shell_live.ex:690
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "%{path} konnte nicht verarbeitet werden: %{reason}"
@@ -3322,6 +3323,7 @@ msgid "Commit message is required"
msgstr "Commit-Nachricht erforderlich"
#: lib/bds/desktop/shell_live/git_handler.ex:106
+#: lib/bds/desktop/shell_live/git_run.ex:123
#, elixir-autogen, elixir-format
msgid "Done"
msgstr "Fertig"
@@ -3351,6 +3353,7 @@ msgid "Local only"
msgstr "Nur lokal"
#: lib/bds/desktop/shell_live/git_handler.ex:113
+#: lib/bds/desktop/shell_live/git_run.ex:82
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Kein aktives Projekt"
@@ -3442,12 +3445,12 @@ msgstr "umbenannt"
msgid "untracked"
msgstr "nicht verfolgt"
-#: lib/bds/desktop/shell_live.ex:742
+#: lib/bds/desktop/shell_live.ex:769
#, elixir-autogen, elixir-format
msgid "Blogmark"
msgstr "Blogmark"
-#: lib/bds/desktop/shell_live.ex:785
+#: lib/bds/desktop/shell_live.ex:812
#, elixir-autogen, elixir-format
msgid "Open a project before importing a blogmark."
msgstr "Öffnen Sie ein Projekt, bevor Sie ein Blogmark importieren."
@@ -3488,7 +3491,7 @@ msgstr "Loeschen"
msgid "Failed to copy bookmarklet to clipboard"
msgstr "Bookmarklet konnte nicht in die Zwischenablage kopiert werden"
-#: lib/bds/desktop/shell_live.ex:754
+#: lib/bds/desktop/shell_live.ex:781
#, elixir-autogen, elixir-format
msgid "The project this blogmark targets does not exist here."
msgstr "Das Projekt, auf das dieses Blogmark verweist, existiert hier nicht."
@@ -3532,3 +3535,34 @@ msgstr "Fenster"
#, elixir-autogen, elixir-format
msgid "Zoom"
msgstr "Zoomen"
+
+#: lib/bds/desktop/shell_live/git_run.ex:91
+#: lib/bds/desktop/shell_live/git_run.ex:127
+#, elixir-autogen, elixir-format
+msgid "Close"
+msgstr "Schließen"
+
+#: lib/bds/desktop/shell_live/git_run.ex:124
+#, elixir-autogen, elixir-format
+msgid "Failed (exit %{status})"
+msgstr "Fehlgeschlagen (Exit %{status})"
+
+#: lib/bds/desktop/shell_live/git_run.ex:122
+#, elixir-autogen, elixir-format
+msgid "Running…"
+msgstr "Läuft…"
+
+#: lib/bds/desktop/shell_live/git_run.ex:111
+#, elixir-autogen, elixir-format
+msgid "git fetch"
+msgstr "git fetch"
+
+#: lib/bds/desktop/shell_live/git_run.ex:112
+#, elixir-autogen, elixir-format
+msgid "git pull"
+msgstr "git pull"
+
+#: lib/bds/desktop/shell_live/git_run.ex:113
+#, elixir-autogen, elixir-format
+msgid "git push"
+msgstr "git push"
diff --git a/priv/gettext/en/LC_MESSAGES/ui.po b/priv/gettext/en/LC_MESSAGES/ui.po
index 301460f..1e04028 100644
--- a/priv/gettext/en/LC_MESSAGES/ui.po
+++ b/priv/gettext/en/LC_MESSAGES/ui.po
@@ -336,7 +336,7 @@ msgstr ""
msgid "Auto"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:424
+#: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
@@ -418,6 +418,7 @@ msgstr ""
msgid "Bookmarklet copy support is wired through the desktop runtime and project public URL."
msgstr ""
+#: lib/bds/desktop/shell_live/git_run.ex:126
#: lib/bds/desktop/shell_live/import_editor.ex:1366
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:298
#: lib/bds/desktop/shell_live/menu_editor.ex:335
@@ -485,7 +486,7 @@ msgstr ""
msgid "Category name is required"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:721
+#: lib/bds/desktop/shell_live.ex:748
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:233
#: lib/bds/desktop/shell_live/chat_editor.ex:323
@@ -1062,7 +1063,7 @@ msgstr ""
msgid "Generate Site"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:722
+#: lib/bds/desktop/shell_live.ex:749
#: lib/bds/desktop/shell_live/socket_state.ex:109
#: lib/bds/ui/sidebar.ex:789
#, elixir-autogen, elixir-format
@@ -1076,7 +1077,7 @@ msgid "Git Diff"
msgstr ""
#: lib/bds/desktop/shell_data.ex:226
-#: lib/bds/desktop/shell_live.ex:718
+#: lib/bds/desktop/shell_live.ex:745
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1199,9 +1200,9 @@ msgstr ""
msgid "Import failed: %{error}"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:574
-#: lib/bds/desktop/shell_live.ex:834
-#: lib/bds/desktop/shell_live.ex:840
+#: lib/bds/desktop/shell_live.ex:592
+#: lib/bds/desktop/shell_live.ex:861
+#: lib/bds/desktop/shell_live.ex:867
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1783,7 +1784,7 @@ msgstr ""
msgid "Other (%{count})"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:717
+#: lib/bds/desktop/shell_live.ex:744
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -2537,7 +2538,7 @@ msgstr ""
msgid "Tags"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:716
+#: lib/bds/desktop/shell_live.ex:743
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -3217,12 +3218,12 @@ msgstr ""
msgid "Comparing database and filesystem metadata"
msgstr "Comparing database and filesystem metadata"
-#: lib/bds/desktop/shell_live.ex:650
+#: lib/bds/desktop/shell_live.ex:668
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "Added %{count} images to post"
-#: lib/bds/desktop/shell_live.ex:618
+#: lib/bds/desktop/shell_live.ex:636
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "Added %{title}"
@@ -3242,18 +3243,18 @@ msgstr ""
msgid "Image Import Concurrency"
msgstr "Image Import Concurrency"
-#: lib/bds/desktop/shell_live.ex:423
-#: lib/bds/desktop/shell_live.ex:436
-#: lib/bds/desktop/shell_live.ex:617
-#: lib/bds/desktop/shell_live.ex:649
-#: lib/bds/desktop/shell_live.ex:660
-#: lib/bds/desktop/shell_live.ex:671
+#: lib/bds/desktop/shell_live.ex:441
+#: lib/bds/desktop/shell_live.ex:454
+#: lib/bds/desktop/shell_live.ex:635
+#: lib/bds/desktop/shell_live.ex:667
+#: lib/bds/desktop/shell_live.ex:678
+#: lib/bds/desktop/shell_live.ex:689
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Add Gallery Images"
-#: lib/bds/desktop/shell_live.ex:672
+#: lib/bds/desktop/shell_live.ex:690
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "Failed to process %{path}: %{reason}"
@@ -3322,6 +3323,7 @@ msgid "Commit message is required"
msgstr ""
#: lib/bds/desktop/shell_live/git_handler.ex:106
+#: lib/bds/desktop/shell_live/git_run.ex:123
#, elixir-autogen, elixir-format, fuzzy
msgid "Done"
msgstr ""
@@ -3351,6 +3353,7 @@ msgid "Local only"
msgstr ""
#: lib/bds/desktop/shell_live/git_handler.ex:113
+#: lib/bds/desktop/shell_live/git_run.ex:82
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr ""
@@ -3442,12 +3445,12 @@ msgstr ""
msgid "untracked"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:742
+#: lib/bds/desktop/shell_live.ex:769
#, elixir-autogen, elixir-format, fuzzy
msgid "Blogmark"
msgstr "Blogmark"
-#: lib/bds/desktop/shell_live.ex:785
+#: lib/bds/desktop/shell_live.ex:812
#, elixir-autogen, elixir-format
msgid "Open a project before importing a blogmark."
msgstr ""
@@ -3488,7 +3491,7 @@ msgstr ""
msgid "Failed to copy bookmarklet to clipboard"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:754
+#: lib/bds/desktop/shell_live.ex:781
#, elixir-autogen, elixir-format
msgid "The project this blogmark targets does not exist here."
msgstr ""
@@ -3532,3 +3535,34 @@ msgstr ""
#, elixir-autogen, elixir-format, fuzzy
msgid "Zoom"
msgstr ""
+
+#: lib/bds/desktop/shell_live/git_run.ex:91
+#: lib/bds/desktop/shell_live/git_run.ex:127
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Close"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/git_run.ex:124
+#, elixir-autogen, elixir-format
+msgid "Failed (exit %{status})"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/git_run.ex:122
+#, elixir-autogen, elixir-format
+msgid "Running…"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/git_run.ex:111
+#, elixir-autogen, elixir-format
+msgid "git fetch"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/git_run.ex:112
+#, elixir-autogen, elixir-format
+msgid "git pull"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/git_run.ex:113
+#, elixir-autogen, elixir-format
+msgid "git push"
+msgstr ""
diff --git a/priv/gettext/es/LC_MESSAGES/ui.po b/priv/gettext/es/LC_MESSAGES/ui.po
index 68f0abf..6dc2591 100644
--- a/priv/gettext/es/LC_MESSAGES/ui.po
+++ b/priv/gettext/es/LC_MESSAGES/ui.po
@@ -336,7 +336,7 @@ msgstr "Autor"
msgid "Auto"
msgstr "Automático"
-#: lib/bds/desktop/shell_live.ex:424
+#: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
@@ -418,6 +418,7 @@ msgstr "Categoría de blogmark"
msgid "Bookmarklet copy support is wired through the desktop runtime and project public URL."
msgstr "La copia del bookmarklet está conectada mediante el entorno de escritorio y la URL pública del proyecto."
+#: lib/bds/desktop/shell_live/git_run.ex:126
#: lib/bds/desktop/shell_live/import_editor.ex:1366
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:298
#: lib/bds/desktop/shell_live/menu_editor.ex:335
@@ -485,7 +486,7 @@ msgstr "Valores predeterminados de categoría, opciones de renderizado y conexi
msgid "Category name is required"
msgstr "El nombre de la categoría es obligatorio"
-#: lib/bds/desktop/shell_live.ex:721
+#: lib/bds/desktop/shell_live.ex:748
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:233
#: lib/bds/desktop/shell_live/chat_editor.ex:323
@@ -1062,7 +1063,7 @@ msgstr "Galeria"
msgid "Generate Site"
msgstr "Generar sitio"
-#: lib/bds/desktop/shell_live.ex:722
+#: lib/bds/desktop/shell_live.ex:749
#: lib/bds/desktop/shell_live/socket_state.ex:109
#: lib/bds/ui/sidebar.ex:789
#, elixir-autogen, elixir-format
@@ -1076,7 +1077,7 @@ msgid "Git Diff"
msgstr "Diff de Git"
#: lib/bds/desktop/shell_data.ex:226
-#: lib/bds/desktop/shell_live.ex:718
+#: lib/bds/desktop/shell_live.ex:745
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1199,9 +1200,9 @@ msgstr "Definiciones de importación"
msgid "Import failed: %{error}"
msgstr "La importación falló: %{error}"
-#: lib/bds/desktop/shell_live.ex:574
-#: lib/bds/desktop/shell_live.ex:834
-#: lib/bds/desktop/shell_live.ex:840
+#: lib/bds/desktop/shell_live.ex:592
+#: lib/bds/desktop/shell_live.ex:861
+#: lib/bds/desktop/shell_live.ex:867
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1783,7 +1784,7 @@ msgstr "Otros"
msgid "Other (%{count})"
msgstr "Otros (%{count})"
-#: lib/bds/desktop/shell_live.ex:717
+#: lib/bds/desktop/shell_live.ex:744
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -2537,7 +2538,7 @@ msgstr "Nombre de la etiqueta"
msgid "Tags"
msgstr "Etiquetas"
-#: lib/bds/desktop/shell_live.ex:716
+#: lib/bds/desktop/shell_live.ex:743
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -3217,12 +3218,12 @@ msgstr "Bienvenido al asistente de IA"
msgid "Comparing database and filesystem metadata"
msgstr "Comparando metadatos de la base de datos y del sistema de archivos"
-#: lib/bds/desktop/shell_live.ex:650
+#: lib/bds/desktop/shell_live.ex:668
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "%{count} imágenes añadidas a la publicación"
-#: lib/bds/desktop/shell_live.ex:618
+#: lib/bds/desktop/shell_live.ex:636
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "%{title} añadido"
@@ -3242,18 +3243,18 @@ msgstr "Guía del usuario para flujos editoriales, medios, plantillas, traducci
msgid "Image Import Concurrency"
msgstr "Importación simultánea de imágenes"
-#: lib/bds/desktop/shell_live.ex:423
-#: lib/bds/desktop/shell_live.ex:436
-#: lib/bds/desktop/shell_live.ex:617
-#: lib/bds/desktop/shell_live.ex:649
-#: lib/bds/desktop/shell_live.ex:660
-#: lib/bds/desktop/shell_live.ex:671
+#: lib/bds/desktop/shell_live.ex:441
+#: lib/bds/desktop/shell_live.ex:454
+#: lib/bds/desktop/shell_live.ex:635
+#: lib/bds/desktop/shell_live.ex:667
+#: lib/bds/desktop/shell_live.ex:678
+#: lib/bds/desktop/shell_live.ex:689
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Añadir imágenes a la galería"
-#: lib/bds/desktop/shell_live.ex:672
+#: lib/bds/desktop/shell_live.ex:690
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "No se pudo procesar %{path}: %{reason}"
@@ -3322,6 +3323,7 @@ msgid "Commit message is required"
msgstr "Se requiere un mensaje de commit"
#: lib/bds/desktop/shell_live/git_handler.ex:106
+#: lib/bds/desktop/shell_live/git_run.ex:123
#, elixir-autogen, elixir-format
msgid "Done"
msgstr "Listo"
@@ -3351,6 +3353,7 @@ msgid "Local only"
msgstr "Solo local"
#: lib/bds/desktop/shell_live/git_handler.ex:113
+#: lib/bds/desktop/shell_live/git_run.ex:82
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Sin proyecto activo"
@@ -3442,12 +3445,12 @@ msgstr "renombrado"
msgid "untracked"
msgstr "sin seguimiento"
-#: lib/bds/desktop/shell_live.ex:742
+#: lib/bds/desktop/shell_live.ex:769
#, elixir-autogen, elixir-format
msgid "Blogmark"
msgstr "Blogmark"
-#: lib/bds/desktop/shell_live.ex:785
+#: lib/bds/desktop/shell_live.ex:812
#, elixir-autogen, elixir-format
msgid "Open a project before importing a blogmark."
msgstr "Abre un proyecto antes de importar un blogmark."
@@ -3488,7 +3491,7 @@ msgstr "Eliminar"
msgid "Failed to copy bookmarklet to clipboard"
msgstr "Error al copiar el bookmarklet al portapapeles"
-#: lib/bds/desktop/shell_live.ex:754
+#: lib/bds/desktop/shell_live.ex:781
#, elixir-autogen, elixir-format
msgid "The project this blogmark targets does not exist here."
msgstr "El proyecto al que apunta este blogmark no existe aquí."
@@ -3532,3 +3535,34 @@ msgstr "Ventana"
#, elixir-autogen, elixir-format
msgid "Zoom"
msgstr "Ampliar/reducir"
+
+#: lib/bds/desktop/shell_live/git_run.ex:91
+#: lib/bds/desktop/shell_live/git_run.ex:127
+#, elixir-autogen, elixir-format
+msgid "Close"
+msgstr "Cerrar"
+
+#: lib/bds/desktop/shell_live/git_run.ex:124
+#, elixir-autogen, elixir-format
+msgid "Failed (exit %{status})"
+msgstr "Error (salida %{status})"
+
+#: lib/bds/desktop/shell_live/git_run.ex:122
+#, elixir-autogen, elixir-format
+msgid "Running…"
+msgstr "En curso…"
+
+#: lib/bds/desktop/shell_live/git_run.ex:111
+#, elixir-autogen, elixir-format
+msgid "git fetch"
+msgstr "git fetch"
+
+#: lib/bds/desktop/shell_live/git_run.ex:112
+#, elixir-autogen, elixir-format
+msgid "git pull"
+msgstr "git pull"
+
+#: lib/bds/desktop/shell_live/git_run.ex:113
+#, elixir-autogen, elixir-format
+msgid "git push"
+msgstr "git push"
diff --git a/priv/gettext/fr/LC_MESSAGES/ui.po b/priv/gettext/fr/LC_MESSAGES/ui.po
index bc727c4..91a2f7e 100644
--- a/priv/gettext/fr/LC_MESSAGES/ui.po
+++ b/priv/gettext/fr/LC_MESSAGES/ui.po
@@ -336,7 +336,7 @@ msgstr "Auteur"
msgid "Auto"
msgstr "Automatique"
-#: lib/bds/desktop/shell_live.ex:424
+#: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
@@ -418,6 +418,7 @@ msgstr "Catégorie de blogmark"
msgid "Bookmarklet copy support is wired through the desktop runtime and project public URL."
msgstr "La copie du bookmarklet est reliée via l’environnement desktop et l’URL publique du projet."
+#: lib/bds/desktop/shell_live/git_run.ex:126
#: lib/bds/desktop/shell_live/import_editor.ex:1366
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:298
#: lib/bds/desktop/shell_live/menu_editor.ex:335
@@ -485,7 +486,7 @@ msgstr "Valeurs par défaut des catégories, options de rendu et liaison des mod
msgid "Category name is required"
msgstr "Le nom de la catégorie est requis"
-#: lib/bds/desktop/shell_live.ex:721
+#: lib/bds/desktop/shell_live.ex:748
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:233
#: lib/bds/desktop/shell_live/chat_editor.ex:323
@@ -1062,7 +1063,7 @@ msgstr "Galerie"
msgid "Generate Site"
msgstr "Générer le site"
-#: lib/bds/desktop/shell_live.ex:722
+#: lib/bds/desktop/shell_live.ex:749
#: lib/bds/desktop/shell_live/socket_state.ex:109
#: lib/bds/ui/sidebar.ex:789
#, elixir-autogen, elixir-format
@@ -1076,7 +1077,7 @@ msgid "Git Diff"
msgstr "Diff Git"
#: lib/bds/desktop/shell_data.ex:226
-#: lib/bds/desktop/shell_live.ex:718
+#: lib/bds/desktop/shell_live.ex:745
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1199,9 +1200,9 @@ msgstr "Définitions d’import"
msgid "Import failed: %{error}"
msgstr "Échec de l’import : %{error}"
-#: lib/bds/desktop/shell_live.ex:574
-#: lib/bds/desktop/shell_live.ex:834
-#: lib/bds/desktop/shell_live.ex:840
+#: lib/bds/desktop/shell_live.ex:592
+#: lib/bds/desktop/shell_live.ex:861
+#: lib/bds/desktop/shell_live.ex:867
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1783,7 +1784,7 @@ msgstr "Autre"
msgid "Other (%{count})"
msgstr "Autres (%{count})"
-#: lib/bds/desktop/shell_live.ex:717
+#: lib/bds/desktop/shell_live.ex:744
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -2537,7 +2538,7 @@ msgstr "Nom du mot-clé"
msgid "Tags"
msgstr "Tags"
-#: lib/bds/desktop/shell_live.ex:716
+#: lib/bds/desktop/shell_live.ex:743
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -3217,12 +3218,12 @@ msgstr "Bienvenue dans l’assistant IA"
msgid "Comparing database and filesystem metadata"
msgstr "Comparaison des métadonnées entre la base et le système de fichiers"
-#: lib/bds/desktop/shell_live.ex:650
+#: lib/bds/desktop/shell_live.ex:668
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "%{count} images ajoutées à l'article"
-#: lib/bds/desktop/shell_live.ex:618
+#: lib/bds/desktop/shell_live.ex:636
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "%{title} ajouté"
@@ -3242,18 +3243,18 @@ msgstr "Guide utilisateur pour les flux éditoriaux, médias, modèles, traducti
msgid "Image Import Concurrency"
msgstr "Importation simultanée d'images"
-#: lib/bds/desktop/shell_live.ex:423
-#: lib/bds/desktop/shell_live.ex:436
-#: lib/bds/desktop/shell_live.ex:617
-#: lib/bds/desktop/shell_live.ex:649
-#: lib/bds/desktop/shell_live.ex:660
-#: lib/bds/desktop/shell_live.ex:671
+#: lib/bds/desktop/shell_live.ex:441
+#: lib/bds/desktop/shell_live.ex:454
+#: lib/bds/desktop/shell_live.ex:635
+#: lib/bds/desktop/shell_live.ex:667
+#: lib/bds/desktop/shell_live.ex:678
+#: lib/bds/desktop/shell_live.ex:689
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Ajouter des images à la galerie"
-#: lib/bds/desktop/shell_live.ex:672
+#: lib/bds/desktop/shell_live.ex:690
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "Impossible de traiter %{path} : %{reason}"
@@ -3322,6 +3323,7 @@ msgid "Commit message is required"
msgstr "Le message de commit est requis"
#: lib/bds/desktop/shell_live/git_handler.ex:106
+#: lib/bds/desktop/shell_live/git_run.ex:123
#, elixir-autogen, elixir-format
msgid "Done"
msgstr "Terminé"
@@ -3351,6 +3353,7 @@ msgid "Local only"
msgstr "Local uniquement"
#: lib/bds/desktop/shell_live/git_handler.ex:113
+#: lib/bds/desktop/shell_live/git_run.ex:82
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Aucun projet actif"
@@ -3442,12 +3445,12 @@ msgstr "renommé"
msgid "untracked"
msgstr "non suivi"
-#: lib/bds/desktop/shell_live.ex:742
+#: lib/bds/desktop/shell_live.ex:769
#, elixir-autogen, elixir-format
msgid "Blogmark"
msgstr "Blogmark"
-#: lib/bds/desktop/shell_live.ex:785
+#: lib/bds/desktop/shell_live.ex:812
#, elixir-autogen, elixir-format
msgid "Open a project before importing a blogmark."
msgstr "Ouvrez un projet avant d’importer un blogmark."
@@ -3488,7 +3491,7 @@ msgstr "Supprimer"
msgid "Failed to copy bookmarklet to clipboard"
msgstr "Échec de la copie du bookmarklet dans le presse-papiers"
-#: lib/bds/desktop/shell_live.ex:754
+#: lib/bds/desktop/shell_live.ex:781
#, elixir-autogen, elixir-format
msgid "The project this blogmark targets does not exist here."
msgstr "Le projet ciblé par ce blogmark n'existe pas ici."
@@ -3532,3 +3535,34 @@ msgstr "Fenêtre"
#, elixir-autogen, elixir-format
msgid "Zoom"
msgstr "Réduire/agrandir"
+
+#: lib/bds/desktop/shell_live/git_run.ex:91
+#: lib/bds/desktop/shell_live/git_run.ex:127
+#, elixir-autogen, elixir-format
+msgid "Close"
+msgstr "Fermer"
+
+#: lib/bds/desktop/shell_live/git_run.ex:124
+#, elixir-autogen, elixir-format
+msgid "Failed (exit %{status})"
+msgstr "Échec (code %{status})"
+
+#: lib/bds/desktop/shell_live/git_run.ex:122
+#, elixir-autogen, elixir-format
+msgid "Running…"
+msgstr "En cours…"
+
+#: lib/bds/desktop/shell_live/git_run.ex:111
+#, elixir-autogen, elixir-format
+msgid "git fetch"
+msgstr "git fetch"
+
+#: lib/bds/desktop/shell_live/git_run.ex:112
+#, elixir-autogen, elixir-format
+msgid "git pull"
+msgstr "git pull"
+
+#: lib/bds/desktop/shell_live/git_run.ex:113
+#, elixir-autogen, elixir-format
+msgid "git push"
+msgstr "git push"
diff --git a/priv/gettext/it/LC_MESSAGES/ui.po b/priv/gettext/it/LC_MESSAGES/ui.po
index 8e2f010..908bcbb 100644
--- a/priv/gettext/it/LC_MESSAGES/ui.po
+++ b/priv/gettext/it/LC_MESSAGES/ui.po
@@ -336,7 +336,7 @@ msgstr "Autore"
msgid "Auto"
msgstr "Automatico"
-#: lib/bds/desktop/shell_live.ex:424
+#: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
@@ -418,6 +418,7 @@ msgstr "Categoria blogmark"
msgid "Bookmarklet copy support is wired through the desktop runtime and project public URL."
msgstr "La copia del bookmarklet è collegata tramite il runtime desktop e l’URL pubblica del progetto."
+#: lib/bds/desktop/shell_live/git_run.ex:126
#: lib/bds/desktop/shell_live/import_editor.ex:1366
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:298
#: lib/bds/desktop/shell_live/menu_editor.ex:335
@@ -485,7 +486,7 @@ msgstr "Valori predefiniti delle categorie, opzioni di rendering e collegamento
msgid "Category name is required"
msgstr "Il nome della categoria è obbligatorio"
-#: lib/bds/desktop/shell_live.ex:721
+#: lib/bds/desktop/shell_live.ex:748
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:233
#: lib/bds/desktop/shell_live/chat_editor.ex:323
@@ -1062,7 +1063,7 @@ msgstr "Galleria"
msgid "Generate Site"
msgstr "Genera sito"
-#: lib/bds/desktop/shell_live.ex:722
+#: lib/bds/desktop/shell_live.ex:749
#: lib/bds/desktop/shell_live/socket_state.ex:109
#: lib/bds/ui/sidebar.ex:789
#, elixir-autogen, elixir-format
@@ -1076,7 +1077,7 @@ msgid "Git Diff"
msgstr "Diff Git"
#: lib/bds/desktop/shell_data.ex:226
-#: lib/bds/desktop/shell_live.ex:718
+#: lib/bds/desktop/shell_live.ex:745
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1199,9 +1200,9 @@ msgstr "Definizioni di importazione"
msgid "Import failed: %{error}"
msgstr "Importazione non riuscita: %{error}"
-#: lib/bds/desktop/shell_live.ex:574
-#: lib/bds/desktop/shell_live.ex:834
-#: lib/bds/desktop/shell_live.ex:840
+#: lib/bds/desktop/shell_live.ex:592
+#: lib/bds/desktop/shell_live.ex:861
+#: lib/bds/desktop/shell_live.ex:867
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1783,7 +1784,7 @@ msgstr "Altro"
msgid "Other (%{count})"
msgstr "Altro (%{count})"
-#: lib/bds/desktop/shell_live.ex:717
+#: lib/bds/desktop/shell_live.ex:744
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -2537,7 +2538,7 @@ msgstr "Nome del tag"
msgid "Tags"
msgstr "Tag"
-#: lib/bds/desktop/shell_live.ex:716
+#: lib/bds/desktop/shell_live.ex:743
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -3217,12 +3218,12 @@ msgstr "Benvenuto nell’assistente IA"
msgid "Comparing database and filesystem metadata"
msgstr "Confronto tra i metadati del database e del filesystem"
-#: lib/bds/desktop/shell_live.ex:650
+#: lib/bds/desktop/shell_live.ex:668
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "%{count} immagini aggiunte al post"
-#: lib/bds/desktop/shell_live.ex:618
+#: lib/bds/desktop/shell_live.ex:636
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "%{title} aggiunto"
@@ -3242,18 +3243,18 @@ msgstr "Guida per l'utente finale per flussi editoriali, media, modelli, traduzi
msgid "Image Import Concurrency"
msgstr "Importazione simultanea immagini"
-#: lib/bds/desktop/shell_live.ex:423
-#: lib/bds/desktop/shell_live.ex:436
-#: lib/bds/desktop/shell_live.ex:617
-#: lib/bds/desktop/shell_live.ex:649
-#: lib/bds/desktop/shell_live.ex:660
-#: lib/bds/desktop/shell_live.ex:671
+#: lib/bds/desktop/shell_live.ex:441
+#: lib/bds/desktop/shell_live.ex:454
+#: lib/bds/desktop/shell_live.ex:635
+#: lib/bds/desktop/shell_live.ex:667
+#: lib/bds/desktop/shell_live.ex:678
+#: lib/bds/desktop/shell_live.ex:689
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Aggiungi immagini alla galleria"
-#: lib/bds/desktop/shell_live.ex:672
+#: lib/bds/desktop/shell_live.ex:690
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "Impossibile elaborare %{path}: %{reason}"
@@ -3322,6 +3323,7 @@ msgid "Commit message is required"
msgstr "Il messaggio di commit è obbligatorio"
#: lib/bds/desktop/shell_live/git_handler.ex:106
+#: lib/bds/desktop/shell_live/git_run.ex:123
#, elixir-autogen, elixir-format
msgid "Done"
msgstr "Fatto"
@@ -3351,6 +3353,7 @@ msgid "Local only"
msgstr "Solo locale"
#: lib/bds/desktop/shell_live/git_handler.ex:113
+#: lib/bds/desktop/shell_live/git_run.ex:82
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Nessun progetto attivo"
@@ -3442,12 +3445,12 @@ msgstr "rinominato"
msgid "untracked"
msgstr "non tracciato"
-#: lib/bds/desktop/shell_live.ex:742
+#: lib/bds/desktop/shell_live.ex:769
#, elixir-autogen, elixir-format
msgid "Blogmark"
msgstr "Blogmark"
-#: lib/bds/desktop/shell_live.ex:785
+#: lib/bds/desktop/shell_live.ex:812
#, elixir-autogen, elixir-format
msgid "Open a project before importing a blogmark."
msgstr "Apri un progetto prima di importare un blogmark."
@@ -3488,7 +3491,7 @@ msgstr "Elimina"
msgid "Failed to copy bookmarklet to clipboard"
msgstr "Impossibile copiare il bookmarklet negli appunti"
-#: lib/bds/desktop/shell_live.ex:754
+#: lib/bds/desktop/shell_live.ex:781
#, elixir-autogen, elixir-format
msgid "The project this blogmark targets does not exist here."
msgstr "Il progetto a cui punta questo blogmark non esiste qui."
@@ -3532,3 +3535,34 @@ msgstr "Finestra"
#, elixir-autogen, elixir-format
msgid "Zoom"
msgstr "Riduci/ingrandisci"
+
+#: lib/bds/desktop/shell_live/git_run.ex:91
+#: lib/bds/desktop/shell_live/git_run.ex:127
+#, elixir-autogen, elixir-format
+msgid "Close"
+msgstr "Chiudi"
+
+#: lib/bds/desktop/shell_live/git_run.ex:124
+#, elixir-autogen, elixir-format
+msgid "Failed (exit %{status})"
+msgstr "Non riuscito (uscita %{status})"
+
+#: lib/bds/desktop/shell_live/git_run.ex:122
+#, elixir-autogen, elixir-format
+msgid "Running…"
+msgstr "In corso…"
+
+#: lib/bds/desktop/shell_live/git_run.ex:111
+#, elixir-autogen, elixir-format
+msgid "git fetch"
+msgstr "git fetch"
+
+#: lib/bds/desktop/shell_live/git_run.ex:112
+#, elixir-autogen, elixir-format
+msgid "git pull"
+msgstr "git pull"
+
+#: lib/bds/desktop/shell_live/git_run.ex:113
+#, elixir-autogen, elixir-format
+msgid "git push"
+msgstr "git push"
diff --git a/priv/gettext/ui.pot b/priv/gettext/ui.pot
index fcba0c9..32a63c2 100644
--- a/priv/gettext/ui.pot
+++ b/priv/gettext/ui.pot
@@ -349,7 +349,7 @@ msgstr ""
msgid "Auto"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:424
+#: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
@@ -431,6 +431,7 @@ msgstr ""
msgid "Bookmarklet copy support is wired through the desktop runtime and project public URL."
msgstr ""
+#: lib/bds/desktop/shell_live/git_run.ex:126
#: lib/bds/desktop/shell_live/import_editor.ex:1366
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:298
#: lib/bds/desktop/shell_live/menu_editor.ex:335
@@ -498,7 +499,7 @@ msgstr ""
msgid "Category name is required"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:721
+#: lib/bds/desktop/shell_live.ex:748
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:233
#: lib/bds/desktop/shell_live/chat_editor.ex:323
@@ -1075,7 +1076,7 @@ msgstr ""
msgid "Generate Site"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:722
+#: lib/bds/desktop/shell_live.ex:749
#: lib/bds/desktop/shell_live/socket_state.ex:109
#: lib/bds/ui/sidebar.ex:789
#, elixir-autogen, elixir-format
@@ -1089,7 +1090,7 @@ msgid "Git Diff"
msgstr ""
#: lib/bds/desktop/shell_data.ex:226
-#: lib/bds/desktop/shell_live.ex:718
+#: lib/bds/desktop/shell_live.ex:745
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1212,9 +1213,9 @@ msgstr ""
msgid "Import failed: %{error}"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:574
-#: lib/bds/desktop/shell_live.ex:834
-#: lib/bds/desktop/shell_live.ex:840
+#: lib/bds/desktop/shell_live.ex:592
+#: lib/bds/desktop/shell_live.ex:861
+#: lib/bds/desktop/shell_live.ex:867
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1796,7 +1797,7 @@ msgstr ""
msgid "Other (%{count})"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:717
+#: lib/bds/desktop/shell_live.ex:744
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -2550,7 +2551,7 @@ msgstr ""
msgid "Tags"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:716
+#: lib/bds/desktop/shell_live.ex:743
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -3230,12 +3231,12 @@ msgstr ""
msgid "Comparing database and filesystem metadata"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:650
+#: lib/bds/desktop/shell_live.ex:668
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:618
+#: lib/bds/desktop/shell_live.ex:636
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr ""
@@ -3255,18 +3256,18 @@ msgstr ""
msgid "Image Import Concurrency"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:423
-#: lib/bds/desktop/shell_live.ex:436
-#: lib/bds/desktop/shell_live.ex:617
-#: lib/bds/desktop/shell_live.ex:649
-#: lib/bds/desktop/shell_live.ex:660
-#: lib/bds/desktop/shell_live.ex:671
+#: lib/bds/desktop/shell_live.ex:441
+#: lib/bds/desktop/shell_live.ex:454
+#: lib/bds/desktop/shell_live.ex:635
+#: lib/bds/desktop/shell_live.ex:667
+#: lib/bds/desktop/shell_live.ex:678
+#: lib/bds/desktop/shell_live.ex:689
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:672
+#: lib/bds/desktop/shell_live.ex:690
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr ""
@@ -3335,6 +3336,7 @@ msgid "Commit message is required"
msgstr ""
#: lib/bds/desktop/shell_live/git_handler.ex:106
+#: lib/bds/desktop/shell_live/git_run.ex:123
#, elixir-autogen, elixir-format
msgid "Done"
msgstr ""
@@ -3364,6 +3366,7 @@ msgid "Local only"
msgstr ""
#: lib/bds/desktop/shell_live/git_handler.ex:113
+#: lib/bds/desktop/shell_live/git_run.ex:82
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr ""
@@ -3455,12 +3458,12 @@ msgstr ""
msgid "untracked"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:742
+#: lib/bds/desktop/shell_live.ex:769
#, elixir-autogen, elixir-format
msgid "Blogmark"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:785
+#: lib/bds/desktop/shell_live.ex:812
#, elixir-autogen, elixir-format
msgid "Open a project before importing a blogmark."
msgstr ""
@@ -3501,7 +3504,7 @@ msgstr ""
msgid "Failed to copy bookmarklet to clipboard"
msgstr ""
-#: lib/bds/desktop/shell_live.ex:754
+#: lib/bds/desktop/shell_live.ex:781
#, elixir-autogen, elixir-format
msgid "The project this blogmark targets does not exist here."
msgstr ""
@@ -3545,3 +3548,34 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Zoom"
msgstr ""
+
+#: lib/bds/desktop/shell_live/git_run.ex:91
+#: lib/bds/desktop/shell_live/git_run.ex:127
+#, elixir-autogen, elixir-format
+msgid "Close"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/git_run.ex:124
+#, elixir-autogen, elixir-format
+msgid "Failed (exit %{status})"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/git_run.ex:122
+#, elixir-autogen, elixir-format
+msgid "Running…"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/git_run.ex:111
+#, elixir-autogen, elixir-format
+msgid "git fetch"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/git_run.ex:112
+#, elixir-autogen, elixir-format
+msgid "git pull"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/git_run.ex:113
+#, elixir-autogen, elixir-format
+msgid "git push"
+msgstr ""
diff --git a/priv/static/assets/app.css b/priv/static/assets/app.css
index c4df386..9356357 100644
--- a/priv/static/assets/app.css
+++ b/priv/static/assets/app.css
@@ -4468,7 +4468,7 @@ button svg, button svg * {
position: relative;
margin-bottom: 14px;
}
-.ai-suggestions-modal-backdrop, .insert-modal-backdrop, .language-picker-modal-backdrop, .confirm-delete-modal-backdrop, .confirm-dialog-overlay, .gallery-overlay, .lightbox-overlay {
+.ai-suggestions-modal-backdrop, .insert-modal-backdrop, .language-picker-modal-backdrop, .confirm-delete-modal-backdrop, .confirm-dialog-overlay, .git-run-backdrop, .gallery-overlay, .lightbox-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.68);
@@ -4477,7 +4477,7 @@ button svg, button svg * {
justify-content: center;
pointer-events: auto;
}
-.ai-suggestions-modal, .insert-modal, .language-picker-modal, .confirm-delete-modal, .confirm-dialog, .gallery-overlay-content {
+.ai-suggestions-modal, .insert-modal, .language-picker-modal, .confirm-delete-modal, .confirm-dialog, .git-run-modal, .gallery-overlay-content {
position: relative;
z-index: 1;
background: #1e1e1e;
@@ -4485,6 +4485,65 @@ button svg, button svg * {
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
+.git-run-modal {
+ width: min(760px, calc(100vw - 32px));
+ max-height: calc(100vh - 48px);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+.git-run-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 12px 16px;
+ border-bottom: 1px solid #3c3c3c;
+}
+.git-run-title {
+ margin: 0;
+ font-size: 14px;
+ font-family: var(--font-mono, monospace);
+}
+.git-run-status {
+ font-size: 12px;
+ font-weight: 600;
+}
+.git-run-status.running {
+ color: #d7ba7d;
+}
+.git-run-status.ok {
+ color: #4ec9b0;
+}
+.git-run-status.fail {
+ color: #f48771;
+}
+.git-run-output {
+ margin: 0;
+ padding: 12px 16px;
+ overflow: auto;
+ flex: 1;
+ min-height: 160px;
+ max-height: 60vh;
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-family: var(--font-mono, monospace);
+ font-size: 12px;
+ line-height: 1.5;
+ background: #141414;
+}
+.git-run-stdout {
+ color: #d4d4d4;
+}
+.git-run-stderr {
+ color: #f48771;
+}
+.git-run-footer {
+ display: flex;
+ justify-content: flex-end;
+ padding: 12px 16px;
+ border-top: 1px solid #3c3c3c;
+}
.ai-suggestions-modal, .language-picker-modal, .confirm-delete-modal, .confirm-dialog {
width: min(680px, calc(100vw - 32px));
max-height: calc(100vh - 48px);
diff --git a/test/bds/desktop/shell_live/git_run_test.exs b/test/bds/desktop/shell_live/git_run_test.exs
new file mode 100644
index 0000000..d7cce28
--- /dev/null
+++ b/test/bds/desktop/shell_live/git_run_test.exs
@@ -0,0 +1,92 @@
+defmodule BDS.Desktop.ShellLive.GitRunTest do
+ use ExUnit.Case, async: true
+
+ import Phoenix.LiveViewTest
+
+ alias BDS.Desktop.ShellLive.GitRun
+ alias Phoenix.Component
+
+ defp socket(git_run) do
+ %Phoenix.LiveView.Socket{}
+ |> Component.assign(:git_run, git_run)
+ end
+
+ test "network_event? recognises fetch/pull/push but not prune" do
+ assert GitRun.network_event?("git_push")
+ assert GitRun.network_event?("git_pull")
+ assert GitRun.network_event?("git_fetch")
+ refute GitRun.network_event?("git_prune_lfs")
+ end
+
+ test "start with no project shows an error and a failed status" do
+ result = GitRun.start(socket(nil), "git_push", nil)
+ run = result.assigns.git_run
+
+ assert run.operation == :push
+ assert run.status == {:done, 1}
+ assert [{:stderr, text}] = run.lines
+ assert text =~ "No active project"
+ end
+
+ test "append accumulates chunks for the active ref only" do
+ ref = make_ref()
+ run = %{operation: :push, pid: nil, ref: ref, lines: [], status: :running}
+
+ socket =
+ socket(run)
+ |> GitRun.append(ref, :stdout, "a")
+ |> GitRun.append(ref, :stderr, "b")
+ |> GitRun.append(make_ref(), :stdout, "ignored")
+
+ assert socket.assigns.git_run.lines == [{:stderr, "b"}, {:stdout, "a"}]
+ end
+
+ test "done records the exit status and clears the pid" do
+ ref = make_ref()
+ run = %{operation: :push, pid: self(), ref: ref, lines: [], status: :running}
+
+ socket = GitRun.done(socket(run), ref, 128)
+
+ assert socket.assigns.git_run.status == {:done, 128}
+ assert socket.assigns.git_run.pid == nil
+ end
+
+ test "renders nothing when there is no active run" do
+ assert render_component(&GitRun.git_run_modal/1, git_run: nil) |> String.trim() == ""
+ end
+
+ test "renders the failed run with stderr coloured and a fail status" do
+ run = %{
+ operation: :push,
+ pid: nil,
+ ref: make_ref(),
+ lines: [{:stderr, "rejected"}, {:stdout, "counting"}],
+ status: {:done, 1}
+ }
+
+ html = render_component(&GitRun.git_run_modal/1, git_run: run)
+
+ assert html =~ "git push"
+ assert html =~ "git-run-stderr"
+ assert html =~ "rejected"
+ assert html =~ "git-run-status fail"
+ end
+
+ test "close cancels a running command and clears the modal" do
+ target = self()
+
+ pid =
+ spawn(fn ->
+ receive do
+ :cancel -> send(target, :cancelled)
+ end
+ end)
+
+ run = %{operation: :push, pid: pid, ref: make_ref(), lines: [], status: :running}
+
+ socket = GitRun.close(socket(run))
+
+ assert socket.assigns.git_run == nil
+ assert_receive :cancelled, 1_000
+ end
+end
diff --git a/test/bds/git/stream_test.exs b/test/bds/git/stream_test.exs
new file mode 100644
index 0000000..5ce08ae
--- /dev/null
+++ b/test/bds/git/stream_test.exs
@@ -0,0 +1,57 @@
+defmodule BDS.Git.StreamTest do
+ use ExUnit.Case, async: true
+
+ alias BDS.Git.Stream
+
+ setup do
+ dir = Path.join(System.tmp_dir!(), "bds-git-stream-#{System.unique_integer([:positive])}")
+ File.mkdir_p!(dir)
+ on_exit(fn -> File.rm_rf(dir) end)
+ %{dir: dir}
+ end
+
+ defp write_script(dir, name, body) do
+ path = Path.join(dir, name)
+ File.write!(path, "#!/bin/sh\n" <> body)
+ File.chmod!(path, 0o755)
+ path
+ end
+
+ test "streams stdout and stderr separately and reports the exit status", %{dir: dir} do
+ script =
+ write_script(
+ dir,
+ "emit",
+ ~s(printf 'OUT:%s\\n' "$1"; printf 'ERR:%s\\n' "$1" 1>&2\nexit 3\n)
+ )
+
+ {:ok, _pid, ref} = Stream.start(self(), dir, ["hello"], command: script)
+
+ assert_receive {:git_output, ^ref, :stdout, out}, 2_000
+ assert out =~ "OUT:hello"
+
+ assert_receive {:git_output, ^ref, :stderr, err}, 2_000
+ assert err =~ "ERR:hello"
+
+ assert_receive {:git_done, ^ref, 3}, 2_000
+ end
+
+ test "cancel kills a hanging process and still reports done", %{dir: dir} do
+ script = write_script(dir, "hang", "exec sleep 30\n")
+
+ {:ok, pid, ref} = Stream.start(self(), dir, [], command: script)
+ Stream.cancel(pid)
+
+ assert_receive {:git_done, ^ref, status}, 2_000
+ refute status == 0
+ end
+
+ test "reports a failure when the command is missing", %{dir: dir} do
+ {:ok, _pid, ref} = Stream.start(self(), dir, [], command: "definitely-not-a-real-binary-xyz")
+
+ assert_receive {:git_output, ^ref, :stderr, msg}, 2_000
+ assert msg =~ "definitely-not-a-real-binary-xyz"
+ assert_receive {:git_done, ^ref, status}, 2_000
+ refute status == 0
+ end
+end
diff --git a/test/bds/git_test.exs b/test/bds/git_test.exs
index 879db27..f4d7491 100644
--- a/test/bds/git_test.exs
+++ b/test/bds/git_test.exs
@@ -250,6 +250,21 @@ defmodule BDS.GitTest do
end)
end
+ test "stream/4 runs a network operation for the project and streams output live", %{
+ project: project,
+ temp_root: temp_root
+ } do
+ script = Path.join(temp_root, "fake-git")
+ File.write!(script, "#!/bin/sh\nprintf 'pushing %s\\n' \"$1\" 1>&2\nexit 0\n")
+ File.chmod!(script, 0o755)
+
+ assert {:ok, _pid, ref} = Git.stream(project.id, :push, self(), command: script)
+
+ assert_receive {:git_output, ^ref, :stderr, chunk}, 2_000
+ assert chunk =~ "pushing push"
+ assert_receive {:git_done, ^ref, 0}, 2_000
+ end
+
defp fake_runner(handler) do
fn command, args, opts -> handler.(command, args, opts) end
end