xaizek / vifm (License: GPLv2+) (since 2018-12-07)
Vifm is a file manager with curses interface, which provides Vi[m]-like environment for managing objects within file systems, extended with some useful ideas from mutt.
Commit 56793a51aa502cd0def294d52e3db3f94f35a184

git: add :Gopt to config marking unchanged files
Author: xaizek
Author date (UTC): 2026-04-11 16:34
Committer name: xaizek
Committer date (UTC): 2026-04-11 16:42
Parent(s): 62b97163b4da3541a1075b4995052089ff01a6ec
Signing key: 99DC5E4DB05F6BE2
Tree: 50baea7f73486c48db8902e3f9d5e6bf5efd8371
File Lines added Lines deleted
data/plugins/git/README.md 23 5
data/plugins/git/init.lua 26 0
data/plugins/git/statuses.lua 30 17
File data/plugins/git/README.md changed (mode: 100644) (index 6a30d5232..5d1e7b112)
... ... specific ones.
48 48 wildcard that matches any character. wildcard that matches any character.
49 49 2. Replacement (required). 2. Replacement (required).
50 50
51 ### `:Gopt` command
52
53 **Syntax:** `:Gmap option...`
54
55 Configures behaviour by processing one or more options.
56
57 Default state (see **Parameters** section below for explanations):
58 - `unchanged`
59
60 **Examples:**
61
62 * `:Gopt nounchanged` disables marking unchanged files in any way
63 * `:Gopt unchanged` enabling marking unchanged files as `GG`
64
65 **Parameters:**
66
67 - `unchanged` -- mark unchanged files as `GG`
68 - `nounchanged` -- don't mark unchanged files (` `)
69
51 70 ### `GitStatus` view column ### `GitStatus` view column
52 71
53 72 When inside a Git repository, displays status of files and directories in a way When inside a Git repository, displays status of files and directories in a way
 
... ... similar to how `git status --short` does it.
57 76
58 77 | Value | Meaning | Value | Meaning
59 78 | ----- | ------- | ----- | -------
60 | ` ` | Ignored.
79 | ` ` | Ignored or unchanged (configurable, see `:Gopt`).
61 80 | ` M` | Modified in worktree. | ` M` | Modified in worktree.
62 81 | `MM` | Modified in index and in worktree. | `MM` | Modified in index and in worktree.
63 82 | `AM` | Added in index, modified in worktree. | `AM` | Added in index, modified in worktree.
 
... ... similar to how `git status --short` does it.
74 93 | `DU` | Deleted here, but updated in merged changes. | `DU` | Deleted here, but updated in merged changes.
75 94 | `AA` | Added here and in merged changes. | `AA` | Added here and in merged changes.
76 95 | `UU` | Modified here and in merged changes. | `UU` | Modified here and in merged changes.
77 | `GG` | Unchanged.
96 | `GG` | Unchanged (configurable, see `:Gopt`).
78 97 | `??` | Untracked. | `??` | Untracked.
79 98
80 99 **Possible column values for directories:** **Possible column values for directories:**
 
... ... similar to how `git status --short` does it.
99 118
100 119 **TODO:** **TODO:**
101 120
102 * Could add `:Gopt` to make some behaviour optional:
121 * Could make more behaviour optional via `:Gopt`:
103 122 - asynchronous/synchronous: as a workaround for large repositories - asynchronous/synchronous: as a workaround for large repositories
104 - marking unchanged file: also workaround but also a user's choice (e.g.,
105 marking unchanged or ignored files)
123 - marking ignored files
106 124 - discovering git repositories and showing their statuses - discovering git repositories and showing their statuses
107 125 * Reuse old cache for entire subtree while it's being updated (only a specific * Reuse old cache for entire subtree while it's being updated (only a specific
108 126 path gets to reuse it right now). path gets to reuse it right now).
File data/plugins/git/init.lua changed (mode: 100644) (index 93a5e1f05..4605eb16a)
... ... local function map_command(info)
48 48 map_rules[#map_rules + 1] = info.argv map_rules[#map_rules + 1] = info.argv
49 49 end end
50 50
51 local function opt_command(info)
52 local need_reset = false
53
54 for _, arg in ipairs(info.argv) do
55 if arg == 'unchanged' then
56 statuses.show_unchanged = true
57 need_reset = true
58 elseif arg == 'nounchanged' then
59 statuses.show_unchanged = false
60 need_reset = true
61 end
62 end
63
64 if need_reset then
65 statuses.reset()
66 end
67 end
68
51 69 local function char_matches(char, pattern) local function char_matches(char, pattern)
52 70 return pattern == '*' or char == pattern return pattern == '*' or char == pattern
53 71 end end
 
... ... add_cmd {
110 128 minargs = 2, minargs = 2,
111 129 } }
112 130
131 add_cmd {
132 name = "Gopt",
133 description = "customizes behaviour of the git plugin",
134 handler = opt_command,
135 minargs = 1,
136 maxargs = -1,
137 }
138
113 139 local added = vifm.addcolumntype { local added = vifm.addcolumntype {
114 140 name = "GitStatus", name = "GitStatus",
115 141 handler = status_column handler = status_column
File data/plugins/git/statuses.lua changed (mode: 100644) (index 5dd764edb..eb11a4803)
1 local M = { }
1 local M = {
2 show_unchanged = true,
3 }
2 4
3 5 -- a user may be making changes in a repository quite frequently -- a user may be making changes in a repository quite frequently
4 6 local GIT_DIR_TTL = 5 local GIT_DIR_TTL = 5
5 7 -- repositories are created infrequently -- repositories are created infrequently
6 8 local NON_GIT_DIR_TTL = 60 local NON_GIT_DIR_TTL = 60
7 9
8 local cache = {
9 in_git = false, -- whether current path is covered by Git
10 has_repos = false, -- whether current path is covered by Git
11 status = nil, -- status derived from nested files
12 subs = { }, -- subdirectory name -> cache node
13 items = { }, -- file name -> status
14 expires = 0 -- when the cache entry expires
15 }
10 -- root node of the cache, initialized by M.reset()
11 local cache
16 12
17 13 local function find_node(path) local function find_node(path)
18 14 local current = cache local current = cache
 
... ... function M.get(at)
244 240 end end
245 241 } }
246 242
247 fill_node {
248 node = node,
249 cmd = string.format('git -C %s ls-tree HEAD -r --name-only -z .', vifm.escape(at)),
250 callback = function(status_all)
251 for rel_path in string.gmatch(status_all, '[^\0]+') do
252 set_file_status(node, rel_path, 'GG', expires)
243 if M.show_unchanged then
244 fill_node {
245 node = node,
246 cmd = string.format('git -C %s ls-tree HEAD -r --name-only -z .', vifm.escape(at)),
247 callback = function(status_all)
248 for rel_path in string.gmatch(status_all, '[^\0]+') do
249 set_file_status(node, rel_path, 'GG', expires)
250 end
253 251 end end
254 end
255 }
252 }
253 end
256 254
257 255 return node.past or node return node.past or node
258 256 end end
259 257
258 function M.reset()
259 cache = {
260 in_git = false, -- whether current path is covered by Git
261 has_repos = false, -- whether current path is covered by Git
262 status = nil, -- status derived from nested files
263 subs = { }, -- subdirectory name -> cache node
264 items = { }, -- file name -> status
265 expires = 0 -- when the cache entry expires
266 }
267
268 redraw()
269 end
270
271 M.reset()
272
260 273 return M return M
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/vifm

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/vifm

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master