This repository was archived by the owner on Jan 14, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgit.lua
More file actions
268 lines (232 loc) · 8.3 KB
/
Copy pathgit.lua
File metadata and controls
268 lines (232 loc) · 8.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
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
267
268
---@class CodeCompanion.GitCommit.Git
local Git = {}
-- Store configuration
local config = {}
---Setup Git module with configuration
---@param opts? table Configuration options
function Git.setup(opts)
config = vim.tbl_deep_extend("force", {
exclude_files = {},
}, opts or {})
end
---Filter diff content to exclude specified file patterns
---@param diff_content string The original diff content
---@return string filtered_diff The filtered diff content
function Git._filter_diff(diff_content)
if not config.exclude_files or #config.exclude_files == 0 then
return diff_content
end
local lines = vim.split(diff_content, "\n")
local filtered_lines = {}
local current_file = nil
local skip_current_file = false
for _, line in ipairs(lines) do
-- Check for file header (diff --git a/file b/file)
local file_match = line:match("^diff %-%-git a/(.*) b/")
if file_match then
current_file = file_match
skip_current_file = Git._should_exclude_file(current_file)
end
-- Check for traditional diff format (+++ b/file, --- a/file)
local plus_file = line:match("^%+%+%+ b/(.*)")
local minus_file = line:match("^%-%-%-a/(.*)")
if plus_file then
current_file = plus_file
skip_current_file = Git._should_exclude_file(current_file)
elseif minus_file then
current_file = minus_file
skip_current_file = Git._should_exclude_file(current_file)
end
-- Only include line if we're not skipping current file
if not skip_current_file then
table.insert(filtered_lines, line)
end
end
return table.concat(filtered_lines, "\n")
end
---Check if file should be excluded based on patterns
---@param filepath string The file path to check
---@return boolean should_exclude True if file should be excluded
function Git._should_exclude_file(filepath)
if not config.exclude_files then
return false
end
for _, pattern in ipairs(config.exclude_files) do
-- Convert glob pattern to Lua pattern
local lua_pattern = pattern:gsub("%*", ".*"):gsub("?", ".")
if filepath:match(lua_pattern) then
return true
end
end
return false
end
---Check if current directory is inside a git repository
---@return boolean
function Git.is_repository()
-- First check for .git directory in current and parent directories
local function check_git_dir(path)
local sep = package.config:sub(1, 1)
local git_path = path .. sep .. ".git"
local stat = vim.uv.fs_stat(git_path)
return stat ~= nil
end
-- Search from current directory upwards
local current_dir = vim.fn.getcwd()
while current_dir do
if check_git_dir(current_dir) then
return true
end
-- Move to parent directory
local parent = vim.fn.fnamemodify(current_dir, ":h")
if parent == current_dir then
-- Reached root directory
break
end
current_dir = parent
end
-- Fallback to git command if filesystem check fails
local redirect = (vim.loop.os_uname().sysname == "Windows_NT") and " 2>nul" or " 2>/dev/null"
local cmd = "git rev-parse --is-inside-work-tree" .. redirect
local result = vim.fn.system(cmd)
return vim.v.shell_error == 0 and vim.trim(result) == "true"
end
---Check if currently in git commit --amend state
---@return boolean
function Git.is_amending()
if not Git.is_repository() then
return false
end
-- Check if COMMIT_EDITMSG exists and we're in a rebase/merge state
local git_dir = vim.fn.system("git rev-parse --git-dir"):gsub("\n", "")
if vim.v.shell_error ~= 0 then
return false
end
-- Check for COMMIT_EDITMSG file which indicates we're editing a commit
local commit_editmsg = git_dir .. "/COMMIT_EDITMSG"
local stat = vim.uv.fs_stat(commit_editmsg)
if not stat then
return false
end
-- Additional check: see if we have HEAD commit (not initial commit)
local head_check = vim.fn.system("git rev-parse --verify HEAD")
return vim.v.shell_error == 0
end
---Get git diff for staged changes or last commit (for amend)
---@return string|nil diff The changes diff, nil if no changes or error
function Git.get_staged_diff()
if not Git.is_repository() then
return nil
end
-- First try to get staged changes
local staged_diff = vim.fn.system("git diff --no-ext-diff --staged")
if vim.v.shell_error == 0 and vim.trim(staged_diff) ~= "" then
return Git._filter_diff(staged_diff)
end
-- If no staged changes and we're in amend mode, get the last commit's changes
if Git.is_amending() then
local last_commit_diff = vim.fn.system("git diff --no-ext-diff HEAD~1")
if vim.v.shell_error == 0 and vim.trim(last_commit_diff) ~= "" then
return Git._filter_diff(last_commit_diff)
end
-- Fallback: if HEAD~1 doesn't exist (initial commit), show all files
local show_diff = vim.fn.system("git show --no-ext-diff --format= HEAD")
if vim.v.shell_error == 0 and vim.trim(show_diff) ~= "" then
return Git._filter_diff(show_diff)
end
end
return nil
end
---Get contextual diff based on current git state
---@return string|nil diff The relevant diff, nil if no changes
---@return string context The context of what diff represents
function Git.get_contextual_diff()
if not Git.is_repository() then
return nil, "not_in_repo"
end
-- Check for staged changes first
local staged_diff = vim.fn.system("git diff --no-ext-diff --staged")
if vim.v.shell_error == 0 and vim.trim(staged_diff) ~= "" then
local filtered_diff = Git._filter_diff(staged_diff)
if vim.trim(filtered_diff) ~= "" then
return filtered_diff, "staged"
else
return nil, "no_changes_after_filter"
end
end
-- Check if we're amending
if Git.is_amending() then
-- Try to get the last commit's diff
local last_commit_diff = vim.fn.system("git diff --no-ext-diff HEAD~1")
if vim.v.shell_error == 0 and vim.trim(last_commit_diff) ~= "" then
local filtered_diff = Git._filter_diff(last_commit_diff)
if vim.trim(filtered_diff) ~= "" then
return filtered_diff, "amend_with_parent"
end
end
-- Fallback for initial commit amend
local show_diff = vim.fn.system("git show --no-ext-diff --format= HEAD")
if vim.v.shell_error == 0 and vim.trim(show_diff) ~= "" then
local filtered_diff = Git._filter_diff(show_diff)
if vim.trim(filtered_diff) ~= "" then
return filtered_diff, "amend_initial"
end
end
end
-- Fallback: If no staged changes and not amending, check for all local changes (working directory vs. HEAD)
local all_local_diff = vim.fn.system("git diff --no-ext-diff HEAD")
if vim.v.shell_error == 0 and vim.trim(all_local_diff) ~= "" then
local filtered_diff = Git._filter_diff(all_local_diff)
if vim.trim(filtered_diff) ~= "" then
return filtered_diff, "unstaged_or_all_local"
else
return nil, "no_changes_after_filter"
end
end
return nil, "no_changes"
end
---Commit changes with the provided message
---@param message string The commit message
---@return boolean success True if commit was successful, false otherwise
function Git.commit_changes(message)
if not Git.is_repository() then
vim.notify("Not in a git repository", vim.log.levels.ERROR)
return false
end
-- Check if there are changes to commit
local diff, context = Git.get_contextual_diff()
if not diff then
if context == "no_changes" then
if Git.is_amending() then
vim.notify("No changes to amend. The commit already exists.", vim.log.levels.WARN)
else
vim.notify(
"No changes found to commit. Please stage your changes or ensure there are unstaged changes in your working directory.",
vim.log.levels.ERROR
)
end
end
return false
end
-- Pass commit message directly through stdin without temporary files
local cmd
if Git.is_amending() then
cmd = "git commit --amend -F -"
else
cmd = "git commit -F -"
end
local result = vim.fn.system(cmd, message)
local exit_code = vim.v.shell_error
if exit_code == 0 then
local action = Git.is_amending() and "amended" or "committed"
vim.notify(string.format("Successfully %s changes!", action), vim.log.levels.INFO)
return true
else
local error_msg = vim.trim(result)
if error_msg == "" then
error_msg = "Unknown error occurred during commit"
end
vim.notify("Failed to commit: " .. error_msg, vim.log.levels.ERROR)
return false
end
end
return Git