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
307 lines (252 loc) · 8.45 KB
/
git.lua
File metadata and controls
307 lines (252 loc) · 8.45 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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
---@class CodeCompanion.GitCommit.Git
local Git = {}
local config = {}
local REPO_CACHE_TTL_MS = 1000
local _repo_cache = { valid = false, timestamp = 0, result = false, cwd = nil }
---@param opts? table Configuration options
function Git.setup(opts)
config = vim.tbl_deep_extend("force", {
exclude_files = {},
use_commit_history = true,
commit_history_count = 10,
}, opts or {})
end
---@param diff_content string The original diff content
---@return string filtered_diff The filtered diff content
function Git._filter_diff(diff_content)
return GitUtils.filter_diff(diff_content, config.exclude_files)
end
---@param filepath string The file path to check
---@return boolean should_exclude True if file should be excluded
function Git._should_exclude_file(filepath)
return GitUtils.should_exclude_file(filepath, config.exclude_files)
end
function Git.is_repository()
local current_cwd = vim.fn.getcwd()
local now = vim.uv.now()
if _repo_cache.valid and _repo_cache.cwd == current_cwd and (now - _repo_cache.timestamp) < REPO_CACHE_TTL_MS then
return _repo_cache.result
end
local function check_git_dir(path)
local git_path = GitUtils.path_join(path, ".git")
local stat = vim.uv.fs_stat(git_path)
return stat ~= nil
end
local check_dir = current_cwd
while check_dir do
if check_git_dir(check_dir) then
_repo_cache = { valid = true, timestamp = now, result = true, cwd = current_cwd }
return true
end
local parent = vim.fn.fnamemodify(check_dir, ":h")
if parent == check_dir then
break
end
check_dir = parent
end
local cmd = { "git", "rev-parse", "--is-inside-work-tree" }
local result = vim.fn.system(cmd)
local is_repo = vim.v.shell_error == 0 and vim.trim(result) == "true"
_repo_cache = { valid = true, timestamp = now, result = is_repo, cwd = current_cwd }
return is_repo
end
function Git.is_amending()
local ok, result = pcall(function()
if not Git.is_repository() then
return false
end
local git_dir = vim.trim(vim.fn.system("git rev-parse --git-dir"))
if vim.v.shell_error ~= 0 then
return false
end
local commit_editmsg = GitUtils.path_join(git_dir, "COMMIT_EDITMSG")
local stat = vim.uv.fs_stat(commit_editmsg)
if not stat then
return false
end
vim.fn.system({ "git", "rev-parse", "--verify", "HEAD" })
if vim.v.shell_error ~= 0 then
return false
end
local fd = vim.uv.fs_open(commit_editmsg, "r", 438)
if not fd then
return false
end
local content = vim.uv.fs_read(fd, stat.size, 0)
vim.uv.fs_close(fd)
if not content then
return false
end
local lines = vim.split(content, "\n")
local has_existing_message = false
for _, line in ipairs(lines) do
local trimmed = vim.trim(line)
if trimmed ~= "" and not trimmed:match("^#") then
has_existing_message = true
break
end
end
return has_existing_message
end)
return ok and result or false
end
function Git.get_staged_diff()
local ok, result = pcall(function()
if not Git.is_repository() then
return nil
end
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 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
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)
return ok and result or nil
end
---@return string|nil diff The diff content or nil if no changes
---@return string|nil context The context describing the diff type
function Git.get_contextual_diff()
local ok, diff, context = pcall(function()
if not Git.is_repository() then
return nil, "not_in_repo"
end
local staged_diff = vim.fn.system("git diff --no-ext-diff --staged")
if vim.v.shell_error == 0 and GitUtils.trim(staged_diff) ~= "" then
local filtered_diff = Git._filter_diff(staged_diff)
if GitUtils.trim(filtered_diff) ~= "" then
return filtered_diff, "staged"
else
return nil, "no_changes_after_filter"
end
end
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 GitUtils.trim(last_commit_diff) ~= "" then
local filtered_diff = Git._filter_diff(last_commit_diff)
if GitUtils.trim(filtered_diff) ~= "" then
return filtered_diff, "amend_with_parent"
end
end
local show_diff = vim.fn.system("git show --no-ext-diff --format= HEAD")
if vim.v.shell_error == 0 and GitUtils.trim(show_diff) ~= "" then
local filtered_diff = Git._filter_diff(show_diff)
if GitUtils.trim(filtered_diff) ~= "" then
return filtered_diff, "amend_initial"
end
end
end
local all_local_diff = vim.fn.system("git diff --no-ext-diff HEAD")
if vim.v.shell_error ~= 0 then
all_local_diff = vim.fn.system("git diff --no-ext-diff")
if vim.v.shell_error ~= 0 then
return nil, "git_operation_failed"
end
end
if GitUtils.trim(all_local_diff) ~= "" then
local filtered_diff = Git._filter_diff(all_local_diff)
if GitUtils.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)
if not ok then
return nil, "git_operation_failed"
end
return diff, context
end
function Git.commit_changes(message)
local ok, success = pcall(function()
if not Git.is_repository() then
vim.notify("Not in a git repository", vim.log.levels.ERROR)
return false
end
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
elseif context == "git_operation_failed" then
vim.notify("Git operation failed. Please check your git repository.", vim.log.levels.ERROR)
end
return false
end
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)
if not ok then
vim.notify("Git commit operation failed unexpectedly", vim.log.levels.ERROR)
return false
end
return success
end
---@param count? number Number of recent commits to retrieve (default: 10)
---@return string[]|nil commit_messages Array of commit messages or nil on error
function Git.get_commit_history(count)
count = count or 10
local ok, result = pcall(function()
if not Git.is_repository() then
return nil
end
local cmd = string.format("git log --pretty=format:%%s --no-merges -%d", count)
local output = vim.fn.system(cmd)
if vim.v.shell_error ~= 0 then
return nil
end
local lines = vim.split(output, "\n")
local commit_messages = {}
for _, line in ipairs(lines) do
local trimmed = GitUtils.trim(line)
if trimmed ~= "" then
table.insert(commit_messages, trimmed)
end
end
return commit_messages
end)
if not ok then
return nil
end
return result
end
---@return table config Current configuration
function Git.get_config()
return vim.deepcopy(config)
end
return Git