Module: Chocomint::EditGitHandlers
- Included in:
- Server
- Defined in:
- lib/chocomint/edit_git_handlers.rb
Overview
/edit AI エディタの Git 操作ハンドラ群 (Server に include して使う)。
提供機能:
GET /edit/git/status 初期化有無・ブランチ名・ステージ済み/未ステージのファイル一覧
POST /edit/git/init git init
POST /edit/git/stage ファイルをステージ (git add)
POST /edit/git/unstage ステージ取り消し (git restore --staged)
POST /edit/git/stage_all すべての変更をステージ (git add -A)
POST /edit/git/unstage_all すべてのステージを取り消し (git restore --staged .)
POST /edit/git/discard 変更を破棄 (追跡済みは checkout、未追跡は削除)
POST /edit/git/commit コミット
POST /edit/git/push push (リモート未設定なら reason: "no_remote" 付き 409 を返す)
POST /edit/git/remote origin の URL を設定して push (未設定/差し替えの両対応)
GET /edit/git/branches ブランチ一覧 (現在のブランチも含む)
POST /edit/git/checkout ブランチ切り替え/新規作成
すべて edit_root (workspace ルート) を cwd にして git コマンドを実行する。 workspace が git 管理下でない場合は status で初期化を促す。
Defined Under Namespace
Classes: GitCommandNotFoundStatus
Constant Summary collapse
- GIT_NOT_FOUND =
GitCommandNotFoundStatus.new.freeze
Instance Method Summary collapse
- #discard_paths(paths) ⇒ Object
-
#git_body_paths(req) ⇒ Object
JSON ボディから "paths" (配列) または "path" (単一) を取り出す。.
-
#git_capture(args) ⇒ Object
成功時の stdout を行配列で返す (失敗時は空配列)。.
- #git_current_branch ⇒ Object
-
#git_remote_url ⇒ Object
origin の URL (未設定なら nil)。.
-
#git_repo? ⇒ Boolean
edit_root 自身が git リポジトリのトップレベルである場合のみ true を返す。 workspace が祖先ディレクトリの .git を継承しているだけ (workspace 自体は 管理下に無い) だと git コマンドはその祖先リポジトリに対して動いてしまう ため、show-toplevel が edit_root と一致するかまで確認する。.
-
#git_run(args) ⇒ Object
git コマンドを edit_root で実行し [stdout, stderr, Process::Status] を返す。.
- #git_tracked?(path) ⇒ Boolean
-
#handle_edit_git_branches(req, res) ⇒ Object
---- ブランチ ---------------------------------------------------------------.
- #handle_edit_git_checkout(req, res) ⇒ Object
-
#handle_edit_git_commit(req, res) ⇒ Object
---- コミット ---------------------------------------------------------------.
-
#handle_edit_git_discard(req, res) ⇒ Object
---- 変更の破棄 ---------------------------------------------------------------.
-
#handle_edit_git_init(req, res) ⇒ Object
---- 初期化 ---------------------------------------------------------------.
-
#handle_edit_git_push(req, res) ⇒ Object
---- push ---------------------------------------------------------------.
-
#handle_edit_git_remote_set(req, res) ⇒ Object
---- リモート ---------------------------------------------------------------.
-
#handle_edit_git_stage(req, res) ⇒ Object
---- ステージ / 取り消し ---------------------------------------------------.
- #handle_edit_git_stage_all(req, res) ⇒ Object
-
#handle_edit_git_status(req, res) ⇒ Object
---- ステータス ---------------------------------------------------------.
- #handle_edit_git_unstage(req, res) ⇒ Object
- #handle_edit_git_unstage_all(req, res) ⇒ Object
- #normalize_path(path) ⇒ Object
-
#parse_git_status(lines) ⇒ Object
git status --porcelain=v1 の各行 (XY PATH) をステージ済み/未ステージに振り分ける。 X: インデックス(ステージ)の状態、Y: 作業ツリーの状態。 "??" (未追跡) は unstaged 扱いにする。.
-
#push_target_missing?(message) ⇒ Boolean
push 先が無いことを示すエラー文言か判定する (リモート未設定 / 上流ブランチ未設定の両方)。.
-
#require_git_repo! ⇒ Object
edit_root がリポジトリのトップレベルでなければ拒否する。init/checkout 以外の 全操作の先頭で呼び、祖先ディレクトリの .git を誤って操作しないようにする。.
-
#same_path?(git_path, ruby_path) ⇒ Boolean
git の出力 (常に "/" 区切り) と Ruby 側の絶対パスを比較する。 Windows はドライブレターの大小文字揺れがあるため大文字小文字を無視する。.
-
#unstage_paths(paths) ⇒ Object
ステージ済みの変更を取り消す。
git restore --stagedは初回コミット前 (HEAD が無い) だと "fatal: could not resolve HEAD" で失敗するため、 その場合は index をリセットするgit resetにフォールバックする。. -
#with_git(res) ⇒ Object
Git 操作系ハンドラ共通の例外→HTTP 変換。.
Instance Method Details
#discard_paths(paths) ⇒ Object
159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/chocomint/edit_git_handlers.rb', line 159 def discard_paths(paths) tracked, untracked = paths.partition { |p| git_tracked?(p) } unless tracked.empty? unstage_paths(tracked) _out, err, status = git_run(["restore", "--"] + tracked) raise Chocomint::Error, (err.empty? ? "変更の破棄に失敗しました" : err) unless status.success? end untracked.each do |p| abs = @path_guard.resolve(p) FileUtils.rm_rf(abs) end end |
#git_body_paths(req) ⇒ Object
JSON ボディから "paths" (配列) または "path" (単一) を取り出す。
372 373 374 375 376 377 378 |
# File 'lib/chocomint/edit_git_handlers.rb', line 372 def git_body_paths(req) body = JSON.parse(req.body.to_s) paths = Array(body["paths"] || body["path"]).map(&:to_s).reject(&:empty?) raise Chocomint::Error, "path(s) required" if paths.empty? paths end |
#git_capture(args) ⇒ Object
成功時の stdout を行配列で返す (失敗時は空配列)。
366 367 368 369 |
# File 'lib/chocomint/edit_git_handlers.rb', line 366 def git_capture(args) out, _err, status = git_run(args) status.success? ? out.each_line.map(&:chomp) : [] end |
#git_current_branch ⇒ Object
347 348 349 350 |
# File 'lib/chocomint/edit_git_handlers.rb', line 347 def git_current_branch out, _err, status = git_run(%w[branch --show-current]) status.success? ? out.strip : nil end |
#git_remote_url ⇒ Object
origin の URL (未設定なら nil)。
353 354 355 356 |
# File 'lib/chocomint/edit_git_handlers.rb', line 353 def git_remote_url out, _err, status = git_run(%w[remote get-url origin]) status.success? ? out.strip : nil end |
#git_repo? ⇒ Boolean
edit_root 自身が git リポジトリのトップレベルである場合のみ true を返す。 workspace が祖先ディレクトリの .git を継承しているだけ (workspace 自体は 管理下に無い) だと git コマンドはその祖先リポジトリに対して動いてしまう ため、show-toplevel が edit_root と一致するかまで確認する。
330 331 332 333 334 335 |
# File 'lib/chocomint/edit_git_handlers.rb', line 330 def git_repo? out, _err, status = git_run(%w[rev-parse --show-toplevel]) return false unless status.success? same_path?(out.strip, edit_root) end |
#git_run(args) ⇒ Object
git コマンドを edit_root で実行し [stdout, stderr, Process::Status] を返す。
359 360 361 362 363 |
# File 'lib/chocomint/edit_git_handlers.rb', line 359 def git_run(args) Open3.capture3("git", *args, chdir: edit_root) rescue Errno::ENOENT ["", "git コマンドが見つかりません", GIT_NOT_FOUND] end |
#git_tracked?(path) ⇒ Boolean
184 185 186 187 |
# File 'lib/chocomint/edit_git_handlers.rb', line 184 def git_tracked?(path) _out, _err, status = git_run(["ls-files", "--error-unmatch", "--", path]) status.success? end |
#handle_edit_git_branches(req, res) ⇒ Object
---- ブランチ ---------------------------------------------------------------
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/chocomint/edit_git_handlers.rb', line 270 def handle_edit_git_branches(req, res) return edit_method_guard(res) unless req.request_method == "GET" unless git_repo? return edit_json(res, 400, "error" => "git 管理下ではありません") end out = git_capture(%w[branch --list]) branches = out.map { |l| l.sub(/\A\*?\s*/, "").strip }.reject(&:empty?) current = git_current_branch # コミットが一つも無いブランチ (init 直後など) は `git branch --list` に # 現れないため、現在のブランチが漏れていれば一覧に加えて必ず選択肢に見せる。 branches.unshift(current) if current && !branches.include?(current) edit_json(res, 200, "branches" => branches, "current" => current) rescue Chocomint::Error => e edit_json(res, 500, "error" => e.) end |
#handle_edit_git_checkout(req, res) ⇒ Object
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/chocomint/edit_git_handlers.rb', line 288 def handle_edit_git_checkout(req, res) return edit_method_guard(res) unless req.request_method == "POST" with_git(res) do require_git_repo! body = JSON.parse(req.body.to_s) branch = body["branch"].to_s.strip create = !!body["create"] raise Chocomint::Error, "branch required" if branch.empty? args = create ? ["checkout", "-b", branch] : ["checkout", branch] out, err, status = git_run(args) unless status.success? = err.empty? ? out : err # ブランチ参照が見つからない場合、原因は「名前が間違っている」か # 「切替先にまだコミットが一つも無く Git 上に実体化していない」のどちらか # (エラー文言だけでは判別できない) なので、両方の可能性を伝える。 if !create && (.include?("invalid reference") || .include?("did not match")) += "\n(ブランチ名の誤り、またはコミットが一つも無い空のブランチのため" \ "切り替えられていない可能性があります。空のブランチは最初のコミットを" \ "するまで Git 上に実体化しません。)" end raise Chocomint::Error, end edit_json(res, 200, "branch" => git_current_branch) end end |
#handle_edit_git_commit(req, res) ⇒ Object
---- コミット ---------------------------------------------------------------
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/chocomint/edit_git_handlers.rb', line 191 def handle_edit_git_commit(req, res) return edit_method_guard(res) unless req.request_method == "POST" with_git(res) do require_git_repo! body = JSON.parse(req.body.to_s) = body["message"].to_s.strip raise Chocomint::Error, "コミットメッセージが必要です" if .empty? out, err, status = git_run(["commit", "-m", ]) raise Chocomint::Error, (err.empty? ? out : err) unless status.success? edit_json(res, 200, "committed" => true, "branch" => git_current_branch) end end |
#handle_edit_git_discard(req, res) ⇒ Object
---- 変更の破棄 ---------------------------------------------------------------
追跡済みファイルの変更は git restore で元に戻し、未追跡ファイルはワークツリーから
削除する (git 管理下に無いため restore では戻せない)。ディレクトリ丸ごとの未追跡も
同様に削除する。ステージ済みの変更はまず取り消してから破棄する (add 済みでも作業ツリーを戻す)。
148 149 150 151 152 153 154 155 156 157 |
# File 'lib/chocomint/edit_git_handlers.rb', line 148 def handle_edit_git_discard(req, res) return edit_method_guard(res) unless req.request_method == "POST" with_git(res) do require_git_repo! paths = git_body_paths(req) discard_paths(paths) edit_json(res, 200, "discarded" => paths) end end |
#handle_edit_git_init(req, res) ⇒ Object
---- 初期化 ---------------------------------------------------------------
76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/chocomint/edit_git_handlers.rb', line 76 def handle_edit_git_init(req, res) return edit_method_guard(res) unless req.request_method == "POST" if git_repo? return edit_json(res, 400, "error" => "既に git 管理下です") end out, err, status = git_run(%w[init]) return edit_json(res, 500, "error" => err.empty? ? out : err) unless status.success? edit_json(res, 200, "initialized" => true, "branch" => git_current_branch) rescue Chocomint::Error => e edit_json(res, 500, "error" => e.) end |
#handle_edit_git_push(req, res) ⇒ Object
---- push ---------------------------------------------------------------
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/chocomint/edit_git_handlers.rb', line 209 def handle_edit_git_push(req, res) return edit_method_guard(res) unless req.request_method == "POST" unless git_repo? return edit_json(res, 400, "error" => "git 管理下ではありません") end out, err, status = git_run(%w[push]) return edit_json(res, 200, "pushed" => true, "output" => (err.empty? ? out : err)) if status.success? = err.empty? ? out : err # リモート/上流ブランチが未設定だと push は失敗する。フロントはこの reason で # 「リモート URL を入力させるダイアログ」に切り替える (エラーダイアログではなく)。 if push_target_missing?() return edit_json(res, 409, "error" => , "reason" => "no_remote") end edit_json(res, 400, "error" => ) rescue JSON::ParserError => e edit_json(res, 400, "error" => "invalid JSON: #{e.}") rescue Chocomint::Error => e edit_json(res, 500, "error" => e.) end |
#handle_edit_git_remote_set(req, res) ⇒ Object
---- リモート ---------------------------------------------------------------
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/chocomint/edit_git_handlers.rb', line 241 def handle_edit_git_remote_set(req, res) return edit_method_guard(res) unless req.request_method == "POST" with_git(res) do require_git_repo! body = JSON.parse(req.body.to_s) url = body["url"].to_s.strip raise Chocomint::Error, "リモート URL を入力してください" if url.empty? # 既に origin が設定されていれば URL を差し替え、無ければ新規に追加する。 if git_remote_url out, err, status = git_run(%w[remote set-url origin] + [url]) else out, err, status = git_run(%w[remote add origin] + [url]) end raise Chocomint::Error, (err.empty? ? out : err) unless status.success? # 上流ブランチも合わせて設定しつつ push する (初回 push は -u が無いと # "has no upstream branch" で失敗するため)。 branch = git_current_branch out, err, status = git_run(["push", "-u", "origin", branch]) raise Chocomint::Error, (err.empty? ? out : err) unless status.success? edit_json(res, 200, "pushed" => true, "remote_url" => url, "output" => (err.empty? ? out : err)) end end |
#handle_edit_git_stage(req, res) ⇒ Object
---- ステージ / 取り消し ---------------------------------------------------
93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/chocomint/edit_git_handlers.rb', line 93 def handle_edit_git_stage(req, res) return edit_method_guard(res) unless req.request_method == "POST" with_git(res) do require_git_repo! paths = git_body_paths(req) out, err, status = git_run(["add", "--"] + paths) raise Chocomint::Error, (err.empty? ? out : err) unless status.success? edit_json(res, 200, "staged" => paths) end end |
#handle_edit_git_stage_all(req, res) ⇒ Object
119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/chocomint/edit_git_handlers.rb', line 119 def handle_edit_git_stage_all(req, res) return edit_method_guard(res) unless req.request_method == "POST" with_git(res) do require_git_repo! out, err, status = git_run(%w[add -A]) raise Chocomint::Error, (err.empty? ? out : err) unless status.success? edit_json(res, 200, "staged_all" => true) end end |
#handle_edit_git_status(req, res) ⇒ Object
---- ステータス ---------------------------------------------------------
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/chocomint/edit_git_handlers.rb', line 29 def handle_edit_git_status(req, res) return edit_method_guard(res) unless req.request_method == "GET" unless git_repo? return edit_json(res, 200, "initialized" => false) end branch = git_current_branch # --untracked-files=all: 未追跡ディレクトリを 1 エントリにまとめず中のファイルを # 個別に列挙させる。無指定だと "logs/" のようにディレクトリ単位でしか返らず、 # + ボタンでファイル単位のステージができなくなる。 porcelain = git_capture(%w[status --porcelain=v1 --untracked-files=all]) staged, unstaged = parse_git_status(porcelain) edit_json(res, 200, "initialized" => true, "branch" => branch, "staged" => staged, "unstaged" => unstaged, "remote_url" => git_remote_url) rescue Chocomint::Error => e edit_json(res, 500, "error" => e.) end |
#handle_edit_git_unstage(req, res) ⇒ Object
106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/chocomint/edit_git_handlers.rb', line 106 def handle_edit_git_unstage(req, res) return edit_method_guard(res) unless req.request_method == "POST" with_git(res) do require_git_repo! paths = git_body_paths(req) out, err, status = unstage_paths(paths) raise Chocomint::Error, (err.empty? ? out : err) unless status.success? edit_json(res, 200, "unstaged" => paths) end end |
#handle_edit_git_unstage_all(req, res) ⇒ Object
131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/chocomint/edit_git_handlers.rb', line 131 def handle_edit_git_unstage_all(req, res) return edit_method_guard(res) unless req.request_method == "POST" with_git(res) do require_git_repo! out, err, status = unstage_paths(%w[.]) raise Chocomint::Error, (err.empty? ? out : err) unless status.success? edit_json(res, 200, "unstaged_all" => true) end end |
#normalize_path(path) ⇒ Object
343 344 345 |
# File 'lib/chocomint/edit_git_handlers.rb', line 343 def normalize_path(path) File.(path).tr("\\", "/").downcase end |
#parse_git_status(lines) ⇒ Object
git status --porcelain=v1 の各行 (XY PATH) をステージ済み/未ステージに振り分ける。 X: インデックス(ステージ)の状態、Y: 作業ツリーの状態。 "??" (未追跡) は unstaged 扱いにする。
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/chocomint/edit_git_handlers.rb', line 56 def parse_git_status(lines) staged = [] unstaged = [] lines.each do |line| next if line.length < 4 x = line[0] y = line[1] path = line[3..].to_s.tr("\\", "/") # rename は "R old -> new" の形式。表示・操作対象は new 側のパス。 path = path.split(" -> ").last if path.include?(" -> ") staged << { "path" => path, "status" => x } if x != " " && x != "?" unstaged << { "path" => path, "status" => y } if y != " " || x == "?" end [staged, unstaged] end |
#push_target_missing?(message) ⇒ Boolean
push 先が無いことを示すエラー文言か判定する (リモート未設定 / 上流ブランチ未設定の両方)。
234 235 236 237 |
# File 'lib/chocomint/edit_git_handlers.rb', line 234 def push_target_missing?() .include?("No configured push destination") || .include?("has no upstream branch") end |
#require_git_repo! ⇒ Object
edit_root がリポジトリのトップレベルでなければ拒否する。init/checkout 以外の 全操作の先頭で呼び、祖先ディレクトリの .git を誤って操作しないようにする。
382 383 384 |
# File 'lib/chocomint/edit_git_handlers.rb', line 382 def require_git_repo! raise Chocomint::Error, "git 管理下ではありません" unless git_repo? end |
#same_path?(git_path, ruby_path) ⇒ Boolean
git の出力 (常に "/" 区切り) と Ruby 側の絶対パスを比較する。 Windows はドライブレターの大小文字揺れがあるため大文字小文字を無視する。
339 340 341 |
# File 'lib/chocomint/edit_git_handlers.rb', line 339 def same_path?(git_path, ruby_path) normalize_path(git_path) == normalize_path(ruby_path) end |
#unstage_paths(paths) ⇒ Object
ステージ済みの変更を取り消す。git restore --staged は初回コミット前
(HEAD が無い) だと "fatal: could not resolve HEAD" で失敗するため、
その場合は index をリセットする git reset にフォールバックする。
177 178 179 180 181 182 |
# File 'lib/chocomint/edit_git_handlers.rb', line 177 def unstage_paths(paths) out, err, status = git_run(["restore", "--staged", "--"] + paths) return [out, err, status] if status.success? git_run(["reset", "--"] + paths) end |
#with_git(res) ⇒ Object
Git 操作系ハンドラ共通の例外→HTTP 変換。
387 388 389 390 391 392 393 394 395 |
# File 'lib/chocomint/edit_git_handlers.rb', line 387 def with_git(res) yield rescue JSON::ParserError => e edit_json(res, 400, "error" => "invalid JSON: #{e.}") rescue Chocomint::Error => e edit_json(res, 400, "error" => e.) rescue SystemCallError => e edit_json(res, 500, "error" => e.) end |