Module: Clacky::Server::GitPanel
- Defined in:
- lib/clacky/server/git_panel.rb
Overview
Read-mostly git operations scoped to a session’s working directory, backing the official “git” WebUI panel. Commands run with explicit argv (no shell), so user-supplied values (paths, messages) cannot inject. Write operations are limited to a guarded ‘commit`; history-rewriting / remote-mutating commands are never exposed here.
Class Method Summary collapse
-
.branches(dir) ⇒ Object
- { name:, current: bool }
-
from ‘git branch`.
-
.commit(dir, message:, files:) ⇒ Object
Stage ‘files` (relative paths) and commit with `message`.
-
.diff(dir, file: nil) ⇒ Object
Unified diff.
-
.git(dir, *args) ⇒ Object
Run a git subcommand in ‘dir` with argv-style args (no shell).
-
.log(dir, limit: 50) ⇒ Object
Recent commits: [{ hash:, short:, author:, date:, subject: }].
-
.repo?(dir) ⇒ Boolean
Whether ‘dir` is inside a git work tree.
-
.status(dir) ⇒ Object
{ branch:, ahead:, behind:, files: [{ path:, x:, y:, staged:, untracked: }] } Parsed from ‘git status –porcelain=v2 –branch`.
Class Method Details
.branches(dir) ⇒ Object
- { name:, current: bool }
-
from ‘git branch`.
84 85 86 87 88 89 90 91 92 93 |
# File 'lib/clacky/server/git_panel.rb', line 84 def branches(dir) out, _err, ok = git(dir, "branch", "--format=%(refname:short)%00%(HEAD)") return [] unless ok out.each_line.filter_map do |line| name, head = line.chomp.split("\x00") next unless name && !name.empty? { name: name, current: head == "*" } end end |
.commit(dir, message:, files:) ⇒ Object
Stage ‘files` (relative paths) and commit with `message`. Returns { ok:, error?:, hash? }. Refuses empty message / empty file set. Uses argv so paths/message cannot inject; no –no-verify, no amend.
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/clacky/server/git_panel.rb', line 98 def commit(dir, message:, files:) msg = .to_s.strip paths = Array(files).map(&:to_s).reject(&:empty?) return { ok: false, error: "commit message is required" } if msg.empty? return { ok: false, error: "no files selected" } if paths.empty? _out, add_err, add_ok = git(dir, "add", "--", *paths) return { ok: false, error: "git add failed: #{add_err.strip}" } unless add_ok _out, c_err, c_ok = git(dir, "commit", "-m", msg, "--", *paths) return { ok: false, error: "git commit failed: #{c_err.strip}" } unless c_ok head, _err, _ok = git(dir, "rev-parse", "--short", "HEAD") { ok: true, hash: head.strip } end |
.diff(dir, file: nil) ⇒ Object
Unified diff. ‘file` (optional, relative) limits to one path; omitted = whole working tree (tracked changes). `–` guards path from being read as an option.
62 63 64 65 66 67 |
# File 'lib/clacky/server/git_panel.rb', line 62 def diff(dir, file: nil) args = ["diff"] args += ["--", file] if file && !file.empty? out, _err, _ok = git(dir, *args) out end |
.git(dir, *args) ⇒ Object
Run a git subcommand in ‘dir` with argv-style args (no shell). Returns [stdout, stderr, success_bool]. Never raises on git failure.
17 18 19 20 21 22 |
# File 'lib/clacky/server/git_panel.rb', line 17 def git(dir, *args) out, err, status = Open3.capture3("git", "-C", dir.to_s, *args) [out, err, status.success?] rescue StandardError => e ["", e., false] end |
.log(dir, limit: 50) ⇒ Object
Recent commits: [{ hash:, short:, author:, date:, subject: }].
70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/clacky/server/git_panel.rb', line 70 def log(dir, limit: 50) limit = limit.to_i.clamp(1, 200) fmt = "%H%x1f%h%x1f%an%x1f%ad%x1f%s" out, _err, ok = git(dir, "log", "-n", limit.to_s, "--date=short", "--pretty=format:#{fmt}") return [] unless ok out.each_line.filter_map do |line| h, short, , date, subject = line.chomp.split("\x1f") next unless h { hash: h, short: short, author: , date: date, subject: subject } end end |
.repo?(dir) ⇒ Boolean
Whether ‘dir` is inside a git work tree.
25 26 27 28 |
# File 'lib/clacky/server/git_panel.rb', line 25 def repo?(dir) out, _err, ok = git(dir, "rev-parse", "--is-inside-work-tree") ok && out.strip == "true" end |
.status(dir) ⇒ Object
{ branch:, ahead:, behind:, files: [{ path:, x:, y:, staged:, untracked: }] } Parsed from ‘git status –porcelain=v2 –branch`.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/clacky/server/git_panel.rb', line 32 def status(dir) out, _err, ok = git(dir, "status", "--porcelain=v2", "--branch") return { branch: nil, files: [] } unless ok branch = nil ahead = behind = 0 files = [] out.each_line do |line| line = line.chomp if line.start_with?("# branch.head ") branch = line.sub("# branch.head ", "") elsif line.start_with?("# branch.ab ") m = line.match(/\+(\d+) -(\d+)/) ahead, behind = m[1].to_i, m[2].to_i if m elsif line.start_with?("1 ", "2 ") xy = line.split(" ")[1] path = line.split(" ", 9).last files << { path: path, x: xy[0], y: xy[1], staged: xy[0] != ".", untracked: false } elsif line.start_with?("? ") files << { path: line.sub("? ", ""), x: "?", y: "?", staged: false, untracked: true } end end { branch: branch, ahead: ahead, behind: behind, files: files } end |