chocomint 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +224 -0
- data/THIRD_PARTY_LICENSES.md +45 -0
- data/bin/chocomint +37 -0
- data/bin/chocomint-console-worker +35 -0
- data/bin/chocomint-server +43 -0
- data/config/config.yml +37 -0
- data/lib/chocomint/config.rb +74 -0
- data/lib/chocomint/console/conpty.rb +296 -0
- data/lib/chocomint/console/server.rb +164 -0
- data/lib/chocomint/console/worker.rb +88 -0
- data/lib/chocomint/console/worker_process.rb +254 -0
- data/lib/chocomint/edit_git_handlers.rb +397 -0
- data/lib/chocomint/edit_handlers.rb +696 -0
- data/lib/chocomint/edit_html.rb +9 -0
- data/lib/chocomint/errors.rb +18 -0
- data/lib/chocomint/factory.rb +145 -0
- data/lib/chocomint/llm/base_client.rb +113 -0
- data/lib/chocomint/llm/chat_client.rb +206 -0
- data/lib/chocomint/llm/primary_client.rb +320 -0
- data/lib/chocomint/llm/verifier_client.rb +127 -0
- data/lib/chocomint/logger/sqlite_logger.rb +245 -0
- data/lib/chocomint/orchestrator.rb +175 -0
- data/lib/chocomint/planner.rb +241 -0
- data/lib/chocomint/security/path_guard.rb +84 -0
- data/lib/chocomint/server.rb +863 -0
- data/lib/chocomint/tools/append_file.rb +50 -0
- data/lib/chocomint/tools/base.rb +88 -0
- data/lib/chocomint/tools/delete_file.rb +42 -0
- data/lib/chocomint/tools/edit.rb +65 -0
- data/lib/chocomint/tools/fetch_url.rb +156 -0
- data/lib/chocomint/tools/file_info.rb +54 -0
- data/lib/chocomint/tools/glob.rb +58 -0
- data/lib/chocomint/tools/grep.rb +78 -0
- data/lib/chocomint/tools/list_dir.rb +41 -0
- data/lib/chocomint/tools/ls.rb +51 -0
- data/lib/chocomint/tools/make_dir.rb +44 -0
- data/lib/chocomint/tools/read_file.rb +45 -0
- data/lib/chocomint/tools/registry.rb +29 -0
- data/lib/chocomint/tools/run_command.rb +96 -0
- data/lib/chocomint/tools/shell.rb +169 -0
- data/lib/chocomint/tools/write_file.rb +54 -0
- data/lib/chocomint/validator/machine_validator.rb +65 -0
- data/lib/chocomint/validator/semantic_validator.rb +52 -0
- data/lib/chocomint.rb +51 -0
- data/public/edit/edit.css +414 -0
- data/public/edit/edit.js +2137 -0
- data/public/edit/index.html +251 -0
- data/public/vendor/monaco/LICENSE.txt +23 -0
- data/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- data/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- data/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- data/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- data/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- data/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- data/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- data/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- data/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- data/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- data/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- data/public/vendor/monaco/vs/editor/editor.main.css +8 -0
- data/public/vendor/monaco/vs/editor/editor.main.js +798 -0
- data/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
- data/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- data/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- data/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- data/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- data/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- data/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- data/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- data/public/vendor/monaco/vs/loader.js +11 -0
- data/public/vendor/monaco/vs/nls.messages.de.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.es.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.fr.js +19 -0
- data/public/vendor/monaco/vs/nls.messages.it.js +19 -0
- data/public/vendor/monaco/vs/nls.messages.ja.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.ko.js +19 -0
- data/public/vendor/monaco/vs/nls.messages.ru.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
- data/public/vendor/xterm/LICENSE +25 -0
- data/public/vendor/xterm/addon-fit.js +2 -0
- data/public/vendor/xterm/xterm.css +218 -0
- data/public/vendor/xterm/xterm.js +2 -0
- metadata +318 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
require "json"
|
|
5
|
+
require "base64"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require_relative "../errors"
|
|
8
|
+
|
|
9
|
+
module Chocomint
|
|
10
|
+
module Console
|
|
11
|
+
# コンソールワーカーを「新規コンソール付き」で spawn し、名前付きパイプで
|
|
12
|
+
# 双方向にやり取りするための橋渡し。
|
|
13
|
+
#
|
|
14
|
+
# なぜ名前付きパイプか (重要): ConPTY はコンソール付きプロセスからしか
|
|
15
|
+
# 正しく動かない (chocomint-conpty メモの制約)。CREATE_NEW_CONSOLE で新規
|
|
16
|
+
# コンソールを与える必要があるが、STARTF_USESTDHANDLES で stdout をパイプへ
|
|
17
|
+
# リダイレクトすると新規コンソールとの関連付けが壊れ、ConPTY 出力が 16 バイトの
|
|
18
|
+
# 初期シーケンスで止まる (実測で確認)。そこで stdio は触らず、制御通信は
|
|
19
|
+
# 独立した名前付きパイプで行う。ワーカーはコンソールを完全に保持できる。
|
|
20
|
+
class WorkerProcess
|
|
21
|
+
module Native
|
|
22
|
+
extend FFI::Library
|
|
23
|
+
ffi_lib "kernel32"
|
|
24
|
+
ffi_convention :stdcall
|
|
25
|
+
|
|
26
|
+
class ProcessInformation < FFI::Struct
|
|
27
|
+
layout :hProcess, :pointer, :hThread, :pointer, :dwProcessId, :uint32, :dwThreadId, :uint32
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class StartupInfoW < FFI::Struct
|
|
31
|
+
layout(
|
|
32
|
+
:cb, :uint32, :lpReserved, :pointer, :lpDesktop, :pointer, :lpTitle, :pointer,
|
|
33
|
+
:dwX, :uint32, :dwY, :uint32, :dwXSize, :uint32, :dwYSize, :uint32,
|
|
34
|
+
:dwXCountChars, :uint32, :dwYCountChars, :uint32, :dwFillAttribute, :uint32,
|
|
35
|
+
:dwFlags, :uint32, :wShowWindow, :uint16, :cbReserved2, :uint16,
|
|
36
|
+
:lpReserved2, :pointer, :hStdInput, :pointer, :hStdOutput, :pointer, :hStdError, :pointer
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
CREATE_NEW_CONSOLE = 0x00000010
|
|
41
|
+
STARTF_USESHOWWINDOW = 0x00000001
|
|
42
|
+
SW_HIDE = 0
|
|
43
|
+
STILL_ACTIVE = 259
|
|
44
|
+
|
|
45
|
+
PIPE_ACCESS_DUPLEX = 0x00000003
|
|
46
|
+
PIPE_TYPE_BYTE = 0x00000000
|
|
47
|
+
PIPE_WAIT = 0x00000000
|
|
48
|
+
PIPE_REJECT_REMOTE_CLIENTS = 0x00000008
|
|
49
|
+
INVALID_HANDLE_VALUE = FFI::Pointer.new(-1)
|
|
50
|
+
|
|
51
|
+
attach_function :CreateNamedPipeW,
|
|
52
|
+
%i[pointer uint32 uint32 uint32 uint32 uint32 uint32 pointer], :pointer
|
|
53
|
+
attach_function :ConnectNamedPipe, %i[pointer pointer], :int32
|
|
54
|
+
attach_function :DisconnectNamedPipe, [:pointer], :int32
|
|
55
|
+
attach_function :CreateProcessW,
|
|
56
|
+
%i[pointer pointer pointer pointer int32 uint32
|
|
57
|
+
pointer pointer pointer pointer], :int32
|
|
58
|
+
attach_function :ReadFile, %i[pointer pointer uint32 pointer pointer], :int32
|
|
59
|
+
attach_function :WriteFile, %i[pointer pointer uint32 pointer pointer], :int32
|
|
60
|
+
attach_function :PeekNamedPipe, %i[pointer pointer uint32 pointer pointer pointer], :int32
|
|
61
|
+
attach_function :GetExitCodeProcess, %i[pointer pointer], :int32
|
|
62
|
+
attach_function :TerminateProcess, %i[pointer uint32], :int32
|
|
63
|
+
attach_function :CloseHandle, [:pointer], :int32
|
|
64
|
+
attach_function :GetLastError, [], :uint32
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class Error < Chocomint::Error; end
|
|
68
|
+
|
|
69
|
+
def initialize(command:, cols:, rows:, cwd:, ruby_exe: nil, worker_script: nil)
|
|
70
|
+
@command = command
|
|
71
|
+
@cols = cols
|
|
72
|
+
@rows = rows
|
|
73
|
+
@cwd = cwd
|
|
74
|
+
@ruby_exe = ruby_exe || default_ruby_exe
|
|
75
|
+
@worker_script = worker_script || default_worker_script
|
|
76
|
+
# 双方向を 1 本のパイプで共有すると read/write が競合してブロックするため、
|
|
77
|
+
# 方向ごとに 2 本張る (c2w: 親→worker, w2c: worker→親)。
|
|
78
|
+
base = "\\\\.\\pipe\\chocomint-console-#{SecureRandom.hex(8)}"
|
|
79
|
+
@c2w_name = "#{base}-c2w"
|
|
80
|
+
@w2c_name = "#{base}-w2c"
|
|
81
|
+
@open = false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def open?
|
|
85
|
+
@open
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 名前付きパイプを 2 本作り、ワーカーを新規コンソール付きで spawn して接続を待つ。
|
|
89
|
+
def start
|
|
90
|
+
raise Error, "already started" if @open
|
|
91
|
+
|
|
92
|
+
@c2w = create_named_pipe(@c2w_name)
|
|
93
|
+
@w2c = create_named_pipe(@w2c_name)
|
|
94
|
+
spawn_worker
|
|
95
|
+
# ワーカーが両パイプに接続してくるのを待つ (順序はワーカーの open と一致させる)。
|
|
96
|
+
Native.ConnectNamedPipe(@c2w, nil)
|
|
97
|
+
Native.ConnectNamedPipe(@w2c, nil)
|
|
98
|
+
@open = true
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def send_message(hash)
|
|
103
|
+
write_bytes("#{JSON.generate(hash)}\n".b)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def send_input(bytes)
|
|
107
|
+
send_message("type" => "input", "data" => Base64.strict_encode64(bytes.b))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def send_resize(cols, rows)
|
|
111
|
+
send_message("type" => "resize", "cols" => cols, "rows" => rows)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# ワーカーからの JSON 行を 1 つ読む。無ければ nil (ノンブロッキング)。
|
|
115
|
+
def read_message
|
|
116
|
+
line = read_line
|
|
117
|
+
return nil if line.nil? || line.empty?
|
|
118
|
+
|
|
119
|
+
JSON.parse(line)
|
|
120
|
+
rescue JSON::ParserError
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def alive?
|
|
125
|
+
return false unless @proc_handle
|
|
126
|
+
|
|
127
|
+
code = FFI::MemoryPointer.new(:uint32)
|
|
128
|
+
return false if Native.GetExitCodeProcess(@proc_handle, code).zero?
|
|
129
|
+
|
|
130
|
+
code.read(:uint32) == Native::STILL_ACTIVE
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def close
|
|
134
|
+
return unless @open
|
|
135
|
+
|
|
136
|
+
Native.TerminateProcess(@proc_handle, 0) if alive?
|
|
137
|
+
[@c2w, @w2c].each do |h|
|
|
138
|
+
next unless h
|
|
139
|
+
|
|
140
|
+
Native.DisconnectNamedPipe(h)
|
|
141
|
+
Native.CloseHandle(h)
|
|
142
|
+
end
|
|
143
|
+
Native.CloseHandle(@proc_handle) if @proc_handle
|
|
144
|
+
@open = false
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def default_ruby_exe
|
|
151
|
+
require "rbconfig"
|
|
152
|
+
File.join(RbConfig::CONFIG["bindir"], RbConfig::CONFIG["ruby_install_name"])
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def default_worker_script
|
|
156
|
+
File.expand_path("../../../bin/chocomint-console-worker", __dir__)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def create_named_pipe(name)
|
|
160
|
+
handle = Native.CreateNamedPipeW(
|
|
161
|
+
utf16(name),
|
|
162
|
+
Native::PIPE_ACCESS_DUPLEX,
|
|
163
|
+
Native::PIPE_TYPE_BYTE | Native::PIPE_WAIT | Native::PIPE_REJECT_REMOTE_CLIENTS,
|
|
164
|
+
1, 65_536, 65_536, 0, nil
|
|
165
|
+
)
|
|
166
|
+
if handle == Native::INVALID_HANDLE_VALUE || handle.null?
|
|
167
|
+
raise Error, "CreateNamedPipeW failed (#{Native.GetLastError})"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
handle
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def spawn_worker
|
|
174
|
+
si = Native::StartupInfoW.new
|
|
175
|
+
si[:cb] = Native::StartupInfoW.size
|
|
176
|
+
si[:dwFlags] = Native::STARTF_USESHOWWINDOW
|
|
177
|
+
si[:wShowWindow] = Native::SW_HIDE
|
|
178
|
+
pi = Native::ProcessInformation.new
|
|
179
|
+
|
|
180
|
+
cmd_ptr = utf16(build_command_line)
|
|
181
|
+
cwd_ptr = @cwd ? utf16(@cwd) : nil
|
|
182
|
+
|
|
183
|
+
ok = Native.CreateProcessW(
|
|
184
|
+
nil, cmd_ptr, nil, nil, 0, # stdio は継承しない (コンソールを汚さない)
|
|
185
|
+
Native::CREATE_NEW_CONSOLE, # ConPTY が動くための新規コンソール
|
|
186
|
+
nil, cwd_ptr, si.pointer, pi.pointer
|
|
187
|
+
)
|
|
188
|
+
raise Error, "CreateProcessW worker failed (#{Native.GetLastError})" if ok.zero?
|
|
189
|
+
|
|
190
|
+
@proc_handle = pi[:hProcess]
|
|
191
|
+
Native.CloseHandle(pi[:hThread])
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# ruby worker.rb --c2w <name> --w2c <name> --cols .. --rows .. --cwd .. -- <shell>
|
|
195
|
+
def build_command_line
|
|
196
|
+
parts = [quote(@ruby_exe), quote(@worker_script),
|
|
197
|
+
"--c2w", quote(@c2w_name), "--w2c", quote(@w2c_name),
|
|
198
|
+
"--cols", @cols.to_s, "--rows", @rows.to_s]
|
|
199
|
+
parts += ["--cwd", quote(@cwd)] if @cwd
|
|
200
|
+
parts += ["--", @command]
|
|
201
|
+
parts.join(" ")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def quote(str) = %("#{str}")
|
|
205
|
+
|
|
206
|
+
def utf16(str)
|
|
207
|
+
w = str.encode("UTF-16LE").b + "\x00\x00".b
|
|
208
|
+
ptr = FFI::MemoryPointer.new(:uint8, w.bytesize)
|
|
209
|
+
ptr.put_bytes(0, w)
|
|
210
|
+
ptr
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# 親→worker は c2w パイプへ書く。
|
|
214
|
+
def write_bytes(data)
|
|
215
|
+
buf = FFI::MemoryPointer.new(:uint8, data.bytesize)
|
|
216
|
+
buf.put_bytes(0, data)
|
|
217
|
+
written = FFI::MemoryPointer.new(:uint32)
|
|
218
|
+
ok = Native.WriteFile(@c2w, buf, data.bytesize, written, nil)
|
|
219
|
+
raise Error, "WriteFile pipe (#{Native.GetLastError})" if ok.zero?
|
|
220
|
+
|
|
221
|
+
written.read(:uint32)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def read_line
|
|
225
|
+
@rbuf ||= +""
|
|
226
|
+
loop do
|
|
227
|
+
if (idx = @rbuf.index("\n"))
|
|
228
|
+
return @rbuf.slice!(0..idx).chomp
|
|
229
|
+
end
|
|
230
|
+
chunk = read_available_bytes
|
|
231
|
+
return nil if chunk.nil? || chunk.empty?
|
|
232
|
+
|
|
233
|
+
@rbuf << chunk
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# worker→親は w2c パイプから読む。
|
|
238
|
+
def read_available_bytes(max = 65_536)
|
|
239
|
+
avail = FFI::MemoryPointer.new(:uint32)
|
|
240
|
+
return nil if Native.PeekNamedPipe(@w2c, nil, 0, nil, avail, nil).zero?
|
|
241
|
+
|
|
242
|
+
n = avail.read(:uint32)
|
|
243
|
+
return "".b if n.zero?
|
|
244
|
+
|
|
245
|
+
want = [n, max].min
|
|
246
|
+
buf = FFI::MemoryPointer.new(:uint8, want)
|
|
247
|
+
read = FFI::MemoryPointer.new(:uint32)
|
|
248
|
+
return nil if Native.ReadFile(@w2c, buf, want, read, nil).zero?
|
|
249
|
+
|
|
250
|
+
buf.get_bytes(0, read.read(:uint32))
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module Chocomint
|
|
8
|
+
# /edit AI エディタの Git 操作ハンドラ群 (Server に include して使う)。
|
|
9
|
+
#
|
|
10
|
+
# 提供機能:
|
|
11
|
+
# GET /edit/git/status 初期化有無・ブランチ名・ステージ済み/未ステージのファイル一覧
|
|
12
|
+
# POST /edit/git/init git init
|
|
13
|
+
# POST /edit/git/stage ファイルをステージ (git add)
|
|
14
|
+
# POST /edit/git/unstage ステージ取り消し (git restore --staged)
|
|
15
|
+
# POST /edit/git/stage_all すべての変更をステージ (git add -A)
|
|
16
|
+
# POST /edit/git/unstage_all すべてのステージを取り消し (git restore --staged .)
|
|
17
|
+
# POST /edit/git/discard 変更を破棄 (追跡済みは checkout、未追跡は削除)
|
|
18
|
+
# POST /edit/git/commit コミット
|
|
19
|
+
# POST /edit/git/push push (リモート未設定なら reason: "no_remote" 付き 409 を返す)
|
|
20
|
+
# POST /edit/git/remote origin の URL を設定して push (未設定/差し替えの両対応)
|
|
21
|
+
# GET /edit/git/branches ブランチ一覧 (現在のブランチも含む)
|
|
22
|
+
# POST /edit/git/checkout ブランチ切り替え/新規作成
|
|
23
|
+
#
|
|
24
|
+
# すべて edit_root (workspace ルート) を cwd にして git コマンドを実行する。
|
|
25
|
+
# workspace が git 管理下でない場合は status で初期化を促す。
|
|
26
|
+
module EditGitHandlers
|
|
27
|
+
# ---- ステータス ---------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def handle_edit_git_status(req, res)
|
|
30
|
+
return edit_method_guard(res) unless req.request_method == "GET"
|
|
31
|
+
|
|
32
|
+
unless git_repo?
|
|
33
|
+
return edit_json(res, 200, "initialized" => false)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
branch = git_current_branch
|
|
37
|
+
# --untracked-files=all: 未追跡ディレクトリを 1 エントリにまとめず中のファイルを
|
|
38
|
+
# 個別に列挙させる。無指定だと "logs/" のようにディレクトリ単位でしか返らず、
|
|
39
|
+
# + ボタンでファイル単位のステージができなくなる。
|
|
40
|
+
porcelain = git_capture(%w[status --porcelain=v1 --untracked-files=all])
|
|
41
|
+
staged, unstaged = parse_git_status(porcelain)
|
|
42
|
+
|
|
43
|
+
edit_json(res, 200,
|
|
44
|
+
"initialized" => true,
|
|
45
|
+
"branch" => branch,
|
|
46
|
+
"staged" => staged,
|
|
47
|
+
"unstaged" => unstaged,
|
|
48
|
+
"remote_url" => git_remote_url)
|
|
49
|
+
rescue Chocomint::Error => e
|
|
50
|
+
edit_json(res, 500, "error" => e.message)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# git status --porcelain=v1 の各行 (XY PATH) をステージ済み/未ステージに振り分ける。
|
|
54
|
+
# X: インデックス(ステージ)の状態、Y: 作業ツリーの状態。
|
|
55
|
+
# "??" (未追跡) は unstaged 扱いにする。
|
|
56
|
+
def parse_git_status(lines)
|
|
57
|
+
staged = []
|
|
58
|
+
unstaged = []
|
|
59
|
+
lines.each do |line|
|
|
60
|
+
next if line.length < 4
|
|
61
|
+
|
|
62
|
+
x = line[0]
|
|
63
|
+
y = line[1]
|
|
64
|
+
path = line[3..].to_s.tr("\\", "/")
|
|
65
|
+
# rename は "R old -> new" の形式。表示・操作対象は new 側のパス。
|
|
66
|
+
path = path.split(" -> ").last if path.include?(" -> ")
|
|
67
|
+
|
|
68
|
+
staged << { "path" => path, "status" => x } if x != " " && x != "?"
|
|
69
|
+
unstaged << { "path" => path, "status" => y } if y != " " || x == "?"
|
|
70
|
+
end
|
|
71
|
+
[staged, unstaged]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ---- 初期化 ---------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def handle_edit_git_init(req, res)
|
|
77
|
+
return edit_method_guard(res) unless req.request_method == "POST"
|
|
78
|
+
|
|
79
|
+
if git_repo?
|
|
80
|
+
return edit_json(res, 400, "error" => "既に git 管理下です")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
out, err, status = git_run(%w[init])
|
|
84
|
+
return edit_json(res, 500, "error" => err.empty? ? out : err) unless status.success?
|
|
85
|
+
|
|
86
|
+
edit_json(res, 200, "initialized" => true, "branch" => git_current_branch)
|
|
87
|
+
rescue Chocomint::Error => e
|
|
88
|
+
edit_json(res, 500, "error" => e.message)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# ---- ステージ / 取り消し ---------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def handle_edit_git_stage(req, res)
|
|
94
|
+
return edit_method_guard(res) unless req.request_method == "POST"
|
|
95
|
+
|
|
96
|
+
with_git(res) do
|
|
97
|
+
require_git_repo!
|
|
98
|
+
paths = git_body_paths(req)
|
|
99
|
+
out, err, status = git_run(["add", "--"] + paths)
|
|
100
|
+
raise Chocomint::Error, (err.empty? ? out : err) unless status.success?
|
|
101
|
+
|
|
102
|
+
edit_json(res, 200, "staged" => paths)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def handle_edit_git_unstage(req, res)
|
|
107
|
+
return edit_method_guard(res) unless req.request_method == "POST"
|
|
108
|
+
|
|
109
|
+
with_git(res) do
|
|
110
|
+
require_git_repo!
|
|
111
|
+
paths = git_body_paths(req)
|
|
112
|
+
out, err, status = unstage_paths(paths)
|
|
113
|
+
raise Chocomint::Error, (err.empty? ? out : err) unless status.success?
|
|
114
|
+
|
|
115
|
+
edit_json(res, 200, "unstaged" => paths)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def handle_edit_git_stage_all(req, res)
|
|
120
|
+
return edit_method_guard(res) unless req.request_method == "POST"
|
|
121
|
+
|
|
122
|
+
with_git(res) do
|
|
123
|
+
require_git_repo!
|
|
124
|
+
out, err, status = git_run(%w[add -A])
|
|
125
|
+
raise Chocomint::Error, (err.empty? ? out : err) unless status.success?
|
|
126
|
+
|
|
127
|
+
edit_json(res, 200, "staged_all" => true)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def handle_edit_git_unstage_all(req, res)
|
|
132
|
+
return edit_method_guard(res) unless req.request_method == "POST"
|
|
133
|
+
|
|
134
|
+
with_git(res) do
|
|
135
|
+
require_git_repo!
|
|
136
|
+
out, err, status = unstage_paths(%w[.])
|
|
137
|
+
raise Chocomint::Error, (err.empty? ? out : err) unless status.success?
|
|
138
|
+
|
|
139
|
+
edit_json(res, 200, "unstaged_all" => true)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# ---- 変更の破棄 ---------------------------------------------------------------
|
|
144
|
+
#
|
|
145
|
+
# 追跡済みファイルの変更は `git restore` で元に戻し、未追跡ファイルはワークツリーから
|
|
146
|
+
# 削除する (git 管理下に無いため restore では戻せない)。ディレクトリ丸ごとの未追跡も
|
|
147
|
+
# 同様に削除する。ステージ済みの変更はまず取り消してから破棄する (add 済みでも作業ツリーを戻す)。
|
|
148
|
+
def handle_edit_git_discard(req, res)
|
|
149
|
+
return edit_method_guard(res) unless req.request_method == "POST"
|
|
150
|
+
|
|
151
|
+
with_git(res) do
|
|
152
|
+
require_git_repo!
|
|
153
|
+
paths = git_body_paths(req)
|
|
154
|
+
discard_paths(paths)
|
|
155
|
+
edit_json(res, 200, "discarded" => paths)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def discard_paths(paths)
|
|
160
|
+
tracked, untracked = paths.partition { |p| git_tracked?(p) }
|
|
161
|
+
|
|
162
|
+
unless tracked.empty?
|
|
163
|
+
unstage_paths(tracked)
|
|
164
|
+
_out, err, status = git_run(["restore", "--"] + tracked)
|
|
165
|
+
raise Chocomint::Error, (err.empty? ? "変更の破棄に失敗しました" : err) unless status.success?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
untracked.each do |p|
|
|
169
|
+
abs = @path_guard.resolve(p)
|
|
170
|
+
FileUtils.rm_rf(abs)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# ステージ済みの変更を取り消す。`git restore --staged` は初回コミット前
|
|
175
|
+
# (HEAD が無い) だと "fatal: could not resolve HEAD" で失敗するため、
|
|
176
|
+
# その場合は index をリセットする `git reset` にフォールバックする。
|
|
177
|
+
def unstage_paths(paths)
|
|
178
|
+
out, err, status = git_run(["restore", "--staged", "--"] + paths)
|
|
179
|
+
return [out, err, status] if status.success?
|
|
180
|
+
|
|
181
|
+
git_run(["reset", "--"] + paths)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def git_tracked?(path)
|
|
185
|
+
_out, _err, status = git_run(["ls-files", "--error-unmatch", "--", path])
|
|
186
|
+
status.success?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# ---- コミット ---------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
def handle_edit_git_commit(req, res)
|
|
192
|
+
return edit_method_guard(res) unless req.request_method == "POST"
|
|
193
|
+
|
|
194
|
+
with_git(res) do
|
|
195
|
+
require_git_repo!
|
|
196
|
+
body = JSON.parse(req.body.to_s)
|
|
197
|
+
message = body["message"].to_s.strip
|
|
198
|
+
raise Chocomint::Error, "コミットメッセージが必要です" if message.empty?
|
|
199
|
+
|
|
200
|
+
out, err, status = git_run(["commit", "-m", message])
|
|
201
|
+
raise Chocomint::Error, (err.empty? ? out : err) unless status.success?
|
|
202
|
+
|
|
203
|
+
edit_json(res, 200, "committed" => true, "branch" => git_current_branch)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# ---- push ---------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def handle_edit_git_push(req, res)
|
|
210
|
+
return edit_method_guard(res) unless req.request_method == "POST"
|
|
211
|
+
|
|
212
|
+
unless git_repo?
|
|
213
|
+
return edit_json(res, 400, "error" => "git 管理下ではありません")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
out, err, status = git_run(%w[push])
|
|
217
|
+
return edit_json(res, 200, "pushed" => true, "output" => (err.empty? ? out : err)) if status.success?
|
|
218
|
+
|
|
219
|
+
message = err.empty? ? out : err
|
|
220
|
+
# リモート/上流ブランチが未設定だと push は失敗する。フロントはこの reason で
|
|
221
|
+
# 「リモート URL を入力させるダイアログ」に切り替える (エラーダイアログではなく)。
|
|
222
|
+
if push_target_missing?(message)
|
|
223
|
+
return edit_json(res, 409, "error" => message, "reason" => "no_remote")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
edit_json(res, 400, "error" => message)
|
|
227
|
+
rescue JSON::ParserError => e
|
|
228
|
+
edit_json(res, 400, "error" => "invalid JSON: #{e.message}")
|
|
229
|
+
rescue Chocomint::Error => e
|
|
230
|
+
edit_json(res, 500, "error" => e.message)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# push 先が無いことを示すエラー文言か判定する (リモート未設定 / 上流ブランチ未設定の両方)。
|
|
234
|
+
def push_target_missing?(message)
|
|
235
|
+
message.include?("No configured push destination") ||
|
|
236
|
+
message.include?("has no upstream branch")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# ---- リモート ---------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def handle_edit_git_remote_set(req, res)
|
|
242
|
+
return edit_method_guard(res) unless req.request_method == "POST"
|
|
243
|
+
|
|
244
|
+
with_git(res) do
|
|
245
|
+
require_git_repo!
|
|
246
|
+
body = JSON.parse(req.body.to_s)
|
|
247
|
+
url = body["url"].to_s.strip
|
|
248
|
+
raise Chocomint::Error, "リモート URL を入力してください" if url.empty?
|
|
249
|
+
|
|
250
|
+
# 既に origin が設定されていれば URL を差し替え、無ければ新規に追加する。
|
|
251
|
+
if git_remote_url
|
|
252
|
+
out, err, status = git_run(%w[remote set-url origin] + [url])
|
|
253
|
+
else
|
|
254
|
+
out, err, status = git_run(%w[remote add origin] + [url])
|
|
255
|
+
end
|
|
256
|
+
raise Chocomint::Error, (err.empty? ? out : err) unless status.success?
|
|
257
|
+
|
|
258
|
+
# 上流ブランチも合わせて設定しつつ push する (初回 push は -u が無いと
|
|
259
|
+
# "has no upstream branch" で失敗するため)。
|
|
260
|
+
branch = git_current_branch
|
|
261
|
+
out, err, status = git_run(["push", "-u", "origin", branch])
|
|
262
|
+
raise Chocomint::Error, (err.empty? ? out : err) unless status.success?
|
|
263
|
+
|
|
264
|
+
edit_json(res, 200, "pushed" => true, "remote_url" => url, "output" => (err.empty? ? out : err))
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# ---- ブランチ ---------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
def handle_edit_git_branches(req, res)
|
|
271
|
+
return edit_method_guard(res) unless req.request_method == "GET"
|
|
272
|
+
|
|
273
|
+
unless git_repo?
|
|
274
|
+
return edit_json(res, 400, "error" => "git 管理下ではありません")
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
out = git_capture(%w[branch --list])
|
|
278
|
+
branches = out.map { |l| l.sub(/\A\*?\s*/, "").strip }.reject(&:empty?)
|
|
279
|
+
current = git_current_branch
|
|
280
|
+
# コミットが一つも無いブランチ (init 直後など) は `git branch --list` に
|
|
281
|
+
# 現れないため、現在のブランチが漏れていれば一覧に加えて必ず選択肢に見せる。
|
|
282
|
+
branches.unshift(current) if current && !branches.include?(current)
|
|
283
|
+
edit_json(res, 200, "branches" => branches, "current" => current)
|
|
284
|
+
rescue Chocomint::Error => e
|
|
285
|
+
edit_json(res, 500, "error" => e.message)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def handle_edit_git_checkout(req, res)
|
|
289
|
+
return edit_method_guard(res) unless req.request_method == "POST"
|
|
290
|
+
|
|
291
|
+
with_git(res) do
|
|
292
|
+
require_git_repo!
|
|
293
|
+
body = JSON.parse(req.body.to_s)
|
|
294
|
+
branch = body["branch"].to_s.strip
|
|
295
|
+
create = !!body["create"]
|
|
296
|
+
raise Chocomint::Error, "branch required" if branch.empty?
|
|
297
|
+
|
|
298
|
+
args = create ? ["checkout", "-b", branch] : ["checkout", branch]
|
|
299
|
+
out, err, status = git_run(args)
|
|
300
|
+
unless status.success?
|
|
301
|
+
message = err.empty? ? out : err
|
|
302
|
+
# ブランチ参照が見つからない場合、原因は「名前が間違っている」か
|
|
303
|
+
# 「切替先にまだコミットが一つも無く Git 上に実体化していない」のどちらか
|
|
304
|
+
# (エラー文言だけでは判別できない) なので、両方の可能性を伝える。
|
|
305
|
+
if !create && (message.include?("invalid reference") || message.include?("did not match"))
|
|
306
|
+
message += "\n(ブランチ名の誤り、またはコミットが一つも無い空のブランチのため" \
|
|
307
|
+
"切り替えられていない可能性があります。空のブランチは最初のコミットを" \
|
|
308
|
+
"するまで Git 上に実体化しません。)"
|
|
309
|
+
end
|
|
310
|
+
raise Chocomint::Error, message
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
edit_json(res, 200, "branch" => git_current_branch)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# ---- 補助 ---------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
# Open3::Status 互換 (success? だけ呼べれば良い) の失敗ダミー。
|
|
320
|
+
# git コマンド自体が見つからない (Errno::ENOENT) 場合に使う。
|
|
321
|
+
class GitCommandNotFoundStatus
|
|
322
|
+
def success? = false
|
|
323
|
+
end
|
|
324
|
+
GIT_NOT_FOUND = GitCommandNotFoundStatus.new.freeze
|
|
325
|
+
|
|
326
|
+
# edit_root 自身が git リポジトリのトップレベルである場合のみ true を返す。
|
|
327
|
+
# workspace が祖先ディレクトリの .git を継承しているだけ (workspace 自体は
|
|
328
|
+
# 管理下に無い) だと git コマンドはその祖先リポジトリに対して動いてしまう
|
|
329
|
+
# ため、show-toplevel が edit_root と一致するかまで確認する。
|
|
330
|
+
def git_repo?
|
|
331
|
+
out, _err, status = git_run(%w[rev-parse --show-toplevel])
|
|
332
|
+
return false unless status.success?
|
|
333
|
+
|
|
334
|
+
same_path?(out.strip, edit_root)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# git の出力 (常に "/" 区切り) と Ruby 側の絶対パスを比較する。
|
|
338
|
+
# Windows はドライブレターの大小文字揺れがあるため大文字小文字を無視する。
|
|
339
|
+
def same_path?(git_path, ruby_path)
|
|
340
|
+
normalize_path(git_path) == normalize_path(ruby_path)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def normalize_path(path)
|
|
344
|
+
File.expand_path(path).tr("\\", "/").downcase
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def git_current_branch
|
|
348
|
+
out, _err, status = git_run(%w[branch --show-current])
|
|
349
|
+
status.success? ? out.strip : nil
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# origin の URL (未設定なら nil)。
|
|
353
|
+
def git_remote_url
|
|
354
|
+
out, _err, status = git_run(%w[remote get-url origin])
|
|
355
|
+
status.success? ? out.strip : nil
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# git コマンドを edit_root で実行し [stdout, stderr, Process::Status] を返す。
|
|
359
|
+
def git_run(args)
|
|
360
|
+
Open3.capture3("git", *args, chdir: edit_root)
|
|
361
|
+
rescue Errno::ENOENT
|
|
362
|
+
["", "git コマンドが見つかりません", GIT_NOT_FOUND]
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# 成功時の stdout を行配列で返す (失敗時は空配列)。
|
|
366
|
+
def git_capture(args)
|
|
367
|
+
out, _err, status = git_run(args)
|
|
368
|
+
status.success? ? out.each_line.map(&:chomp) : []
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# JSON ボディから "paths" (配列) または "path" (単一) を取り出す。
|
|
372
|
+
def git_body_paths(req)
|
|
373
|
+
body = JSON.parse(req.body.to_s)
|
|
374
|
+
paths = Array(body["paths"] || body["path"]).map(&:to_s).reject(&:empty?)
|
|
375
|
+
raise Chocomint::Error, "path(s) required" if paths.empty?
|
|
376
|
+
|
|
377
|
+
paths
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# edit_root がリポジトリのトップレベルでなければ拒否する。init/checkout 以外の
|
|
381
|
+
# 全操作の先頭で呼び、祖先ディレクトリの .git を誤って操作しないようにする。
|
|
382
|
+
def require_git_repo!
|
|
383
|
+
raise Chocomint::Error, "git 管理下ではありません" unless git_repo?
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Git 操作系ハンドラ共通の例外→HTTP 変換。
|
|
387
|
+
def with_git(res)
|
|
388
|
+
yield
|
|
389
|
+
rescue JSON::ParserError => e
|
|
390
|
+
edit_json(res, 400, "error" => "invalid JSON: #{e.message}")
|
|
391
|
+
rescue Chocomint::Error => e
|
|
392
|
+
edit_json(res, 400, "error" => e.message)
|
|
393
|
+
rescue SystemCallError => e
|
|
394
|
+
edit_json(res, 500, "error" => e.message)
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|