Skip to content

Add git-gost CLI, jobs queue & client build - #126

Merged
livrasand merged 1 commit into
mainfrom
Add-git-gost-CLI,-jobs-queue-&-client-build
Jul 31, 2026
Merged

Add git-gost CLI, jobs queue & client build#126
livrasand merged 1 commit into
mainfrom
Add-git-gost-CLI,-jobs-queue-&-client-build

Conversation

@livrasand

@livrasand livrasand commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Introduce a full client CLI and local job queue: adds cmd/gost main and internal/cli (install, clone, jobs, watch, pause/resume/cancel, rewrite rules, platform process handling). Adds internal/jobs (store backed by modernc.org/sqlite, run/retry logic, layered clone with resume, unit tests). Updates internal HTTP handlers to preserve protocol/query params and increase timeouts. Adds web UI content for the Git extension and a GitHub Actions workflow to build and publish platform binaries with SHA/Sigstore attestations. Also updates go.mod/go.sum for new deps and test utilities.

Summary by CodeRabbit

  • New Features

    • Added the git-gost command-line client for cloning, Git operations, job management, monitoring, pausing, resuming, and cancellation.
    • Added downloadable macOS, Linux, and Windows binaries with checksums and installation instructions in tagged releases.
    • Added resumable, chunked cloning with progress reporting and automatic retries.
    • Added support for GitHub, GitLab, and Codeberg repository URLs.
    • Added a web page documenting downloads, installation, background jobs, and resumable transfers.
  • Bug Fixes

    • Improved Git protocol forwarding and extended upload-pack request timeouts.

Introduce a full client CLI and local job queue: adds cmd/gost main and internal/cli (install, clone, jobs, watch, pause/resume/cancel, rewrite rules, platform process handling). Adds internal/jobs (store backed by modernc.org/sqlite, run/retry logic, layered clone with resume, unit tests). Updates internal HTTP handlers to preserve protocol/query params and increase timeouts. Adds web UI content for the Git extension and a GitHub Actions workflow to build and publish platform binaries with SHA/Sigstore attestations. Also updates go.mod/go.sum for new deps and test utilities.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds a cross-platform git-gost CLI with persistent jobs, background control, resumable cloning, retry handling, forge URL rewriting, upload-pack protocol forwarding, release automation, and a Git Extension page.

Changes

git-gost client workflow

Layer / File(s) Summary
Job model and SQLite store
internal/jobs/jobs.go, internal/jobs/store.go, internal/jobs/*_test.go, go.mod
Defines job states and metadata. Persists jobs, progress, errors, process IDs, timestamps, and serialized arguments in SQLite.
Retryable and resumable Git execution
internal/jobs/retry.go, internal/jobs/run.go, internal/jobs/clone.go, internal/jobs/*_test.go
Adds retry-aware Git execution, progress capture, failure persistence, chunked cloning, checkpoint resumption, and default-branch checkout.
CLI entry points and repository URL rewriting
cmd/gost/main.go, internal/cli/cli.go, internal/cli/rewrite.go, internal/cli/rewrite_test.go
Adds command dispatch, job creation, Git passthrough, target derivation, installation, and forge URL rewriting.
Background execution and job control
internal/cli/cli.go, internal/cli/proc_unix.go, internal/cli/proc_windows.go
Adds detached execution, PID tracking, monitoring, pause, resume, cancellation, and platform-specific process handling.
Upload-pack protocol forwarding
internal/http/handlers.go
Increases the upload-pack timeout and forwards protocol query parameters and headers upstream.
Client distribution and Git Extension page
.github/workflows/release.yml, web/index.html
Builds and publishes signed binaries with checksums. Adds installation, download, and job-management documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant cli.Run
  participant Store
  participant jobs.Run
  participant Git
  User->>cli.Run: Request background clone
  cli.Run->>Store: Create queued job
  cli.Run->>jobs.Run: Start job ID
  jobs.Run->>Git: Execute resumable clone
  Git-->>jobs.Run: Progress and result
  jobs.Run->>Store: Persist job state
  User->>cli.Run: Monitor job
  cli.Run->>Store: Read progress and state
Loading

Possibly related PRs

Suggested reviewers: gitgost-anonymous

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main changes: the git-gost CLI, jobs queue, and client build workflow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Add-git-gost-CLI,-jobs-queue-&-client-build

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@livrasand
livrasand merged commit 9f962af into main Jul 31, 2026
4 of 5 checks passed
@livrasand
livrasand deleted the Add-git-gost-CLI,-jobs-queue-&-client-build branch July 31, 2026 22:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (9)
internal/jobs/store.go-38-43 (1)

38-43: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Restrict the data directory permissions to the owner.

The queue database can contain remote URLs with embedded credentials, because Job.URL and Job.Origin store whatever URL the user passes. Mode 0o755 makes the directory readable by every local user. Use 0o700 for per-user application data.

🔒 Proposed fix
 	if dir := filepath.Dir(path); dir != "" {
-		if err := os.MkdirAll(dir, 0o755); err != nil {
+		if err := os.MkdirAll(dir, 0o700); err != nil {
 			return nil, fmt.Errorf("crear directorio de datos: %w", err)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/store.go` around lines 38 - 43, Update the MkdirAll call in the
data-directory setup to use mode 0o700 instead of 0o755, keeping the existing
directory creation and error handling unchanged.
internal/jobs/clone_test.go-14-24 (1)

14-24: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Pin the git identity in buildRepo.

git commit requires user.name and user.email. buildRepo relies on the ambient git configuration, so the test fails with Author identity unknown on any machine or CI container without a global git config. runGit discards stderr, so the failure appears only as commit 0: exit status 128.

Set the identity explicitly on the test repository.

💚 Proposed fix
 	if err := runGit(src, "checkout", "-q", "-b", "main"); err != nil {
 		t.Fatalf("crear main: %v", err)
 	}
+	if err := runGit(src, "config", "user.email", "test@gitgost.local"); err != nil {
+		t.Fatalf("config user.email: %v", err)
+	}
+	if err := runGit(src, "config", "user.name", "gitgost test"); err != nil {
+		t.Fatalf("config user.name: %v", err)
+	}
 	for i := 0; i < n; i++ {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/clone_test.go` around lines 14 - 24, Update buildRepo to
configure a repository-local Git user.name and user.email before its commit
loop, using runGit against src and failing through the existing test error
pattern if configuration fails. Keep the identity setup scoped to this test
repository and ensure it occurs after initialization and before the first git
commit.
internal/jobs/run.go-44-47 (1)

44-47: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject an empty job.CWD before running fetch, pull, or push.

cmd.Dir is empty when job.CWD is empty, so git runs in the working directory of the calling process. The CLI launches the runner as a detached background process, so that directory is not necessarily the user's repository. The operation then targets the wrong repository or fails with a confusing message. runClone already guards its equivalent field at lines 23-25 of internal/jobs/clone.go.

🛡️ Proposed fix
 func runGitOperation(s *Store, job *Job) error {
+	if job.CWD == "" {
+		return fmt.Errorf("job de %s sin directorio de trabajo", job.Operation)
+	}
 	args := append([]string{job.Operation}, job.Args...)
 	return execGitWithRetry(s, job, job.CWD, args...)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/run.go` around lines 44 - 47, Update runGitOperation to reject
jobs with an empty job.CWD before constructing arguments or calling
execGitWithRetry; return an appropriate error consistent with the existing
runClone validation, while preserving the current fetch, pull, and push
execution path for valid working directories.
internal/jobs/store.go-43-44 (1)

43-44: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid treating plain paths as URI DSNs.

modernc.org/sqlite v1.55.0 parses the DSN as a file: URI and extracts query parameters from the first ?. A path value containing ?, #, or % can split the DSN or be decoded as URI syntax; build the DSN with URL/query encoding, or use a non-URI form if the driver accepts it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/store.go` around lines 43 - 44, Update the DSN construction
before sql.Open in the store initialization to safely encode the database path
when it contains ?, #, or %. Preserve the existing WAL and busy-timeout pragmas,
and use the driver-supported URL/query encoding approach rather than
concatenating the raw path into the file: URI.
internal/jobs/clone.go-63-64 (1)

63-64: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make blob-filtered fetches persist through origin.

After the configured remote origin, these fetches use job.URL directly. Use fetch origin ... and persist remote.origin.promisor=true plus remote.origin.partialclonefilter=blob:none before the first fetch so the subsequent lazy checkout can fetch missing blobs, instead of relying on the tests’ local file:// source.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/clone.go` around lines 63 - 64, Update the clone setup before
the first fetch in the relevant job flow to configure the existing origin remote
with promisor settings: set remote.origin.promisor=true and
remote.origin.partialclonefilter=blob:none. Change the fetch invocation in the
shown execGitWithRetry call to use origin instead of job.URL, preserving the
existing depth, blob filter, and refspec arguments so lazy checkout can retrieve
missing blobs.
.github/workflows/release.yml-142-143 (1)

142-143: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Disable credential persistence in the client checkout.

actions/checkout writes the job token into .git/config by default. The build job only needs source code. Set persist-credentials: false so the token cannot reach build output or a later step.

🛡️ Proposed fix
       - name: Checkout
         uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+        with:
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 142 - 143, Update the
actions/checkout step in the release workflow to set persist-credentials to
false, ensuring the checkout retrieves source code without storing the job token
in .git/config.

Source: Linters/SAST tools

web/index.html-1648-1648 (1)

1648-1648: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Use HTTPS for the Software Freedom Conservancy link and add rel.

The new link uses http://sfconservancy.org/. The sibling GitHub link on the same line uses https with target="_blank" rel="noopener". Match it.

🔒️ Proposed fix
-Git is a trademark of the <a href="http://sfconservancy.org/" style="color:var(--accent);text-decoration:none;">Software Freedom Conservancy</a>.
+Git is a trademark of the <a href="https://sfconservancy.org/" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none;">Software Freedom Conservancy</a>.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/index.html` at line 1648, Update the Software Freedom Conservancy anchor
in the surrounding footer text to use the HTTPS URL and add the same appropriate
rel attribute as the neighboring external GitHub link, while preserving the
existing styling and text.
web/index.html-3682-3685 (1)

3682-3685: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The pause/resume/cancel claim is not true on Windows.

The card states that the user can pause, resume and cancel any job at any time. internal/cli/proc_windows.go returns an error for all three actions while a job runs. The same page offers the Windows binary at Line 3639. Qualify the claim, or state the current platform limit.

📝 Proposed wording
-                                        <p style="margin:0;font-size:0.78rem;color:var(--fg-muted);">Network failures are detected and retried with exponential backoff. Pause, resume and cancel any job at any time.</p>
+                                        <p style="margin:0;font-size:0.78rem;color:var(--fg-muted);">Network failures are detected and retried with exponential backoff. Pause, resume and cancel any job at any time on macOS and Linux; on Windows, control of a running job is not available yet.</p>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/index.html` around lines 3682 - 3685, Update the “Retrying state with
backoff” card text to avoid claiming pause, resume, and cancel support for every
job; explicitly qualify these controls by platform or state that they are
unavailable for running jobs on Windows. Keep the existing retry and
exponential-backoff description unchanged.
internal/cli/proc_windows.go-25-28 (1)

25-28: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add a console-detach flag for this background process.

CREATE_NEW_PROCESS_GROUP isolates console control signals, but the child remains attached to the parent console unless another creation flag changes console behavior. To avoid a visible console window while keeping a console for a console subsystem, add CREATE_NO_WINDOW; use DETACHED_PROCESS only if the child must not inherit the parent console at all.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/proc_windows.go` around lines 25 - 28, Add
syscall.CREATE_NO_WINDOW to the CreationFlags in the SysProcAttr used by the
command setup, alongside CREATE_NEW_PROCESS_GROUP, so the background process
does not display a console window while retaining console-subsystem behavior.
Use DETACHED_PROCESS only if this flow explicitly requires complete detachment
from the parent console.
🧹 Nitpick comments (10)
internal/jobs/store.go (1)

141-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Constrain column to a fixed set to satisfy the SQL-injection linters.

Static analysis flags the fmt.Sprintf interpolation here. The current code is not exploitable, because SetState, SetProgress, and SetError are the only callers and each passes a literal column name. The finding is still worth closing: it silences the recurring lint error and prevents a future caller from passing dynamic input.

♻️ Proposed refactor with an explicit column allowlist
+var touchableColumns = map[string]string{
+	"state":    `UPDATE jobs SET state = ?, updated_at = ? WHERE id = ?`,
+	"progress": `UPDATE jobs SET progress = ?, updated_at = ? WHERE id = ?`,
+	"error":    `UPDATE jobs SET error = ?, updated_at = ? WHERE id = ?`,
+}
+
 func (s *Store) touch(id int64, column, value string) error {
-	_, err := s.db.Exec(
-		fmt.Sprintf(`UPDATE jobs SET %s = ?, updated_at = ? WHERE id = ?`, column),
-		value, time.Now().UTC().Format(time.RFC3339Nano), id)
-	return err
+	query, ok := touchableColumns[column]
+	if !ok {
+		return fmt.Errorf("columna no permitida: %s", column)
+	}
+	_, err := s.db.Exec(query, value, time.Now().UTC().Format(time.RFC3339Nano), id)
+	return err
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/store.go` around lines 141 - 146, Constrain the column
interpolation in Store.touch to an explicit allowlist containing only the
columns used by SetState, SetProgress, and SetError; reject any other value
before executing the update, while preserving the existing timestamp and job-ID
behavior.

Source: Linters/SAST tools

internal/jobs/clone_test.go (1)

83-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reproduce the real checkpoint with the same fetch flags.

runClone creates its first block with fetch --depth=<chunk> --filter=blob:none at lines 63-64 of internal/jobs/clone.go. The setup here omits --filter=blob:none, so the checkpoint that the test resumes from holds full blobs. The test therefore does not exercise the resume path that production creates, which is the path where the partial-clone configuration matters.

💚 Proposed fix
 	refspec := "+refs/heads/*:refs/remotes/origin/*"
-	if err := runGit(dest, "fetch", "--depth=100", "file://"+src, refspec); err != nil {
+	if err := runGit(dest, "fetch", "--depth=100", "--filter=blob:none", "file://"+src, refspec); err != nil {
 		t.Fatalf("checkpoint inicial: %v", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/clone_test.go` around lines 83 - 86, Update the checkpoint
setup in the test around runGit to include the same --filter=blob:none flag used
by runClone’s initial fetch, while preserving the existing depth, source, and
refspec arguments so resume testing starts from a production-equivalent partial
clone.
internal/jobs/retry.go (2)

117-127: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Separate the empty-line check from the nil-progress check.

Line 119 skips the iteration when progress == nil, so lastLine is never assigned in that case. The error returned at line 138 then omits the git message, and isRetryable cannot classify the failure. All current callers pass a non-nil callback from progressFn, so the defect is latent. Assign lastLine first, then call the callback only if it exists.

The truncation at line 123 slices by bytes. A localized git message can be cut in the middle of a UTF-8 rune, and the invalid bytes are then persisted in progress. Slice by runes instead.

♻️ Proposed refactor
 	for scanner.Scan() {
 		line := strings.TrimSpace(scanner.Text())
-		if line == "" || progress == nil {
+		if line == "" {
 			continue
 		}
-		if len(line) > 160 {
-			line = line[len(line)-160:]
+		if r := []rune(line); len(r) > 160 {
+			line = string(r[len(r)-160:])
 		}
 		lastLine = line
-		progress(line)
+		if progress != nil {
+			progress(line)
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/retry.go` around lines 117 - 127, Update the scanner loop in
the retry handling flow to always assign trimmed, rune-safe truncated text to
lastLine regardless of whether progress is nil, then invoke progress only when
the callback exists; keep empty lines skipped and replace byte-based truncation
with rune-based truncation capped at 160 characters.

77-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated retry loop. Both functions implement the identical attempt loop, exponential backoff, StateRetrying transition, and progress message. Only the executed operation differs. The shared root cause is one retry policy written twice, so a change to retryMax, the backoff growth, or the progress text must be applied in two places.

Extract a helper such as withRetry(s *Store, jobID int64, op func() error) error and let both call sites supply the operation.

  • internal/jobs/retry.go#L77-L97: reduce execGitWithRetry to a withRetry call that wraps runGitCapture.
  • internal/jobs/clone.go#L166-L189: reduce gitOutputWithRetry to a withRetry call that wraps gitOutput and captures out in the closure.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/retry.go` around lines 77 - 97, The retry policy is duplicated
across both operations. In internal/jobs/retry.go lines 77-97, add a shared
withRetry helper and reduce execGitWithRetry to supplying runGitCapture as its
operation; in internal/jobs/clone.go lines 166-189, reduce gitOutputWithRetry to
the same helper while capturing out from gitOutput in the closure. Preserve the
existing retry limits, exponential backoff, StateRetrying transition, progress
message, and error handling in withRetry.
internal/jobs/clone.go (2)

123-126: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Do not treat a rev-parse failure as "history complete".

repoShallow returns false both when the repository is complete and when the git rev-parse call fails. runClone uses that value as the loop condition at line 73 and as the --unshallow guard at line 94. If rev-parse fails transiently, runClone exits the deepen loop, skips --unshallow, and returns nil. Run then marks the job completed while the repository still has partial history, and the user receives no error.

Return the error and let runClone propagate it, so a failed probe retries or fails the job instead of reporting success.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/clone.go` around lines 123 - 126, Update repoShallow to
propagate the gitOutput error instead of converting rev-parse failures into
false; adjust its callers in runClone to handle the returned error and propagate
it, while preserving the shallow-state result for successful probes so failed
checks cannot report a completed clone.

29-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make runGit failures diagnosable.

runGit calls cmd.Run() and discards both stdout and stderr. Each wrapped error therefore reports only exit status 1. The user sees crear rama local main: exit status 1 in the job error column with no cause. The truncated message is then persisted by Run and displayed by git gost list.

gitOutput already attaches the git message to the error. Use it for these call sites, or capture stderr inside runGit.

♻️ Proposed refactor
 // runGit ejecuta git sin capturar salida.
 func runGit(dir string, args ...string) error {
-	cmd := exec.Command("git", args...)
-	cmd.Dir = dir
-	return cmd.Run()
+	_, err := gitOutput(dir, args...)
+	return err
 }

This also improves isRetryable classification for the errors that reach it from these paths.

Also applies to: 41-43, 103-108, 192-196

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/clone.go` around lines 29 - 31, Update the git operation call
sites in the clone job, including the shown repository initialization and the
referenced branch-related paths, to use gitOutput instead of runGit so command
output is preserved in returned errors. Keep the existing wrapped error context
and behavior unchanged while ensuring failures include Git’s diagnostic message
for persistence and retry classification.
internal/cli/cli.go (1)

431-469: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

watch never returns for a paused job.

The loop exits only on StateCompleted and StateFailed. A paused job keeps the loop polling every 500 ms with no output. Print the paused state once and return, or document that the user must interrupt the command.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/cli.go` around lines 431 - 469, Update cmdWatch so a job in the
paused state is handled explicitly: print the paused status once and return
instead of continuing the 500 ms polling loop. Preserve the existing progress
output and completed/failed handling.
internal/cli/proc_windows.go (1)

46-50: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Windows can support cancel, even without POSIX signals.

signalJob rejects every request on Windows. Pause and resume need job objects, so the current limit is reasonable for them. Cancel does not: os.FindProcess plus Process.Kill terminates the process without taskkill. Implement cancel so that git gost cancel works on Windows, and keep the error for pause and resume only.

♻️ Proposed direction
-func signalJob(pid int, sig syscall.Signal) error {
+func signalJob(pid int, sig syscall.Signal) error {
+	if sig == sigTerm {
+		if pid <= 0 {
+			return fmt.Errorf("el job no tiene proceso en background activo")
+		}
+		p, err := os.FindProcess(pid)
+		if err != nil {
+			return err
+		}
+		return p.Kill()
+	}
 	return fmt.Errorf("pause/resume/cancel en ejecución no está soportado en Windows en esta fase")
 }

Note that sigStop, sigCont and sigTerm are all syscall.Signal(0) on Windows, so they are not distinguishable. Give them distinct sentinel values before you branch on them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/proc_windows.go` around lines 46 - 50, Update signalJob to
distinguish cancel from pause/resume using distinct sentinel values for sigStop,
sigCont, and sigTerm on Windows, then handle the cancel sentinel by locating the
process with os.FindProcess and terminating it via Process.Kill. Preserve the
unsupported-operation error for pause and resume, and update the signal
definitions so they remain distinguishable on Windows.
internal/cli/proc_unix.go (1)

28-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a log file instead of /dev/null for the child.

The detached job writes stdout and stderr to os.DevNull. Diagnostics for a failed background job then depend only on the Error field in the store. A rotating log file under dataDir(), for example ~/.gitgost/logs/job-<id>.log, makes support and debugging practical.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/proc_unix.go` around lines 28 - 38, The detached child process
currently discards output through os.DevNull; update the command setup around
cmd.Stdin, cmd.Stdout, and cmd.Stderr to create a per-job log file under
dataDir(), such as logs/job-<id>.log, and route the child’s stdout and stderr to
it while preserving detached execution. Ensure the log directory and file are
created using the existing job identifier and close the file appropriately.
internal/cli/rewrite.go (1)

40-47: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Report unsupported nested GitLab groups explicitly.

The server routes only expose /v1/<prefix>/owner/repo, but https://gitlab.com/group/subgroup/repo.git is a valid GitLab URL. RewriteURL currently reports it as URL de repositorio inválida. Return a limitation-specific error for nested paths before falling back to the invalid error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/rewrite.go` around lines 40 - 47, Update RewriteURL’s path
validation around splitPath so paths with more than two segments return a
limitation-specific error indicating nested GitLab groups are unsupported;
retain the existing invalid-repository error for paths with fewer than two
segments or invalid owner/repo segments.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 150-160: Update the release workflow’s Get commit info step and
the build, sha, and checksum steps to pass github.ref, github.ref_name, matrix
values, and steps.*.outputs.* through each step’s env block, then reference
those environment variables inside run scripts instead of interpolating GitHub
expressions directly. Preserve the existing version-selection and output
behavior while ensuring all shell inputs are treated as data.
- Around line 212-218: Consolidate release publishing so only one job invokes
softprops/action-gh-release for each tag. Update the build-and-attest and
build-client matrix flow to collect or pass all generated binaries and checksums
to a single release job, ensuring that job publishes every artifact without
concurrent release updates; alternatively, add a tag-based concurrency group
that serializes the existing release actions.

In `@internal/cli/cli.go`:
- Around line 472-508: Update cmdPause and cmdResume to validate that the stored
j.PID still belongs to the target job before calling signalJob. Add and persist
a job-process identity marker, such as the child’s start time or token, and
compare it with the current process identity after syscall.Kill(pid, 0) confirms
liveness. If the identity does not match or the process is absent, do not signal
the process group and handle the stale state safely.
- Around line 356-367: Update launchBackground to persist the intended
pre-launch state before calling startBackground, then remove the post-launch
SetState write so only SetPID persists after the child starts. Ensure cmdResume
and other callers do not overwrite running, completed, or failed states reported
by jobs.Run.
- Around line 328-338: Update parseFlags to inspect only leading CLI flags and
stop at the first non-flag argument, preserving that argument and all subsequent
Git arguments—including -f—for downstream handling; retain foreground parsing
only before the command. Remove the always-nil err return or update cmdClone and
cmdGitJob to handle the revised signature consistently.

In `@internal/cli/proc_windows.go`:
- Around line 46-50: Implement Windows cancellation in signalJob using
os.FindProcess and Process.Kill, with distinct sigStop, sigCont, and sigTerm
sentinel values for action branching. In internal/cli/cli.go lines 517-520,
propagate signalJob errors from cmdCancel and set StateFailed only after the
process stops. In web/index.html lines 3682-3685, document that pause and resume
are unavailable on Windows until job objects are supported.

In `@internal/http/handlers.go`:
- Around line 37-40: Update the upstream GET and POST request construction in
the relevant handlers to use http.NewRequestWithContext with c.Request.Context()
instead of http.NewRequest, preserving the existing request methods, URLs, and
bodies so client disconnects cancel upstream work promptly.
- Around line 663-676: The UploadPackDiscoveryHandler discovery proxy must
forward the client’s Git-Protocol header to the remote request, since query
parameters do not select protocol v2. Update the remote discovery request
construction around remoteURL to propagate the incoming Git-Protocol value, and
extend the discovery proxy test to verify this header is forwarded.

In `@internal/jobs/retry.go`:
- Around line 34-36: Update isRetryable’s noRetry status matching for 401, 403,
404, 502, 503, and 504 to require surrounding git or curl HTTP-error text rather
than matching bare numeric substrings. Preserve recognition of existing forms
such as “HTTP 502” and “returned error: 504”, while preventing counters, ports,
URLs, or other incidental numbers from being classified as permanent failures.
- Around line 102-104: Force a stable English locale for both git command
builders by setting cmd.Env to the existing process environment plus LC_ALL=C
and LANG=C. Apply this in runGitCapture in internal/jobs/retry.go at lines
102-104 before cmd.Start(), and in gitOutput in internal/jobs/clone.go at lines
153-156 before cmd.CombinedOutput().
- Around line 129-133: Update the scanner.Err() failure path to call cmd.Wait()
after cmd.Process.Kill() and before returning the scanner error, ensuring the
child process is reaped and StderrPipe resources are released.

In `@internal/jobs/run.go`:
- Around line 19-24: Replace the separate state validation and SetState calls in
Run with an atomic claim through a new Store.Claim method. Implement Claim in
internal/jobs/store.go using a conditional UPDATE that excludes Running,
Completed, and Failed states, then verify RowsAffected; have Run return the
existing terminal-state error when the claim is unsuccessful and propagate
database errors.

---

Minor comments:
In @.github/workflows/release.yml:
- Around line 142-143: Update the actions/checkout step in the release workflow
to set persist-credentials to false, ensuring the checkout retrieves source code
without storing the job token in .git/config.

In `@internal/cli/proc_windows.go`:
- Around line 25-28: Add syscall.CREATE_NO_WINDOW to the CreationFlags in the
SysProcAttr used by the command setup, alongside CREATE_NEW_PROCESS_GROUP, so
the background process does not display a console window while retaining
console-subsystem behavior. Use DETACHED_PROCESS only if this flow explicitly
requires complete detachment from the parent console.

In `@internal/jobs/clone_test.go`:
- Around line 14-24: Update buildRepo to configure a repository-local Git
user.name and user.email before its commit loop, using runGit against src and
failing through the existing test error pattern if configuration fails. Keep the
identity setup scoped to this test repository and ensure it occurs after
initialization and before the first git commit.

In `@internal/jobs/clone.go`:
- Around line 63-64: Update the clone setup before the first fetch in the
relevant job flow to configure the existing origin remote with promisor
settings: set remote.origin.promisor=true and
remote.origin.partialclonefilter=blob:none. Change the fetch invocation in the
shown execGitWithRetry call to use origin instead of job.URL, preserving the
existing depth, blob filter, and refspec arguments so lazy checkout can retrieve
missing blobs.

In `@internal/jobs/run.go`:
- Around line 44-47: Update runGitOperation to reject jobs with an empty job.CWD
before constructing arguments or calling execGitWithRetry; return an appropriate
error consistent with the existing runClone validation, while preserving the
current fetch, pull, and push execution path for valid working directories.

In `@internal/jobs/store.go`:
- Around line 38-43: Update the MkdirAll call in the data-directory setup to use
mode 0o700 instead of 0o755, keeping the existing directory creation and error
handling unchanged.
- Around line 43-44: Update the DSN construction before sql.Open in the store
initialization to safely encode the database path when it contains ?, #, or %.
Preserve the existing WAL and busy-timeout pragmas, and use the driver-supported
URL/query encoding approach rather than concatenating the raw path into the
file: URI.

In `@web/index.html`:
- Line 1648: Update the Software Freedom Conservancy anchor in the surrounding
footer text to use the HTTPS URL and add the same appropriate rel attribute as
the neighboring external GitHub link, while preserving the existing styling and
text.
- Around line 3682-3685: Update the “Retrying state with backoff” card text to
avoid claiming pause, resume, and cancel support for every job; explicitly
qualify these controls by platform or state that they are unavailable for
running jobs on Windows. Keep the existing retry and exponential-backoff
description unchanged.

---

Nitpick comments:
In `@internal/cli/cli.go`:
- Around line 431-469: Update cmdWatch so a job in the paused state is handled
explicitly: print the paused status once and return instead of continuing the
500 ms polling loop. Preserve the existing progress output and completed/failed
handling.

In `@internal/cli/proc_unix.go`:
- Around line 28-38: The detached child process currently discards output
through os.DevNull; update the command setup around cmd.Stdin, cmd.Stdout, and
cmd.Stderr to create a per-job log file under dataDir(), such as
logs/job-<id>.log, and route the child’s stdout and stderr to it while
preserving detached execution. Ensure the log directory and file are created
using the existing job identifier and close the file appropriately.

In `@internal/cli/proc_windows.go`:
- Around line 46-50: Update signalJob to distinguish cancel from pause/resume
using distinct sentinel values for sigStop, sigCont, and sigTerm on Windows,
then handle the cancel sentinel by locating the process with os.FindProcess and
terminating it via Process.Kill. Preserve the unsupported-operation error for
pause and resume, and update the signal definitions so they remain
distinguishable on Windows.

In `@internal/cli/rewrite.go`:
- Around line 40-47: Update RewriteURL’s path validation around splitPath so
paths with more than two segments return a limitation-specific error indicating
nested GitLab groups are unsupported; retain the existing invalid-repository
error for paths with fewer than two segments or invalid owner/repo segments.

In `@internal/jobs/clone_test.go`:
- Around line 83-86: Update the checkpoint setup in the test around runGit to
include the same --filter=blob:none flag used by runClone’s initial fetch, while
preserving the existing depth, source, and refspec arguments so resume testing
starts from a production-equivalent partial clone.

In `@internal/jobs/clone.go`:
- Around line 123-126: Update repoShallow to propagate the gitOutput error
instead of converting rev-parse failures into false; adjust its callers in
runClone to handle the returned error and propagate it, while preserving the
shallow-state result for successful probes so failed checks cannot report a
completed clone.
- Around line 29-31: Update the git operation call sites in the clone job,
including the shown repository initialization and the referenced branch-related
paths, to use gitOutput instead of runGit so command output is preserved in
returned errors. Keep the existing wrapped error context and behavior unchanged
while ensuring failures include Git’s diagnostic message for persistence and
retry classification.

In `@internal/jobs/retry.go`:
- Around line 117-127: Update the scanner loop in the retry handling flow to
always assign trimmed, rune-safe truncated text to lastLine regardless of
whether progress is nil, then invoke progress only when the callback exists;
keep empty lines skipped and replace byte-based truncation with rune-based
truncation capped at 160 characters.
- Around line 77-97: The retry policy is duplicated across both operations. In
internal/jobs/retry.go lines 77-97, add a shared withRetry helper and reduce
execGitWithRetry to supplying runGitCapture as its operation; in
internal/jobs/clone.go lines 166-189, reduce gitOutputWithRetry to the same
helper while capturing out from gitOutput in the closure. Preserve the existing
retry limits, exponential backoff, StateRetrying transition, progress message,
and error handling in withRetry.

In `@internal/jobs/store.go`:
- Around line 141-146: Constrain the column interpolation in Store.touch to an
explicit allowlist containing only the columns used by SetState, SetProgress,
and SetError; reject any other value before executing the update, while
preserving the existing timestamp and job-ID behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a2c1b4c4-4286-40fe-9da2-32f312658c08

📥 Commits

Reviewing files that changed from the base of the PR and between b278fcb and 6161ecb.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (18)
  • .github/workflows/release.yml
  • cmd/gost/main.go
  • go.mod
  • internal/cli/cli.go
  • internal/cli/proc_unix.go
  • internal/cli/proc_windows.go
  • internal/cli/rewrite.go
  • internal/cli/rewrite_test.go
  • internal/http/handlers.go
  • internal/jobs/clone.go
  • internal/jobs/clone_test.go
  • internal/jobs/jobs.go
  • internal/jobs/retry.go
  • internal/jobs/retry_test.go
  • internal/jobs/run.go
  • internal/jobs/store.go
  • internal/jobs/store_test.go
  • web/index.html

Comment on lines +150 to +160
- name: Get commit info
id: info
run: |
short=$(git rev-parse --short HEAD)
echo "short=$short" >> "$GITHUB_OUTPUT"
echo "built=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "ver=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
else
echo "ver=$short" >> "$GITHUB_OUTPUT"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Pass github.ref and github.ref_name through env, not string interpolation.

The run block expands ${{ github.ref }} and ${{ github.ref_name }} directly into bash. A ref name that contains shell metacharacters becomes shell code in the runner. The same value then flows into -ldflags at Line 175. Use environment variables so the shell receives data, not code.

🛡️ Proposed fix
       - name: Get commit info
         id: info
+        env:
+          GH_REF: ${{ github.ref }}
+          GH_REF_NAME: ${{ github.ref_name }}
         run: |
           short=$(git rev-parse --short HEAD)
           echo "short=$short" >> "$GITHUB_OUTPUT"
           echo "built=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
-          if [[ "${{ github.ref }}" == refs/tags/* ]]; then
-            echo "ver=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
+          if [[ "$GH_REF" == refs/tags/* ]]; then
+            echo "ver=$GH_REF_NAME" >> "$GITHUB_OUTPUT"
           else
             echo "ver=$short" >> "$GITHUB_OUTPUT"
           fi

Apply the same pattern to the matrix and steps.*.outputs.* expansions in the build, sha and checksum steps.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Get commit info
id: info
run: |
short=$(git rev-parse --short HEAD)
echo "short=$short" >> "$GITHUB_OUTPUT"
echo "built=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "ver=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
else
echo "ver=$short" >> "$GITHUB_OUTPUT"
fi
- name: Get commit info
id: info
env:
GH_REF: ${{ github.ref }}
GH_REF_NAME: ${{ github.ref_name }}
run: |
short=$(git rev-parse --short HEAD)
echo "short=$short" >> "$GITHUB_OUTPUT"
echo "built=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
if [[ "$GH_REF" == refs/tags/* ]]; then
echo "ver=$GH_REF_NAME" >> "$GITHUB_OUTPUT"
else
echo "ver=$short" >> "$GITHUB_OUTPUT"
fi
🧰 Tools
🪛 zizmor (1.28.0)

[error] 156-156: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 157-157: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 150 - 160, Update the release
workflow’s Get commit info step and the build, sha, and checksum steps to pass
github.ref, github.ref_name, matrix values, and steps.*.outputs.* through each
step’s env block, then reference those environment variables inside run scripts
instead of interpolating GitHub expressions directly. Preserve the existing
version-selection and output behavior while ensuring all shell inputs are
treated as data.

Source: Linters/SAST tools

Comment on lines +212 to +218
- name: Create/update release on tags
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe
with:
files: |
${{ steps.build.outputs.bin }}
${{ steps.build.outputs.bin }}.sha256

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the workflow header and any permissions blocks.
sed -n '1,20p' .github/workflows/release.yml
rg -n 'permissions:|id-token|attestations|contents:|concurrency:' .github/workflows/release.yml

Repository: livrasand/gitGost

Length of output: 563


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow outline/line count =="
wc -l .github/workflows/release.yml
echo

echo "== jobs and relevant steps =="
awk '
  /^jobs:/ { in_jobs=1; next }
  in_jobs && /^[^ ]/ { in_jobs=0; print "---", prev_job, "lines:", start_line, "-", NR-1 }
  in_jobs && /^[[:space:]]{2}[a-zA-Z0-9_-]+:/{
    start_line=NR; prev_job=$0; gsub(/^  [/[:space:]/, "", prev_job); print "---", prev_job
  }
  in_jobs && /action-gh-release|attest-build-provenance|build-client|matrix|concurrency|permissions:/{
    printf "%d: %s\n", NR, $0
  }
  in_jobs { prev_job=$0 }
' .github/workflows/release.yml
echo

echo "== action releases/attest usages =="
rg -n 'uses: softprops/action-gh-release|uses:.*/attest-build-provenance|build-client|build-and-attest|matrix:|packages|contents:' .github/workflows/release.yml
echo

echo "== lines 180-225 =="
sed -n '180,225p' .github/workflows/release.yml
echo

echo "== lines 70-95 =="
sed -n '70,95p' .github/workflows/release.yml

Repository: livrasand/gitGost

Length of output: 542


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow line count =="
wc -l .github/workflows/release.yml
echo

echo "== releases/attests/usages =="
rg -n 'uses: softprops/action-gh-release|attest-build-provenance|build-client|build-and-attest|mach-(?:arm64|x64|amd)64|matrix:|packages|contents:' .github/workflows/release.yml || true
echo

echo "== lines 180-218 =="
sed -n '180,218p' .github/workflows/release.yml
echo

echo "== lines 70-110 =="
sed -n '70,110p' .github/workflows/release.yml >/tmp/release_slice.txt
cat -n /tmp/release_slice.txt

Repository: livrasand/gitGost

Length of output: 4106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lines 110-140 =="
sed -n '110,140p' .github/workflows/release.yml

echo
echo "== full workflow text with line numbers relevant to jobs =="
awk '
  NR>=1 && NR<=218 { printf "%d: %s\n", NR, $0 }
' .github/workflows/release.yml >/tmp/release.yml.lined
sed -n '110,135p' /tmp/release.yml.lined
sed -n '1,25p' /tmp/release.yml.lined

Repository: livrasand/gitGost

Length of output: 2945


🌐 Web query:

softprops action-gh-release files multiple files append assets documentation

💡 Result:

To upload multiple files using the softprops/action-gh-release GitHub Action, you use the 'files' input [1][2]. This input accepts a newline-delimited list of paths or glob expressions, allowing you to specify multiple files or patterns to include as release assets [1][3][4]. Configuration Example: To upload multiple files, provide them as a multi-line string in your workflow YAML [1][5]: steps: - name: Release uses: softprops/action-gh-release@v3 with: files: | path/to/file1.txt path/to/file2.exe dist/*.tar.gz Key points regarding asset uploading: - Glob Support: The 'files' input supports glob patterns, which are useful for matching multiple files in a directory [1][6][5]. - Working Directory: If your assets are located in a subdirectory, you can use the 'working_directory' input to set the base directory for resolving your 'files' paths [1][2][6]. - Updating Releases: If a release already exists for the given tag, the action will automatically update the release by adding or overwriting the specified assets [1][3]. - Overwriting: By default, the action overwrites existing files with the same name [7][4]. - Preservation of Order: The 'preserve_order' input is available if you need to upload artifacts sequentially, though this does not affect the final display order in the GitHub UI [4]. Documentation regarding appending: The action provides an 'append_body' input (boolean, default is false), which is used for appending text to an existing release body rather than overwriting it [1][4][8]. This setting is distinct from managing release assets (files), which are handled via the 'files' input [1][4].

Citations:


Run release publishing from a single release job.

The workflow has one global permissions block, so id-token and attestations are granted. The remaining hazard is the six concurrent publish actions: one build-and-attest release step plus five build-client matrix release steps. They can create/update the same tag release at the same time and overwrite assets or release metadata. Collect the built artifacts in one job and run softprops/action-gh-release there, or gate the release job with a concurrency group.

🧰 Tools
🪛 zizmor (1.28.0)

[info] 214-214: action functionality is already included by the runner (superfluous-actions): use gh release in a script step

(superfluous-actions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 212 - 218, Consolidate release
publishing so only one job invokes softprops/action-gh-release for each tag.
Update the build-and-attest and build-client matrix flow to collect or pass all
generated binaries and checksums to a single release job, ensuring that job
publishes every artifact without concurrent release updates; alternatively, add
a tag-based concurrency group that serializes the existing release actions.

Comment thread internal/cli/cli.go
Comment on lines +328 to +338
func parseFlags(args []string) (foreground bool, rest []string, err error) {
for _, a := range args {
switch a {
case "-f", "--foreground":
foreground = true
default:
rest = append(rest, a)
}
}
return foreground, rest, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

parseFlags swallows -f meant for Git.

parseFlags scans every argument. git gost push -f therefore runs as a foreground job and drops -f from the Git arguments. The force push does not happen. The same applies to git gost push --force-with-lease? No — only exact -f and --foreground match, but -f is the common Git force flag for push.

Parse only leading flags, and stop at the first non-flag argument. The err return value is also always nil.

🐛 Proposed fix
-func parseFlags(args []string) (foreground bool, rest []string, err error) {
-	for _, a := range args {
-		switch a {
-		case "-f", "--foreground":
-			foreground = true
-		default:
-			rest = append(rest, a)
-		}
-	}
-	return foreground, rest, nil
-}
+func parseFlags(args []string) (foreground bool, rest []string, err error) {
+	i := 0
+	for ; i < len(args); i++ {
+		switch args[i] {
+		case "-f", "--foreground":
+			foreground = true
+		case "--":
+			i++
+			rest = append(rest, args[i:]...)
+			return foreground, rest, nil
+		default:
+			rest = append(rest, args[i:]...)
+			return foreground, rest, nil
+		}
+	}
+	return foreground, rest, nil
+}

Update the call sites in cmdClone and cmdGitJob if you keep the always-nil error.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func parseFlags(args []string) (foreground bool, rest []string, err error) {
for _, a := range args {
switch a {
case "-f", "--foreground":
foreground = true
default:
rest = append(rest, a)
}
}
return foreground, rest, nil
}
func parseFlags(args []string) (foreground bool, rest []string, err error) {
i := 0
for ; i < len(args); i++ {
switch args[i] {
case "-f", "--foreground":
foreground = true
case "--":
i++
rest = append(rest, args[i:]...)
return foreground, rest, nil
default:
rest = append(rest, args[i:]...)
return foreground, rest, nil
}
}
return foreground, rest, nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/cli.go` around lines 328 - 338, Update parseFlags to inspect
only leading CLI flags and stop at the first non-flag argument, preserving that
argument and all subsequent Git arguments—including -f—for downstream handling;
retain foreground parsing only before the command. Remove the always-nil err
return or update cmdClone and cmdGitJob to handle the revised signature
consistently.

Comment thread internal/cli/cli.go
Comment on lines +356 to +367
func launchBackground(s *jobs.Store, id int64) int {
pid, err := startBackground(id)
if err != nil {
_ = s.SetState(id, jobs.StateFailed)
_ = s.SetError(id, err.Error())
fmt.Fprintf(os.Stderr, "git-gost: %v\n", err)
return 1
}
_ = s.SetPID(id, pid)
fmt.Printf("Job created.\n\nID: %d\nRunning in background...\n\nUse 'git gost watch %d' to follow progress.\n", id, id)
return 0
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm that jobs.Run performs its own state transitions (running/completed/failed).
fd -t f 'run.go' internal/jobs --exec cat -n
rg -n --type=go 'func \(s \*Store\) (SetState|SetPID|SetError)' internal/jobs/store.go

Repository: livrasand/gitGost

Length of output: 2428


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cli.go relevant sections =="
fd -t f 'cli.go' internal --exec sh -c 'echo "--- $1"; rg -n -C 4 "func launchBackground|cmdResume|startBackground|SetPID|SetState\\(j\\.ID, jobs\\.StateQueued\\)" "$1"' sh {}

echo
echo "== jobs store methods =="
fd -t f 'store.go' internal/jobs --exec sh -c 'echo "--- $1"; cat -n "$1" | sed -n "100,180p"' sh {}

echo
echo "== startBackground definitions/usages =="
rg -n --type=go "func startBackground|startBackground\\(" internal

Repository: livrasand/gitGost

Length of output: 4520


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in internal/cli/proc_unix.go internal/cli/proc_windows.go; do
  echo "--- $f"
  cat -n "$f"
  echo
done

fd -t f 'store.go' internal/jobs --exec sh -c 'echo "--- $1"; cat -n "$1" | sed -n "1,140p"' sh {}

echo "== signalCommand and SetState usages in signal handling =="
fd -t f 'cli.go' internal --exec sh -c 'echo "--- $1"; rg -n -C 3 "func signalCommand|s\\.SetState|jobs\\.StatePa|jobs\\.StateQueued|jobs\\.StateRunning" "$1"' sh {}

Repository: livrasand/gitGost

Length of output: 10566


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== command/state transitions around launchBackground =="
fd -t f 'cli.go' internal --exec sh -c 'echo "--- $1"; rg -n -C 5 "launchBackground\\(|cmdRun|StateRunning|StateCompleted|StateFailed" "$1"' sh {}

echo
echo "== job status/read functions =="
rg -n --type=go 'func cmd(Show|List|Watch|Status)|ReadState|state|completed|StateCompleted|Paused|Queued' internal --glob '*.go' | head -n 200

Repository: livrasand/gitGost

Length of output: 15059


Write the final background state before launching the child.

jobs.Run transitions running and can set completed/failed; any parent state write after startBackground can overwrite that result. Move the intended state write before Start, and let only SetPID persist after launch, not SetState. This is especially visible in cmdResume, where writing StateQueued after the child has already reported running or completed reverts the job’s state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/cli.go` around lines 356 - 367, Update launchBackground to
persist the intended pre-launch state before calling startBackground, then
remove the post-launch SetState write so only SetPID persists after the child
starts. Ensure cmdResume and other callers do not overwrite running, completed,
or failed states reported by jobs.Run.

Comment thread internal/cli/cli.go
Comment on lines +472 to +508
func cmdPause(args []string) int {
return signalCommand("pause", args, func(j *jobs.Job, s *jobs.Store) (int, error) {
switch j.State {
case jobs.StateQueued, jobs.StateRetrying:
return 0, s.SetState(j.ID, jobs.StatePaused)
case jobs.StateRunning:
if err := signalJob(j.PID, sigStop); err != nil {
return 0, err
}
return 0, s.SetState(j.ID, jobs.StatePaused)
default:
return 0, fmt.Errorf("el job %d no se puede pausar (estado: %s)", j.ID, j.State)
}
})
}

// cmdResume reanuda un job pausado.
func cmdResume(args []string) int {
return signalCommand("resume", args, func(j *jobs.Job, s *jobs.Store) (int, error) {
if j.State != jobs.StatePaused {
return 0, fmt.Errorf("el job %d no está pausado (estado: %s)", j.ID, j.State)
}
if j.PID > 0 {
if err := signalJob(j.PID, sigCont); err != nil {
return 0, err
}
return 0, s.SetState(j.ID, jobs.StateRunning)
}
// Pausado sin proceso (estaba en cola): relanzar en background.
pid, err := startBackground(j.ID)
if err != nil {
return 0, err
}
_ = s.SetPID(j.ID, pid)
return 0, s.SetState(j.ID, jobs.StateQueued)
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

A stale PID can signal an unrelated process group.

cmdPause and cmdResume read j.PID from the store and call signalJob(j.PID, …), which sends the signal to the whole process group -pid on Unix. If the recorded process already exited and the operating system reused the PID, gitGost stops or continues a process group that belongs to another program. The store state can stay running after a crash, so this path is reachable.

Verify that the recorded process is still the job process before you signal it. Check liveness with syscall.Kill(pid, 0) and store an additional identity marker, for example the process start time or a job token that the child records at startup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/cli.go` around lines 472 - 508, Update cmdPause and cmdResume to
validate that the stored j.PID still belongs to the target job before calling
signalJob. Add and persist a job-process identity marker, such as the child’s
start time or token, and compare it with the current process identity after
syscall.Kill(pid, 0) confirms liveness. If the identity does not match or the
process is absent, do not signal the process group and handle the stale state
safely.

Comment thread internal/http/handlers.go
Comment on lines +663 to +676
// Reenviar los query params del cliente (protocol=v2 y otros) además del
// service: sin protocol=v2 el servidor remoto responde en protocolo v1 y
// git no puede usar filtros (partial clone) ni la negociación eficiente.
q := url.Values{}
q.Set("service", "git-upload-pack")
for k, vals := range c.Request.URL.Query() {
if k == "service" {
continue
}
for _, v := range vals {
q.Add(k, v)
}
}
remoteURL := prov.CloneURL(owner, repo) + "/info/refs?" + q.Encode()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 6 'UploadPackDiscoveryHandler|Git-Protocol|NewRequest' internal/http/handlers.go
rg -n -C 6 'TestUploadPackDiscoveryHandler_ProxiesGitHub|Git-Protocol' internal/http/handlers_upload_test.go

Repository: livrasand/gitGost

Length of output: 10851


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== handlers.go relevant section =="
sed -n '656,705p' internal/http/handlers.go

echo "== test relevant section =="
sed -n '111,180p' internal/http/handlers_upload_test.go

echo "== protocol v2 header occurrences =="
rg -n -C 3 'Git-Protocol|protocol=v2|info/refs' internal/http handlers.go internal/http/handlers_*test.go README.md 2>/dev/null || true

Repository: livrasand/gitGost

Length of output: 21039


🌐 Web query:

Git HTTP protocol v2 client sends GET /info/refs service=git-upload-pack Git-Protocol header

💡 Result:

In the Git wire protocol version 2, a client initiates a request to use v2 over HTTP or HTTPS by including the Git-Protocol header with the value version=2 in its initial smart info/refs request [1][2][3]. The client makes a GET request to the URL path /info/refs with the service query parameter set to the desired service (typically git-upload-pack for fetches or git-receive-pack for pushes) [1][4][5]. The full request structure is as follows: C: GET $GIT_URL/info/refs?service=git-upload-pack HTTP/1.0 C: Git-Protocol: version=2 Upon receiving this request, if the server supports protocol version 2, it responds with a capability advertisement [3][6][7]. This advertisement begins with a version string (version 2) followed by a list of available capabilities [3][6]. The Git-Protocol header acts as a side-channel mechanism for the client to negotiate the protocol version [6][7]. On the server side, the web server or CGI script (such as git-http-backend) must be configured to pass the contents of this header into the environment variable GIT_PROTOCOL so that the Git backend processes can access the requested protocol version [4][3]. After this initial handshake, subsequent requests are directed to the service endpoint, such as $GIT_URL/git-upload-pack, where the client can then issue specific commands like ls-refs or fetch [1][3].

Citations:


Forward Git-Protocol during discovery.

The initial GET /info/refs?service=git-upload-pack for protocol v2 uses the Git-Protocol: version=2 header, not protocol=v2 as a query parameter. UploadPackDiscoveryHandler copies query params but does not copy this header, while the later POST handler does; the later header cannot change the advertisement already returned. Add the header forwarding to the discovery request and cover it in the discovery proxy test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/http/handlers.go` around lines 663 - 676, The
UploadPackDiscoveryHandler discovery proxy must forward the client’s
Git-Protocol header to the remote request, since query parameters do not select
protocol v2. Update the remote discovery request construction around remoteURL
to propagate the incoming Git-Protocol value, and extend the discovery proxy
test to verify this header is forwarded.

Comment thread internal/jobs/retry.go
Comment on lines +34 to +36
"401",
"403",
"404",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Anchor the HTTP status codes to avoid misclassifying transient failures.

isRetryable matches bare "401", "403", "404", "502", "503", and "504" anywhere in the message. The message that reaches this function is the last stderr line captured by runGitCapture, which often contains counters, ports, byte sizes, and URLs. For example Receiving objects: 50% (404/808) or a host …:4040 matches "404". Because noRetry is evaluated first, such a match turns a transient network failure into a permanent failure, and the job fails without any retry.

Match the codes together with their surrounding git or curl text.

🐛 Proposed fix
 	noRetry := []string{
 		"not found",
 		"repository not found",
 		"does not appear to be a git repository",
 		"authentication failed",
 		"access denied",
 		"permission denied",
 		"already exists",
 		"denied",
 		"invalid refspec",
 		"couldn't find remote ref",
-		"401",
-		"403",
-		"404",
+		"error: 401",
+		"error: 403",
+		"error: 404",
+		"http 401",
+		"http 403",
+		"http 404",
 	}
@@
-		"502",
-		"503",
-		"504",
+		"error: 502",
+		"error: 503",
+		"error: 504",
+		"http 502",
+		"http 503",
+		"http 504",
 		"http error",
 	}

The existing test cases in internal/jobs/retry_test.go still pass with this change, because they use the HTTP 502 and returned error: 504 forms.

Also applies to: 62-64

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/retry.go` around lines 34 - 36, Update isRetryable’s noRetry
status matching for 401, 403, 404, 502, 503, and 504 to require surrounding git
or curl HTTP-error text rather than matching bare numeric substrings. Preserve
recognition of existing forms such as “HTTP 502” and “returned error: 504”,
while preventing counters, ports, URLs, or other incidental numbers from being
classified as permanent failures.

Comment thread internal/jobs/retry.go
Comment on lines +102 to +104
func runGitCapture(dir string, progress func(string), args ...string) error {
cmd := exec.Command("git", args...)
cmd.Dir = dir

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Force a stable locale for the git commands. isRetryable matches English substrings such as "could not resolve host" and "repository not found". Neither command builder sets the child environment, so git emits localized messages when the user's environment defines a non-English locale. Every retry decision then falls through to return false, and transient network failures fail the job immediately. The shared root cause is the missing environment control on the two exec.Command("git", …) builders.

  • internal/jobs/retry.go#L102-L104: set cmd.Env = append(os.Environ(), "LC_ALL=C", "LANG=C") in runGitCapture before cmd.Start().
  • internal/jobs/clone.go#L153-L156: set the same cmd.Env in gitOutput before cmd.CombinedOutput(), so the wrapped stderr message that reaches isRetryable is also in English.
📍 Affects 2 files
  • internal/jobs/retry.go#L102-L104 (this comment)
  • internal/jobs/clone.go#L153-L156
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/retry.go` around lines 102 - 104, Force a stable English locale
for both git command builders by setting cmd.Env to the existing process
environment plus LC_ALL=C and LANG=C. Apply this in runGitCapture in
internal/jobs/retry.go at lines 102-104 before cmd.Start(), and in gitOutput in
internal/jobs/clone.go at lines 153-156 before cmd.CombinedOutput().

Comment thread internal/jobs/retry.go
Comment on lines +129 to +133
if err := scanner.Err(); err != nil {
// Si el stderr deja de leerse, git puede bloquearse en el pipe: se aborta.
_ = cmd.Process.Kill()
return err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Call cmd.Wait() after killing the process.

If scanner.Err() returns an error, the code kills the child and returns without calling cmd.Wait(). os/exec requires Wait to reap the child and release the pipe held by StderrPipe. Without it the killed git process stays as a zombie and the pipe file descriptor leaks for the lifetime of the caller. Run and the retry loop can invoke this repeatedly, so the leak accumulates across retries.

🐛 Proposed fix
 	if err := scanner.Err(); err != nil {
 		// Si el stderr deja de leerse, git puede bloquearse en el pipe: se aborta.
 		_ = cmd.Process.Kill()
+		_ = cmd.Wait()
 		return err
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := scanner.Err(); err != nil {
// Si el stderr deja de leerse, git puede bloquearse en el pipe: se aborta.
_ = cmd.Process.Kill()
return err
}
if err := scanner.Err(); err != nil {
// Si el stderr deja de leerse, git puede bloquearse en el pipe: se aborta.
_ = cmd.Process.Kill()
_ = cmd.Wait()
return err
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/retry.go` around lines 129 - 133, Update the scanner.Err()
failure path to call cmd.Wait() after cmd.Process.Kill() and before returning
the scanner error, ensuring the child process is reaped and StderrPipe resources
are released.

Comment thread internal/jobs/run.go
Comment on lines +19 to +24
if job.State == StateCompleted || job.State == StateFailed {
return fmt.Errorf("el job %d ya terminó (%s)", id, job.State)
}
if err := s.SetState(id, StateRunning); err != nil {
return err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Claim the job atomically to prevent two runners on the same job.

The check at line 19 rejects StateCompleted and StateFailed, but it accepts StateRunning. The check and the SetState at line 22 are also separate statements, so two concurrent git gost run <id> invocations can both pass the check. Both then execute git in the same target directory, which produces index.lock failures or a corrupted working tree.

Replace the read-then-write sequence with a single conditional UPDATE and verify RowsAffected.

🔒 Proposed fix

Add a claim method to internal/jobs/store.go:

// Claim marca el job como running solo si no está terminado ni en ejecución.
func (s *Store) Claim(id int64) (bool, error) {
	res, err := s.db.Exec(
		`UPDATE jobs SET state = ?, updated_at = ?
		 WHERE id = ? AND state NOT IN (?, ?, ?)`,
		StateRunning, time.Now().UTC().Format(time.RFC3339Nano),
		id, StateRunning, StateCompleted, StateFailed)
	if err != nil {
		return false, err
	}
	n, err := res.RowsAffected()
	return n == 1, err
}

Then use it in Run:

-	if job.State == StateCompleted || job.State == StateFailed {
-		return fmt.Errorf("el job %d ya terminó (%s)", id, job.State)
-	}
-	if err := s.SetState(id, StateRunning); err != nil {
-		return err
-	}
+	claimed, err := s.Claim(id)
+	if err != nil {
+		return err
+	}
+	if !claimed {
+		return fmt.Errorf("el job %d no es ejecutable (%s)", id, job.State)
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if job.State == StateCompleted || job.State == StateFailed {
return fmt.Errorf("el job %d ya terminó (%s)", id, job.State)
}
if err := s.SetState(id, StateRunning); err != nil {
return err
}
claimed, err := s.Claim(id)
if err != nil {
return err
}
if !claimed {
return fmt.Errorf("el job %d no es ejecutable (%s)", id, job.State)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/jobs/run.go` around lines 19 - 24, Replace the separate state
validation and SetState calls in Run with an atomic claim through a new
Store.Claim method. Implement Claim in internal/jobs/store.go using a
conditional UPDATE that excludes Running, Completed, and Failed states, then
verify RowsAffected; have Run return the existing terminal-state error when the
claim is unsuccessful and propagate database errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants