e2b 0.2.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.txt +21 -0
- data/README.md +181 -0
- data/lib/e2b/api/http_client.rb +164 -0
- data/lib/e2b/client.rb +201 -0
- data/lib/e2b/configuration.rb +119 -0
- data/lib/e2b/errors.rb +88 -0
- data/lib/e2b/models/entry_info.rb +243 -0
- data/lib/e2b/models/process_result.rb +127 -0
- data/lib/e2b/models/sandbox_info.rb +94 -0
- data/lib/e2b/sandbox.rb +407 -0
- data/lib/e2b/services/base_service.rb +485 -0
- data/lib/e2b/services/command_handle.rb +350 -0
- data/lib/e2b/services/commands.rb +229 -0
- data/lib/e2b/services/filesystem.rb +373 -0
- data/lib/e2b/services/git.rb +893 -0
- data/lib/e2b/services/pty.rb +297 -0
- data/lib/e2b/services/watch_handle.rb +110 -0
- data/lib/e2b/version.rb +5 -0
- data/lib/e2b.rb +87 -0
- metadata +142 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
|
|
6
|
+
module E2B
|
|
7
|
+
module Services
|
|
8
|
+
# Represents the status of a single file in the git working tree
|
|
9
|
+
#
|
|
10
|
+
# @attr_reader path [String] File path relative to the repository root
|
|
11
|
+
# @attr_reader index_status [String] Status in the index (staged area)
|
|
12
|
+
# @attr_reader work_tree_status [String] Status in the working tree
|
|
13
|
+
GitFileStatus = Struct.new(:path, :index_status, :work_tree_status, keyword_init: true)
|
|
14
|
+
|
|
15
|
+
# Represents the result of `git status`, including branch info and file statuses
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# status = sandbox.git.status("/home/user/repo")
|
|
19
|
+
# puts status.current_branch
|
|
20
|
+
# puts "Clean!" if status.clean?
|
|
21
|
+
class GitStatus
|
|
22
|
+
# @return [String, nil] Name of the current branch, or nil if detached
|
|
23
|
+
attr_reader :current_branch
|
|
24
|
+
|
|
25
|
+
# @return [String, nil] Name of the upstream tracking branch
|
|
26
|
+
attr_reader :upstream
|
|
27
|
+
|
|
28
|
+
# @return [Integer] Number of commits ahead of upstream
|
|
29
|
+
attr_reader :ahead
|
|
30
|
+
|
|
31
|
+
# @return [Integer] Number of commits behind upstream
|
|
32
|
+
attr_reader :behind
|
|
33
|
+
|
|
34
|
+
# @return [Boolean] Whether HEAD is in detached state
|
|
35
|
+
attr_reader :detached
|
|
36
|
+
|
|
37
|
+
# @return [Array<GitFileStatus>] List of file statuses
|
|
38
|
+
attr_reader :file_status
|
|
39
|
+
|
|
40
|
+
# @param current_branch [String, nil] Current branch name
|
|
41
|
+
# @param upstream [String, nil] Upstream tracking branch
|
|
42
|
+
# @param ahead [Integer] Commits ahead of upstream
|
|
43
|
+
# @param behind [Integer] Commits behind upstream
|
|
44
|
+
# @param detached [Boolean] Whether HEAD is detached
|
|
45
|
+
# @param file_status [Array<GitFileStatus>] File status entries
|
|
46
|
+
def initialize(current_branch: nil, upstream: nil, ahead: 0, behind: 0, detached: false, file_status: [])
|
|
47
|
+
@current_branch = current_branch
|
|
48
|
+
@upstream = upstream
|
|
49
|
+
@ahead = ahead
|
|
50
|
+
@behind = behind
|
|
51
|
+
@detached = detached
|
|
52
|
+
@file_status = file_status
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Whether the working tree is clean (no changes at all)
|
|
56
|
+
#
|
|
57
|
+
# @return [Boolean]
|
|
58
|
+
def clean?
|
|
59
|
+
file_status.empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Whether the working tree has any changes
|
|
63
|
+
#
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def has_changes?
|
|
66
|
+
!clean?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Whether there are any staged changes
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean]
|
|
72
|
+
def has_staged?
|
|
73
|
+
file_status.any? { |f| f.index_status != "." && f.index_status != "?" }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Whether there are any untracked files
|
|
77
|
+
#
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
def has_untracked?
|
|
80
|
+
file_status.any? { |f| f.index_status == "?" }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Whether there are any merge conflicts
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def has_conflicts?
|
|
87
|
+
file_status.any? { |f| f.index_status == "u" || f.index_status == "U" }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Number of staged files
|
|
91
|
+
#
|
|
92
|
+
# @return [Integer]
|
|
93
|
+
def staged_count
|
|
94
|
+
file_status.count { |f| f.index_status != "." && f.index_status != "?" }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Number of untracked files
|
|
98
|
+
#
|
|
99
|
+
# @return [Integer]
|
|
100
|
+
def untracked_count
|
|
101
|
+
file_status.count { |f| f.index_status == "?" }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Number of conflicted files
|
|
105
|
+
#
|
|
106
|
+
# @return [Integer]
|
|
107
|
+
def conflict_count
|
|
108
|
+
file_status.count { |f| f.index_status == "u" || f.index_status == "U" }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Number of modified files (in the working tree)
|
|
112
|
+
#
|
|
113
|
+
# @return [Integer]
|
|
114
|
+
def modified_count
|
|
115
|
+
file_status.count { |f| f.work_tree_status == "M" }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Represents the branches in a git repository
|
|
120
|
+
#
|
|
121
|
+
# @example
|
|
122
|
+
# branches = sandbox.git.branches("/home/user/repo")
|
|
123
|
+
# puts "Current: #{branches.current}"
|
|
124
|
+
# puts "Local: #{branches.local.join(', ')}"
|
|
125
|
+
class GitBranches
|
|
126
|
+
# @return [String, nil] The currently checked-out branch
|
|
127
|
+
attr_reader :current
|
|
128
|
+
|
|
129
|
+
# @return [Array<String>] List of local branch names
|
|
130
|
+
attr_reader :local
|
|
131
|
+
|
|
132
|
+
# @return [Array<String>] List of remote branch names
|
|
133
|
+
attr_reader :remote
|
|
134
|
+
|
|
135
|
+
# @param current [String, nil] Current branch name
|
|
136
|
+
# @param local [Array<String>] Local branch names
|
|
137
|
+
# @param remote [Array<String>] Remote branch names
|
|
138
|
+
def initialize(current: nil, local: [], remote: [])
|
|
139
|
+
@current = current
|
|
140
|
+
@local = local
|
|
141
|
+
@remote = remote
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Git operations service for E2B sandbox
|
|
146
|
+
#
|
|
147
|
+
# Runs git CLI commands inside the sandbox by delegating to the Commands service.
|
|
148
|
+
# Does not use RPC directly - all operations are performed via shell commands.
|
|
149
|
+
#
|
|
150
|
+
# @example
|
|
151
|
+
# sandbox.git.clone("https://github.com/user/repo.git", path: "/home/user/repo")
|
|
152
|
+
# sandbox.git.add("/home/user/repo")
|
|
153
|
+
# sandbox.git.commit("/home/user/repo", "Initial commit", author_name: "Bot", author_email: "bot@example.com")
|
|
154
|
+
# sandbox.git.push("/home/user/repo")
|
|
155
|
+
class Git
|
|
156
|
+
# Default environment variables applied to all git commands
|
|
157
|
+
DEFAULT_GIT_ENV = { "GIT_TERMINAL_PROMPT" => "0" }.freeze
|
|
158
|
+
|
|
159
|
+
# Default timeout for git operations in seconds
|
|
160
|
+
DEFAULT_TIMEOUT = 300
|
|
161
|
+
|
|
162
|
+
# Valid git config scopes
|
|
163
|
+
VALID_SCOPES = %w[global local system].freeze
|
|
164
|
+
|
|
165
|
+
# @param commands [E2B::Services::Commands] The commands service instance
|
|
166
|
+
def initialize(commands:)
|
|
167
|
+
@commands = commands
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Clone a git repository
|
|
171
|
+
#
|
|
172
|
+
# @param url [String] Repository URL to clone
|
|
173
|
+
# @param path [String, nil] Destination path; if nil, git chooses the directory name
|
|
174
|
+
# @param branch [String, nil] Branch to checkout after cloning
|
|
175
|
+
# @param depth [Integer, nil] Create a shallow clone with the given depth
|
|
176
|
+
# @param username [String, nil] Username for authentication
|
|
177
|
+
# @param password [String, nil] Password or token for authentication
|
|
178
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
179
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
180
|
+
# @param cwd [String, nil] Working directory for the clone command
|
|
181
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
182
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
183
|
+
# @raise [E2B::GitAuthError] If authentication fails
|
|
184
|
+
def clone(url, path: nil, branch: nil, depth: nil, username: nil, password: nil,
|
|
185
|
+
envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
186
|
+
clone_url = if username && password
|
|
187
|
+
with_credentials(url, username, password)
|
|
188
|
+
else
|
|
189
|
+
url
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
args = ["clone"]
|
|
193
|
+
args += ["--branch", branch] if branch
|
|
194
|
+
args += ["--depth", depth.to_s] if depth
|
|
195
|
+
args << Shellwords.escape(clone_url)
|
|
196
|
+
args << Shellwords.escape(path) if path
|
|
197
|
+
|
|
198
|
+
result = run_git(args, nil, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
199
|
+
check_auth_failure!(result)
|
|
200
|
+
result
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Initialize a new git repository
|
|
204
|
+
#
|
|
205
|
+
# @param path [String] Path where the repository should be initialized
|
|
206
|
+
# @param bare [Boolean] Create a bare repository
|
|
207
|
+
# @param initial_branch [String, nil] Name for the initial branch
|
|
208
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
209
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
210
|
+
# @param cwd [String, nil] Working directory
|
|
211
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
212
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
213
|
+
def init(path, bare: false, initial_branch: nil, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
214
|
+
args = ["init"]
|
|
215
|
+
args << "--bare" if bare
|
|
216
|
+
args += ["--initial-branch", initial_branch] if initial_branch
|
|
217
|
+
args << Shellwords.escape(path)
|
|
218
|
+
|
|
219
|
+
run_git(args, nil, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Add a remote to a repository
|
|
223
|
+
#
|
|
224
|
+
# @param path [String] Repository path
|
|
225
|
+
# @param name [String] Remote name (e.g., "origin")
|
|
226
|
+
# @param url [String] Remote URL
|
|
227
|
+
# @param fetch [Boolean] Fetch from the remote after adding
|
|
228
|
+
# @param overwrite [Boolean] If true, set-url on an existing remote instead of failing
|
|
229
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
230
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
231
|
+
# @param cwd [String, nil] Working directory
|
|
232
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
233
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
234
|
+
def remote_add(path, name, url, fetch: false, overwrite: false, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
235
|
+
if overwrite
|
|
236
|
+
# Check if remote already exists
|
|
237
|
+
existing = remote_get(path, name, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
238
|
+
if existing
|
|
239
|
+
args = ["remote", "set-url", name, Shellwords.escape(url)]
|
|
240
|
+
result = run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
241
|
+
else
|
|
242
|
+
args = ["remote", "add", name, Shellwords.escape(url)]
|
|
243
|
+
result = run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
244
|
+
end
|
|
245
|
+
else
|
|
246
|
+
args = ["remote", "add", name, Shellwords.escape(url)]
|
|
247
|
+
result = run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
if fetch
|
|
251
|
+
fetch_args = ["fetch", name]
|
|
252
|
+
run_git(fetch_args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
result
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Get the URL of a named remote
|
|
259
|
+
#
|
|
260
|
+
# @param path [String] Repository path
|
|
261
|
+
# @param name [String] Remote name (e.g., "origin")
|
|
262
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
263
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
264
|
+
# @param cwd [String, nil] Working directory
|
|
265
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
266
|
+
# @return [String, nil] Remote URL, or nil if the remote does not exist
|
|
267
|
+
def remote_get(path, name, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
268
|
+
args = ["remote", "get-url", name]
|
|
269
|
+
result = run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
270
|
+
return nil unless result.success?
|
|
271
|
+
|
|
272
|
+
url = result.stdout.strip
|
|
273
|
+
url.empty? ? nil : url
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Get the status of a git repository
|
|
277
|
+
#
|
|
278
|
+
# @param path [String] Repository path
|
|
279
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
280
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
281
|
+
# @param cwd [String, nil] Working directory
|
|
282
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
283
|
+
# @return [GitStatus] Parsed status information
|
|
284
|
+
def status(path, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
285
|
+
args = ["status", "--porcelain=v2", "--branch"]
|
|
286
|
+
result = run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
287
|
+
parse_status(result.stdout)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# List all branches (local and remote)
|
|
291
|
+
#
|
|
292
|
+
# @param path [String] Repository path
|
|
293
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
294
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
295
|
+
# @param cwd [String, nil] Working directory
|
|
296
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
297
|
+
# @return [GitBranches] Parsed branch information
|
|
298
|
+
def branches(path, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
299
|
+
args = ["branch", "-a", "--format=%(refname:short) %(HEAD)"]
|
|
300
|
+
result = run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
301
|
+
parse_branches(result.stdout)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Create a new branch
|
|
305
|
+
#
|
|
306
|
+
# @param path [String] Repository path
|
|
307
|
+
# @param branch [String] Branch name to create
|
|
308
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
309
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
310
|
+
# @param cwd [String, nil] Working directory
|
|
311
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
312
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
313
|
+
def create_branch(path, branch, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
314
|
+
args = ["branch", branch]
|
|
315
|
+
run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Checkout an existing branch
|
|
319
|
+
#
|
|
320
|
+
# @param path [String] Repository path
|
|
321
|
+
# @param branch [String] Branch name to checkout
|
|
322
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
323
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
324
|
+
# @param cwd [String, nil] Working directory
|
|
325
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
326
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
327
|
+
def checkout_branch(path, branch, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
328
|
+
args = ["checkout", branch]
|
|
329
|
+
run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Delete a branch
|
|
333
|
+
#
|
|
334
|
+
# @param path [String] Repository path
|
|
335
|
+
# @param branch [String] Branch name to delete
|
|
336
|
+
# @param force [Boolean] Force-delete the branch (even if not fully merged)
|
|
337
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
338
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
339
|
+
# @param cwd [String, nil] Working directory
|
|
340
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
341
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
342
|
+
def delete_branch(path, branch, force: false, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
343
|
+
flag = force ? "-D" : "-d"
|
|
344
|
+
args = ["branch", flag, branch]
|
|
345
|
+
run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Stage files for commit
|
|
349
|
+
#
|
|
350
|
+
# @param path [String] Repository path
|
|
351
|
+
# @param files [Array<String>, nil] Specific files to stage; ignored when +all+ is true
|
|
352
|
+
# @param all [Boolean] Stage all changes (tracked and untracked)
|
|
353
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
354
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
355
|
+
# @param cwd [String, nil] Working directory
|
|
356
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
357
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
358
|
+
def add(path, files: nil, all: true, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
359
|
+
args = ["add"]
|
|
360
|
+
if all
|
|
361
|
+
args << "-A"
|
|
362
|
+
elsif files && !files.empty?
|
|
363
|
+
files.each { |f| args << Shellwords.escape(f) }
|
|
364
|
+
else
|
|
365
|
+
args << "-A"
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Create a commit
|
|
372
|
+
#
|
|
373
|
+
# @param path [String] Repository path
|
|
374
|
+
# @param message [String] Commit message
|
|
375
|
+
# @param author_name [String, nil] Author name (overrides git config)
|
|
376
|
+
# @param author_email [String, nil] Author email (overrides git config)
|
|
377
|
+
# @param allow_empty [Boolean] Allow creating a commit with no changes
|
|
378
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
379
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
380
|
+
# @param cwd [String, nil] Working directory
|
|
381
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
382
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
383
|
+
def commit(path, message, author_name: nil, author_email: nil, allow_empty: false,
|
|
384
|
+
envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
385
|
+
args = ["commit", "-m", Shellwords.escape(message)]
|
|
386
|
+
args << "--allow-empty" if allow_empty
|
|
387
|
+
|
|
388
|
+
commit_envs = (envs || {}).dup
|
|
389
|
+
if author_name && author_email
|
|
390
|
+
commit_envs["GIT_AUTHOR_NAME"] = author_name
|
|
391
|
+
commit_envs["GIT_COMMITTER_NAME"] = author_name
|
|
392
|
+
commit_envs["GIT_AUTHOR_EMAIL"] = author_email
|
|
393
|
+
commit_envs["GIT_COMMITTER_EMAIL"] = author_email
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
run_git(args, path, envs: commit_envs, user: user, cwd: cwd, timeout: timeout)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Reset the current HEAD to a specified state
|
|
400
|
+
#
|
|
401
|
+
# @param path [String] Repository path
|
|
402
|
+
# @param mode [String, nil] Reset mode: "soft", "mixed", "hard", "merge", or "keep"
|
|
403
|
+
# @param target [String, nil] Commit, branch, or tag to reset to
|
|
404
|
+
# @param paths [Array<String>, nil] Specific paths to reset (unstage)
|
|
405
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
406
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
407
|
+
# @param cwd [String, nil] Working directory
|
|
408
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
409
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
410
|
+
def reset(path, mode: nil, target: nil, paths: nil, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
411
|
+
args = ["reset"]
|
|
412
|
+
args << "--#{mode}" if mode
|
|
413
|
+
args << target if target
|
|
414
|
+
|
|
415
|
+
if paths && !paths.empty?
|
|
416
|
+
args << "--"
|
|
417
|
+
paths.each { |p| args << Shellwords.escape(p) }
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Restore working tree files
|
|
424
|
+
#
|
|
425
|
+
# @param path [String] Repository path
|
|
426
|
+
# @param paths [Array<String>] Paths to restore
|
|
427
|
+
# @param staged [Boolean, nil] Restore staged changes (--staged)
|
|
428
|
+
# @param worktree [Boolean, nil] Restore working tree changes (--worktree)
|
|
429
|
+
# @param source [String, nil] Restore from a specific source (commit, branch, etc.)
|
|
430
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
431
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
432
|
+
# @param cwd [String, nil] Working directory
|
|
433
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
434
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
435
|
+
def restore(path, paths, staged: nil, worktree: nil, source: nil, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
436
|
+
args = ["restore"]
|
|
437
|
+
args << "--staged" if staged
|
|
438
|
+
args << "--worktree" if worktree
|
|
439
|
+
args += ["--source", source] if source
|
|
440
|
+
|
|
441
|
+
if paths.is_a?(Array)
|
|
442
|
+
paths.each { |p| args << Shellwords.escape(p) }
|
|
443
|
+
else
|
|
444
|
+
args << Shellwords.escape(paths)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Push commits to a remote repository
|
|
451
|
+
#
|
|
452
|
+
# When +username+ and +password+ are provided, the remote URL is temporarily
|
|
453
|
+
# modified to include credentials, then restored after the push completes.
|
|
454
|
+
#
|
|
455
|
+
# @param path [String] Repository path
|
|
456
|
+
# @param remote [String, nil] Remote name (defaults to "origin" by git)
|
|
457
|
+
# @param branch [String, nil] Branch to push
|
|
458
|
+
# @param set_upstream [Boolean] Set the upstream tracking branch (-u)
|
|
459
|
+
# @param username [String, nil] Username for authentication
|
|
460
|
+
# @param password [String, nil] Password or token for authentication
|
|
461
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
462
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
463
|
+
# @param cwd [String, nil] Working directory
|
|
464
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
465
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
466
|
+
# @raise [E2B::GitAuthError] If authentication fails
|
|
467
|
+
# @raise [E2B::GitUpstreamError] If no upstream is configured and cannot be determined
|
|
468
|
+
def push(path, remote: nil, branch: nil, set_upstream: true, username: nil, password: nil,
|
|
469
|
+
envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
470
|
+
effective_remote = remote || "origin"
|
|
471
|
+
|
|
472
|
+
if username && password
|
|
473
|
+
with_authenticated_remote(path, effective_remote, username, password,
|
|
474
|
+
envs: envs, user: user, cwd: cwd, timeout: timeout) do
|
|
475
|
+
do_push(path, effective_remote, branch, set_upstream,
|
|
476
|
+
envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
477
|
+
end
|
|
478
|
+
else
|
|
479
|
+
do_push(path, effective_remote, branch, set_upstream,
|
|
480
|
+
envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Pull changes from a remote repository
|
|
485
|
+
#
|
|
486
|
+
# When +username+ and +password+ are provided, the remote URL is temporarily
|
|
487
|
+
# modified to include credentials, then restored after the pull completes.
|
|
488
|
+
#
|
|
489
|
+
# @param path [String] Repository path
|
|
490
|
+
# @param remote [String, nil] Remote name (defaults to "origin" by git)
|
|
491
|
+
# @param branch [String, nil] Branch to pull
|
|
492
|
+
# @param username [String, nil] Username for authentication
|
|
493
|
+
# @param password [String, nil] Password or token for authentication
|
|
494
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
495
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
496
|
+
# @param cwd [String, nil] Working directory
|
|
497
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
498
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
499
|
+
# @raise [E2B::GitAuthError] If authentication fails
|
|
500
|
+
def pull(path, remote: nil, branch: nil, username: nil, password: nil,
|
|
501
|
+
envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
502
|
+
effective_remote = remote || "origin"
|
|
503
|
+
|
|
504
|
+
if username && password
|
|
505
|
+
with_authenticated_remote(path, effective_remote, username, password,
|
|
506
|
+
envs: envs, user: user, cwd: cwd, timeout: timeout) do
|
|
507
|
+
do_pull(path, effective_remote, branch,
|
|
508
|
+
envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
509
|
+
end
|
|
510
|
+
else
|
|
511
|
+
do_pull(path, effective_remote, branch,
|
|
512
|
+
envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Set a git configuration value
|
|
517
|
+
#
|
|
518
|
+
# @param key [String] Configuration key (e.g., "user.name")
|
|
519
|
+
# @param value [String] Configuration value
|
|
520
|
+
# @param scope [String] Config scope: "global", "local", or "system"
|
|
521
|
+
# @param path [String, nil] Repository path (required for "local" scope)
|
|
522
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
523
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
524
|
+
# @param cwd [String, nil] Working directory
|
|
525
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
526
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
527
|
+
# @raise [E2B::E2BError] If the scope is invalid
|
|
528
|
+
def set_config(key, value, scope: "global", path: nil, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
529
|
+
validate_scope!(scope)
|
|
530
|
+
|
|
531
|
+
args = ["config", scope_flag(scope), key, Shellwords.escape(value)]
|
|
532
|
+
run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# Get a git configuration value
|
|
536
|
+
#
|
|
537
|
+
# @param key [String] Configuration key (e.g., "user.name")
|
|
538
|
+
# @param scope [String] Config scope: "global", "local", or "system"
|
|
539
|
+
# @param path [String, nil] Repository path (required for "local" scope)
|
|
540
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
541
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
542
|
+
# @param cwd [String, nil] Working directory
|
|
543
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
544
|
+
# @return [String, nil] Configuration value, or nil if not set
|
|
545
|
+
def get_config(key, scope: "global", path: nil, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
546
|
+
validate_scope!(scope)
|
|
547
|
+
|
|
548
|
+
args = ["config", scope_flag(scope), "--get", key]
|
|
549
|
+
result = run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
550
|
+
return nil unless result.success?
|
|
551
|
+
|
|
552
|
+
value = result.stdout.strip
|
|
553
|
+
value.empty? ? nil : value
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Configure a credential helper that accepts any credentials without prompting
|
|
557
|
+
#
|
|
558
|
+
# WARNING: This stores credentials in plaintext in the git credential store.
|
|
559
|
+
# Use only in sandbox environments where security of stored credentials is acceptable.
|
|
560
|
+
#
|
|
561
|
+
# @param username [String] Username for authentication
|
|
562
|
+
# @param password [String] Password or token for authentication
|
|
563
|
+
# @param host [String] Host to authenticate against (default: "github.com")
|
|
564
|
+
# @param protocol [String] Protocol to use (default: "https")
|
|
565
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
566
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
567
|
+
# @param cwd [String, nil] Working directory
|
|
568
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
569
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
570
|
+
def dangerously_authenticate(username, password, host: "github.com", protocol: "https",
|
|
571
|
+
envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
572
|
+
# Configure credential helper to use the store
|
|
573
|
+
set_config("credential.helper", "store", scope: "global",
|
|
574
|
+
envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
575
|
+
|
|
576
|
+
# Write credentials to the credential store via git credential approve
|
|
577
|
+
credential_input = [
|
|
578
|
+
"protocol=#{protocol}",
|
|
579
|
+
"host=#{host}",
|
|
580
|
+
"username=#{username}",
|
|
581
|
+
"password=#{password}",
|
|
582
|
+
""
|
|
583
|
+
].join("\n")
|
|
584
|
+
|
|
585
|
+
escaped_input = Shellwords.escape(credential_input)
|
|
586
|
+
args = ["credential", "approve"]
|
|
587
|
+
cmd = build_git_command(args, nil)
|
|
588
|
+
full_cmd = "echo #{escaped_input} | #{cmd}"
|
|
589
|
+
|
|
590
|
+
merged_envs = DEFAULT_GIT_ENV.merge(envs || {})
|
|
591
|
+
@commands.run(full_cmd, cwd: cwd, envs: merged_envs, timeout: timeout || DEFAULT_TIMEOUT)
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# Configure the git user name and email
|
|
595
|
+
#
|
|
596
|
+
# @param name [String] User name for commits
|
|
597
|
+
# @param email [String] User email for commits
|
|
598
|
+
# @param scope [String] Config scope: "global", "local", or "system"
|
|
599
|
+
# @param path [String, nil] Repository path (required for "local" scope)
|
|
600
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
601
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
602
|
+
# @param cwd [String, nil] Working directory
|
|
603
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
604
|
+
# @return [void]
|
|
605
|
+
def configure_user(name, email, scope: "global", path: nil, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
606
|
+
set_config("user.name", name, scope: scope, path: path,
|
|
607
|
+
envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
608
|
+
set_config("user.email", email, scope: scope, path: path,
|
|
609
|
+
envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
private
|
|
613
|
+
|
|
614
|
+
# Run a git command through the Commands service
|
|
615
|
+
#
|
|
616
|
+
# @param args [Array<String>] Git subcommand and arguments
|
|
617
|
+
# @param repo_path [String, nil] Repository path to pass via -C
|
|
618
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
619
|
+
# @param user [String, nil] User context (reserved for future use)
|
|
620
|
+
# @param cwd [String, nil] Working directory
|
|
621
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
622
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
623
|
+
def run_git(args, repo_path = nil, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
624
|
+
cmd = build_git_command(args, repo_path)
|
|
625
|
+
merged_envs = DEFAULT_GIT_ENV.merge(envs || {})
|
|
626
|
+
@commands.run(cmd, cwd: cwd, envs: merged_envs, timeout: timeout || DEFAULT_TIMEOUT)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# Build a complete git command string
|
|
630
|
+
#
|
|
631
|
+
# @param args [Array<String>] Git subcommand and arguments
|
|
632
|
+
# @param repo_path [String, nil] Repository path to pass via -C
|
|
633
|
+
# @return [String] Complete command string
|
|
634
|
+
def build_git_command(args, repo_path)
|
|
635
|
+
parts = ["git"]
|
|
636
|
+
parts += ["-C", Shellwords.escape(repo_path)] if repo_path
|
|
637
|
+
parts += args
|
|
638
|
+
parts.join(" ")
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# Execute a push operation
|
|
642
|
+
#
|
|
643
|
+
# @param path [String] Repository path
|
|
644
|
+
# @param remote [String] Remote name
|
|
645
|
+
# @param branch [String, nil] Branch to push
|
|
646
|
+
# @param set_upstream [Boolean] Whether to set the upstream tracking branch
|
|
647
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
648
|
+
# @param user [String, nil] User context
|
|
649
|
+
# @param cwd [String, nil] Working directory
|
|
650
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
651
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
652
|
+
def do_push(path, remote, branch, set_upstream, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
653
|
+
args = ["push"]
|
|
654
|
+
args << "-u" if set_upstream
|
|
655
|
+
args << remote
|
|
656
|
+
args << branch if branch
|
|
657
|
+
|
|
658
|
+
result = run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
659
|
+
check_auth_failure!(result)
|
|
660
|
+
check_upstream_failure!(result)
|
|
661
|
+
result
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
# Execute a pull operation
|
|
665
|
+
#
|
|
666
|
+
# @param path [String] Repository path
|
|
667
|
+
# @param remote [String] Remote name
|
|
668
|
+
# @param branch [String, nil] Branch to pull
|
|
669
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
670
|
+
# @param user [String, nil] User context
|
|
671
|
+
# @param cwd [String, nil] Working directory
|
|
672
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
673
|
+
# @return [E2B::Models::ProcessResult] Command result
|
|
674
|
+
def do_pull(path, remote, branch, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
675
|
+
args = ["pull"]
|
|
676
|
+
args << remote
|
|
677
|
+
args << branch if branch
|
|
678
|
+
|
|
679
|
+
result = run_git(args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
680
|
+
check_auth_failure!(result)
|
|
681
|
+
result
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# Temporarily set credentials on a remote URL, execute a block, then restore the original URL
|
|
685
|
+
#
|
|
686
|
+
# @param path [String] Repository path
|
|
687
|
+
# @param remote [String] Remote name
|
|
688
|
+
# @param username [String] Username for authentication
|
|
689
|
+
# @param password [String] Password or token for authentication
|
|
690
|
+
# @param envs [Hash{String => String}, nil] Additional environment variables
|
|
691
|
+
# @param user [String, nil] User context
|
|
692
|
+
# @param cwd [String, nil] Working directory
|
|
693
|
+
# @param timeout [Integer, nil] Command timeout in seconds
|
|
694
|
+
# @yield Block to execute with credentials set on the remote URL
|
|
695
|
+
# @return [Object] Result of the block
|
|
696
|
+
def with_authenticated_remote(path, remote, username, password, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
697
|
+
# Get current remote URL
|
|
698
|
+
original_url = remote_get(path, remote, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
699
|
+
raise E2B::E2BError, "Remote '#{remote}' not found in repository" unless original_url
|
|
700
|
+
|
|
701
|
+
# Set authenticated URL
|
|
702
|
+
authenticated_url = with_credentials(original_url, username, password)
|
|
703
|
+
set_url_args = ["remote", "set-url", remote, Shellwords.escape(authenticated_url)]
|
|
704
|
+
run_git(set_url_args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
705
|
+
|
|
706
|
+
begin
|
|
707
|
+
yield
|
|
708
|
+
ensure
|
|
709
|
+
# Restore original URL (without credentials)
|
|
710
|
+
clean_url = strip_credentials(original_url)
|
|
711
|
+
restore_args = ["remote", "set-url", remote, Shellwords.escape(clean_url)]
|
|
712
|
+
run_git(restore_args, path, envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# Embed credentials into a URL
|
|
717
|
+
#
|
|
718
|
+
# @param url [String] Original URL
|
|
719
|
+
# @param username [String] Username
|
|
720
|
+
# @param password [String] Password or token
|
|
721
|
+
# @return [String] URL with embedded credentials
|
|
722
|
+
def with_credentials(url, username, password)
|
|
723
|
+
uri = URI.parse(url)
|
|
724
|
+
uri.user = URI.encode_www_form_component(username)
|
|
725
|
+
uri.password = URI.encode_www_form_component(password)
|
|
726
|
+
uri.to_s
|
|
727
|
+
rescue URI::InvalidURIError
|
|
728
|
+
# Fallback for URLs that URI cannot parse (e.g., git@ SSH URLs)
|
|
729
|
+
url
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
# Remove credentials from a URL
|
|
733
|
+
#
|
|
734
|
+
# @param url [String] URL potentially containing credentials
|
|
735
|
+
# @return [String] URL without credentials
|
|
736
|
+
def strip_credentials(url)
|
|
737
|
+
uri = URI.parse(url)
|
|
738
|
+
uri.user = nil
|
|
739
|
+
uri.password = nil
|
|
740
|
+
uri.to_s
|
|
741
|
+
rescue URI::InvalidURIError
|
|
742
|
+
url
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Check if a command result indicates an authentication failure
|
|
746
|
+
#
|
|
747
|
+
# @param result [E2B::Models::ProcessResult] Command result to check
|
|
748
|
+
# @raise [E2B::GitAuthError] If authentication failure is detected
|
|
749
|
+
def check_auth_failure!(result)
|
|
750
|
+
return if result.success?
|
|
751
|
+
|
|
752
|
+
stderr = result.stderr.to_s
|
|
753
|
+
if stderr.include?("Authentication failed") ||
|
|
754
|
+
stderr.include?("could not read Username") ||
|
|
755
|
+
stderr.include?("terminal prompts disabled") ||
|
|
756
|
+
stderr.match?(/fatal:.*403/) ||
|
|
757
|
+
stderr.include?("Invalid username or password")
|
|
758
|
+
raise E2B::GitAuthError, "Git authentication failed: #{stderr.strip}"
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# Check if a command result indicates an upstream configuration failure
|
|
763
|
+
#
|
|
764
|
+
# @param result [E2B::Models::ProcessResult] Command result to check
|
|
765
|
+
# @raise [E2B::GitUpstreamError] If upstream failure is detected
|
|
766
|
+
def check_upstream_failure!(result)
|
|
767
|
+
return if result.success?
|
|
768
|
+
|
|
769
|
+
stderr = result.stderr.to_s
|
|
770
|
+
if stderr.include?("has no upstream branch") ||
|
|
771
|
+
stderr.include?("no upstream configured") ||
|
|
772
|
+
stderr.include?("does not appear to be a git repository")
|
|
773
|
+
raise E2B::GitUpstreamError, "Git upstream error: #{stderr.strip}"
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# Validate that a scope string is one of the accepted values
|
|
778
|
+
#
|
|
779
|
+
# @param scope [String] Scope to validate
|
|
780
|
+
# @raise [E2B::E2BError] If the scope is not valid
|
|
781
|
+
def validate_scope!(scope)
|
|
782
|
+
return if VALID_SCOPES.include?(scope)
|
|
783
|
+
|
|
784
|
+
raise E2B::E2BError, "Invalid git config scope '#{scope}'. Must be one of: #{VALID_SCOPES.join(', ')}"
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
# Convert a scope name to its git CLI flag
|
|
788
|
+
#
|
|
789
|
+
# @param scope [String] Scope name ("global", "local", or "system")
|
|
790
|
+
# @return [String] Corresponding git flag
|
|
791
|
+
def scope_flag(scope)
|
|
792
|
+
"--#{scope}"
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# Parse the output of `git status --porcelain=v2 --branch`
|
|
796
|
+
#
|
|
797
|
+
# @param output [String] Raw stdout from git status
|
|
798
|
+
# @return [GitStatus] Parsed status object
|
|
799
|
+
def parse_status(output)
|
|
800
|
+
current_branch = nil
|
|
801
|
+
upstream = nil
|
|
802
|
+
ahead = 0
|
|
803
|
+
behind = 0
|
|
804
|
+
detached = false
|
|
805
|
+
file_status = []
|
|
806
|
+
|
|
807
|
+
output.each_line do |line|
|
|
808
|
+
line = line.chomp
|
|
809
|
+
|
|
810
|
+
case line
|
|
811
|
+
when /\A# branch\.head (.+)\z/
|
|
812
|
+
head = Regexp.last_match(1)
|
|
813
|
+
if head == "(detached)"
|
|
814
|
+
detached = true
|
|
815
|
+
else
|
|
816
|
+
current_branch = head
|
|
817
|
+
end
|
|
818
|
+
when /\A# branch\.upstream (.+)\z/
|
|
819
|
+
upstream = Regexp.last_match(1)
|
|
820
|
+
when /\A# branch\.ab \+(\d+) -(\d+)\z/
|
|
821
|
+
ahead = Regexp.last_match(1).to_i
|
|
822
|
+
behind = Regexp.last_match(2).to_i
|
|
823
|
+
when /\A1 (.)(.) .+ .+ .+ .+ .+ (.+)\z/
|
|
824
|
+
# Ordinary changed entry
|
|
825
|
+
idx = Regexp.last_match(1)
|
|
826
|
+
wt = Regexp.last_match(2)
|
|
827
|
+
filepath = Regexp.last_match(3)
|
|
828
|
+
file_status << GitFileStatus.new(path: filepath, index_status: idx, work_tree_status: wt)
|
|
829
|
+
when /\A2 (.)(.) .+ .+ .+ .+ .+ .+ (.+)\z/
|
|
830
|
+
# Renamed or copied entry (path includes original\ttab\tnew)
|
|
831
|
+
idx = Regexp.last_match(1)
|
|
832
|
+
wt = Regexp.last_match(2)
|
|
833
|
+
filepath = Regexp.last_match(3)
|
|
834
|
+
# The path field for renames is "new_path\told_path"; use the new path
|
|
835
|
+
file_status << GitFileStatus.new(path: filepath.split("\t").first, index_status: idx, work_tree_status: wt)
|
|
836
|
+
when /\Au (.)(.) .+ .+ .+ .+ .+ (.+)\z/
|
|
837
|
+
# Unmerged entry
|
|
838
|
+
idx = Regexp.last_match(1)
|
|
839
|
+
wt = Regexp.last_match(2)
|
|
840
|
+
filepath = Regexp.last_match(3)
|
|
841
|
+
file_status << GitFileStatus.new(path: filepath, index_status: "u", work_tree_status: wt)
|
|
842
|
+
when /\A\? (.+)\z/
|
|
843
|
+
# Untracked file
|
|
844
|
+
filepath = Regexp.last_match(1)
|
|
845
|
+
file_status << GitFileStatus.new(path: filepath, index_status: "?", work_tree_status: "?")
|
|
846
|
+
end
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
GitStatus.new(
|
|
850
|
+
current_branch: current_branch,
|
|
851
|
+
upstream: upstream,
|
|
852
|
+
ahead: ahead,
|
|
853
|
+
behind: behind,
|
|
854
|
+
detached: detached,
|
|
855
|
+
file_status: file_status
|
|
856
|
+
)
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# Parse the output of `git branch -a --format="%(refname:short) %(HEAD)"`
|
|
860
|
+
#
|
|
861
|
+
# @param output [String] Raw stdout from git branch
|
|
862
|
+
# @return [GitBranches] Parsed branch information
|
|
863
|
+
def parse_branches(output)
|
|
864
|
+
current = nil
|
|
865
|
+
local = []
|
|
866
|
+
remote = []
|
|
867
|
+
|
|
868
|
+
output.each_line do |line|
|
|
869
|
+
line = line.chomp.strip
|
|
870
|
+
next if line.empty?
|
|
871
|
+
|
|
872
|
+
# Format: "branch_name *" for current, "branch_name " for others
|
|
873
|
+
# Remote branches appear as "origin/branch_name"
|
|
874
|
+
if line.end_with?("*")
|
|
875
|
+
branch_name = line.sub(/\s*\*\s*\z/, "").strip
|
|
876
|
+
current = branch_name
|
|
877
|
+
local << branch_name unless branch_name.include?("/")
|
|
878
|
+
else
|
|
879
|
+
branch_name = line.strip
|
|
880
|
+
if branch_name.include?("/")
|
|
881
|
+
# Skip HEAD pointer entries like "origin/HEAD"
|
|
882
|
+
remote << branch_name unless branch_name.end_with?("/HEAD")
|
|
883
|
+
else
|
|
884
|
+
local << branch_name
|
|
885
|
+
end
|
|
886
|
+
end
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
GitBranches.new(current: current, local: local, remote: remote)
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
end
|
|
893
|
+
end
|