-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathinit.lua
More file actions
569 lines (518 loc) · 16.5 KB
/
init.lua
File metadata and controls
569 lines (518 loc) · 16.5 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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
local M = {}
local U = require "git-dev.utils"
M.config = {
-- Whether to delete an opened repository when nvim exits.
-- If `true`, it will create an auto command for opened repositories
-- to delete the local directory when nvim exists.
ephemeral = true,
-- Set buffers of opened repositories to be read-only and unmodifiable.
read_only = true,
-- Whether / how to CD into opened repository.
---@type "global"|"tab"|"window"|"none"
cd_type = "global",
-- The actual `open` behavior.
---@param dir string The path to the local repository.
---@param repo_uri string The URI that was used to clone this repository.
---@param selected_path? string A relative path to a file in this repository.
opener = function(dir, repo_uri, selected_path)
M.ui:print("Opening " .. repo_uri)
local dest =
vim.fn.fnameescape(selected_path and dir .. "/" .. selected_path or dir)
vim.cmd("edit " .. dest)
end,
-- Location of cloned repositories. Should be dedicated for this purpose.
repositories_dir = vim.fn.stdpath "cache" .. "/git-dev",
-- Extend the builtin URL parsers.
-- Should map domains to parse functions. See |parser.lua|.
extra_domain_to_parser = nil,
git = {
-- Name / path of `git` command.
command = "git",
-- Default organization if none is specified.
-- If given repository name does not contain '/' and `default_org` is
-- not `nil` nor empty, it will be prepended to the given name.
default_org = nil,
-- Base URI to use when given repository name is scheme-less.
base_uri_format = "https://github.com/%s.git",
-- Arguments for `git clone`.
-- Triggered when repository does not exist locally.
-- It will clone submodules too, disable it if it is too slow.
clone_args = "--jobs=2 --single-branch --recurse-submodules "
.. "--shallow-submodules --progress",
-- Arguments for `git fetch`.
-- Triggered when repository already exists locally to refresh the local
-- copy.
fetch_args = "--jobs=2 --no-all --update-shallow -f --prune --no-tags",
-- Arguments for `git checkout`.
-- Triggered by `open` when a branch, tag or commit is given.
checkout_args = "-f --recurse-submodules",
},
-- UI configuration.
ui = {
-- Whether to enable builtin output buffer or fallback to `vim.notify`.
enabled = true,
-- Auto-close window after repository was opened.
auto_close = true,
-- Delay window closing.
close_after_ms = 3000,
-- Window mode.
---@type "floating"|"split"
mode = "floating",
-- Window configuration for floating mode.
-- See `:h nvim_open_win`.
---@type win_config
floating_win_config = {
title = "git-dev",
title_pos = "center",
anchor = "NE",
style = "minimal",
border = "rounded",
relative = "editor",
width = 79,
height = 9,
row = 1,
col = vim.o.columns,
noautocmd = true,
},
-- Window configuration for split mode.
-- See `:h nvim_open_win`.
---@type win_config
split_win_config = {
split = "right",
width = 79,
noautocmd = true,
},
},
-- History configuration.
history = {
-- Maximum number of records to keep in history.
n = 32,
-- Store file path.
path = vim.fn.stdpath "data" .. "/git-dev/history.json",
},
-- Repository cleaning configuration.
clean = {
-- Close all related buffers.
close_buffers = true,
-- Whether to delete repository directory, keep it, or determine deletion
-- by its current ephemeral setting.
---@type "always"|"never"|"current"
delete_repo_dir = "current",
},
-- XDG handling of `nvim-getdev` URIs.
-- Requires: `xdg-mime` and `xdg-open`.
xdg_handler = {
enabled = false,
-- A location for the desktop entry.
desktop_entry_path = vim.fs.normalize(
vim.fn.stdpath "data" .. "/../applications/git-dev.desktop"
),
-- Launcher script.
script = {
path = vim.fn.expand "~/.local/bin/git-dev-open",
content = '#!/usr/bin/env sh\nnvim -c GitDevXDGHandle\\ "$@"',
},
},
-- More verbosity.
verbose = false,
}
M.session = nil
M.history = nil
local cd_func = {
global = U.cmd_to_func "cd",
tab = U.cmd_to_func "tcd",
window = U.cmd_to_func "lcd",
none = function(_, _) end,
}
local function delete_repo_dir(repo_dir)
local is_deleted = vim.fn.delete(repo_dir, "rf")
local msg
if is_deleted == 0 then
msg = "Deleted: " .. repo_dir
else
msg = "Not found: " .. repo_dir
end
M.ui:print(msg)
end
local augroup = vim.api.nvim_create_augroup("GitDev", { clear = true })
---@class GitRef Holds a reference for a repository. At least one field must
--- not be `nil`.
---@field branch? string
---@field tag? string
---@field commit? string
---Opens a repository.
---It will clone / refresh repository directory,
---@param repo string Git URI or repository name.
---@param ref? GitRef If more than one is specified, the priority is:
--- `commit` > `tag` > `branch`.
---@param opts? table Override plugin settings.
M.open = function(repo, ref, opts)
if not repo then
vim.notify "Missing repository. See |:h git-dev-usage-open|"
return
end
ref = ref or {}
local config = vim.tbl_deep_extend("force", M.config, opts or {})
local ui = M.ui
ui:show()
ui:print("Opening " .. repo):emit()
local gitcmd = require("git-dev.gitcmd"):init {
cmd = config.git.command,
on_output = function(err, data)
if data and config.verbose then
ui:print(data)
end
if err then
ui:print("ERROR: " .. err)
end
end,
}
local parser = require("git-dev.parser"):init {
gitcmd = gitcmd,
base_uri_format = config.git.base_uri_format,
extra_domain_to_parser = config.extra_domain_to_parser,
}
ui:print("Parsing: " .. repo)
local parsed_repo = parser:parse(repo)
local branch = ref.commit
or ref.tag
or ref.branch
or parsed_repo.commit
or parsed_repo.branch
if not branch and parsed_repo.full_blob then
ui:print "Could not detect a ref in given URI, falling back to default."
end
local repo_dir = config.repositories_dir
.. "/"
.. U.git_uri_to_dir_name(parsed_repo.repo_url, branch)
if config.verbose then
ui:print(parsed_repo)
end
local post_action_callback = function()
if vim.fn.isdirectory(repo_dir) == 0 then
ui:print(
"Repository not found at: " .. parsed_repo.repo_url .. ", aborting..."
)
return false
end
if branch then
ui:print "Making sure that the correct branch / tag is checked out..."
gitcmd:checkout {
repo_dir = repo_dir,
ref = branch,
extra_args = config.git.checkout_args,
}
end
---@type GitDevSessionRepo
local repo_ctx = {
repo = parsed_repo.repo_url,
ref = ref,
repo_dir = repo_dir,
}
if config.ephemeral then
ui:print "Ephemeral mode: creating autocmd to cleanup when nvim exits..."
-- Delete repository directory when vim exits.
repo_ctx.ephemeral_autocmd_id = vim.api.nvim_create_autocmd(
{ "VimLeavePre" },
{
group = augroup,
callback = function()
delete_repo_dir(repo_dir)
end,
}
)
end
if config.read_only then
-- Set all buffers in the repository directory as read-only
-- and unmodifiable.
ui:print("Setting read only mode for " .. repo_dir)
repo_ctx.read_only_autocmd_id = vim.api.nvim_create_autocmd(
{ "BufReadPost" },
{
group = augroup,
pattern = repo_dir .. "*",
callback = function()
vim.cmd "setlocal readonly nomodifiable"
end,
}
)
end
local ctx_id = M.session:set_repo(repo_ctx)
repo_ctx.set_session_autocmd_id = vim.api.nvim_create_autocmd(
{ "BufReadPost" },
{
group = augroup,
pattern = repo_dir .. "*",
callback = function(t)
vim.api.nvim_buf_set_var(t.buf, "git_dev_session_id", ctx_id)
end,
}
)
-- Open directory (or selected path)
config.opener(repo_dir, parsed_repo.repo_url, parsed_repo.selected_path)
-- CD into repository directory
cd_func[config.cd_type](
{ vim.fn.fnameescape(repo_dir) },
not config.verbose
)
-- Add this call to history store.
M.history:add(repo, ref, opts, parsed_repo)
ui:print "Done."
end
local clone_callback = vim.schedule_wrap(function()
pcall(post_action_callback)
if config.ui.auto_close then
ui:close(config.ui.close_after_ms)
end
end)
if vim.fn.isdirectory(repo_dir) == 1 then
ui:print "Repository directory exists, refreshing..."
gitcmd:refresh(
{ repo_dir = repo_dir, extra_args = config.git.fetch_args },
clone_callback
)
else
-- Fresh clone
ui:print "Repository does not exist locally, cloning..."
gitcmd:clone({
repo_url = parsed_repo.repo_url,
repo_dir = repo_dir,
branch = parsed_repo.branch,
extra_args = config.git.clone_args,
}, clone_callback)
end
end
---Cleans all repositories in the repositories directory
---DANGER: Make sure that the repositories directory is exclusive to
---this plugin.
M.clean_all = function()
vim.fn.delete(M.config.repositories_dir)
end
---@return GitDevSessionRepo?
local function get_session_repo(repo, ref)
ref = ref or {}
if repo then
local ids = M.session:find {
{ name = "repo", value = repo, type = "contains" },
{ name = "ref", value = ref, type = "equal" },
}
if #ids == 0 then
vim.notify "Could not find related repository session."
return
elseif #ids > 1 then
vim.notify "More than one related repository sessions."
return
else
return M.session:get_repo(ids[1])
end
end
-- Fallback to current buffer
local ctx_id = U.buf_get_var(nil, "git_dev_session_id")
if ctx_id then
return M.session:get_repo(ctx_id)
end
end
---Closes (deletes) all buffers associated with the given repository and ref.
---If none was given, it will try to determine the repository directory from
---current buffer.
---@param repo? string
---@param ref? GitRef
---@return string[]? @An array of closed paths.
M.close_buffers = function(repo, ref)
local repo_ctx = get_session_repo(repo, ref)
if not repo_ctx then
vim.notify "Could not determine repository session."
return nil
end
local ctx_id = M.session:key(repo_ctx)
local deleted = {}
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
local other = U.buf_get_var(buf, "git_dev_session_id")
if ctx_id == other then
table.insert(deleted, vim.api.nvim_buf_get_name(buf))
vim.api.nvim_buf_delete(buf, { force = true })
end
end
return deleted
end
---Cleans a repository. It will close all associated buffers and delete the
---repository directory if it was ephemeral.
---If no repository is given, assume it is related to current buffer.
---If no ref is given, assume it is related to current buffer if an explicit
---repository was given.
---@param repo? string
---@param ref? GitRef
---@param opts? table Override plugin settings.
M.clean = function(repo, ref, opts)
local config = vim.tbl_deep_extend("force", M.config, opts or {})
local repo_ctx = get_session_repo(repo, ref)
if not repo_ctx then
vim.notify "Could not determine repository session."
return
end
if config.clean.close_buffers then
if not M.close_buffers(repo_ctx.repo, repo_ctx.ref) then
return
end
end
if vim.uv.cwd() == repo_ctx.repo_dir then
vim.fn.chdir(vim.env.PWD)
end
-- If repository is marked as ephemeral, remove its directory.
if
config.clean.delete_repo_dir == "always"
or config.clean.delete_repo_dir == "current"
and repo_ctx.ephemeral_autocmd_id
then
delete_repo_dir(repo_ctx.repo_dir)
end
-- Delete autocmds
if repo_ctx.ephemeral_autocmd_id then
vim.api.nvim_del_autocmd(repo_ctx.ephemeral_autocmd_id)
end
if repo_ctx.read_only_autocmd_id then
vim.api.nvim_del_autocmd(repo_ctx.read_only_autocmd_id)
end
vim.api.nvim_del_autocmd(repo_ctx.set_session_autocmd_id)
-- Remove from session
M.session:remove(M.session:key(repo_ctx))
end
---Parses a Git URL.
---@param repo string
---@return GitDevParsedRepo
M.parse = function(repo, opts)
local config = vim.tbl_deep_extend("force", M.config, opts or {})
local gitcmd = require("git-dev.gitcmd"):init { cmd = config.git.command }
local parser = require("git-dev.parser"):init {
gitcmd = gitcmd,
base_uri_format = config.git.base_uri_format,
extra_domain_to_parser = config.extra_domain_to_parser,
}
return parser:parse(repo)
end
---Toggle UI Window
---@param win_config? vim.api.keyset.win_config
M.toggle_ui = function(win_config)
M.ui:toggle(win_config)
end
local function complete_from_session(_, line, pos)
local argv = vim.split(line:sub(1, pos), " +")
if #argv < 2 then
return {}
end
local repo_ids =
M.session:find { { name = "repo", value = argv[2], type = "contains" } }
if #argv == 2 then
return U.uniq(U.map(function(k)
return M.session:get_repo(k).repo
end, repo_ids))
elseif #argv == 3 then
local refs = U.map(function(k)
local ref = M.session:get_repo(k).ref
if ref and vim.tbl_count(ref) > 0 then
return vim.inspect(ref):gsub("%s", "")
end
return "{}"
end, repo_ids)
table.insert(refs, "{}")
return U.uniq(refs)
end
return {}
end
---Plugin Setup
---@param opts? table Module config table. See |M.config|.
M.setup = function(opts)
M.config = vim.tbl_deep_extend("force", M.config, opts or {})
-- Create needed directories if non existent.
vim.fn.mkdir(M.config.repositories_dir, "p")
vim.fn.mkdir(vim.fs.dirname(M.config.history.path), "p")
-- Prepare UI
if M.config.ui.enabled then
local win_config
if M.config.ui.mode == "split" then
local v = vim.version()
-- `nvim_open_win` supports splitting in Neovim>=0.10.0
-- `ge` was added in 0.10.0.
if
vim.version.lt({ v.major, v.minor, v.patch }, { 0, 10, 0 }) == false
then
win_config = M.config.ui.split_win_config
else
vim.notify(
"Split mode is not supported in Neovim < 0.10.0. "
.. "Falling back to floating mode."
)
win_config = M.config.ui.floating_win_config
end
else
win_config = M.config.ui.floating_win_config
end
M.ui = require("git-dev.ui"):init {
win_config = win_config,
}
vim.api.nvim_create_user_command("GitDevToggleUI", function(_)
require("git-dev").toggle_ui()
end, {
desc = "Toggle output window.",
})
else
M.ui = require("git-dev.uistub"):init {}
end
-- Initialize store
---@type GitDevHistory
M.history = require("git-dev.history"):init(M.config.history)
-- Initialize session
---@type GitDevSession
M.session = require("git-dev.session"):init()
local xdg = require "git-dev.xdg"
if M.config.xdg_handler.enabled then
xdg.enable(M.config.xdg_handler)
vim.api.nvim_create_user_command("GitDevXDGHandle", function(cmd_args)
local uri = U.parse_cmd_args(cmd_args)
xdg.handle(uri)
end, {
desc = "xdg-open handler for git-dev.nvim URIs.",
nargs = "*",
})
else
xdg.disable(M.config.xdg_handler)
end
-- Create commands
vim.api.nvim_create_user_command("GitDevOpen", function(cmd_args)
local repo, ref, cmd_opts = U.parse_cmd_args(cmd_args)
require("git-dev").open(repo, ref, cmd_opts)
end, {
desc = "Open a git repository.",
nargs = "*",
})
vim.api.nvim_create_user_command("GitDevCleanAll", function(_)
require("git-dev").clean_all()
end, {
desc = "Delete the repositories directory. CAUTION: be careful "
.. "with custom paths.",
})
vim.api.nvim_create_user_command("GitDevCloseBuffers", function(cmd_args)
local repo, ref = U.parse_cmd_args(cmd_args)
require("git-dev").close_buffers(repo, ref)
end, {
desc = "Close (delete) all buffers associated with the same "
.. "repository as the current buffer.",
nargs = "*",
complete = complete_from_session,
})
vim.api.nvim_create_user_command("GitDevClean", function(cmd_args)
local repo, ref, cmd_opts = U.parse_cmd_args(cmd_args)
require("git-dev").clean(repo, ref, cmd_opts)
end, {
desc = "Close related buffers, delete repository directory "
.. "and remove repository from history store.",
nargs = "*",
complete = complete_from_session,
})
vim.api.nvim_create_user_command(
"GitDevRecents",
"Telescope git_dev recents",
{ desc = "Revisit previously opened repositories." }
)
end
return M