dots

my dotfiles
git clone https://git.awy.one/dots
Log | Files | Refs | Submodules | README | LICENSE

main.lua (7015B) - View raw


  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
--- @since 25.5.31

local WINDOWS = ya.target_family() == "windows"

-- The code of supported git status,
-- also used to determine which status to show for directories when they contain different statuses
-- see `bubble_up`
---@enum CODES
local CODES = {
  excluded = 100, -- ignored directory
  ignored = 6,   -- ignored file
  untracked = 5,
  modified = 4,
  added = 3,
  deleted = 2,
  updated = 1,
  unknown = 0,
}

local PATTERNS = {
  { "!$",       CODES.ignored },
  { "?$",       CODES.untracked },
  { "[MT]",     CODES.modified },
  { "[AC]",     CODES.added },
  { "D",        CODES.deleted },
  { "U",        CODES.updated },
  { "[AD][AD]", CODES.updated },
}

---@param line string
---@return CODES, string
local function match(line)
  local signs = line:sub(1, 2)
  for _, p in ipairs(PATTERNS) do
    local path, pattern, code = nil, p[1], p[2]
    if signs:find(pattern) then
      path = line:sub(4, 4) == '"' and line:sub(5, -2) or line:sub(4)
      path = WINDOWS and path:gsub("/", "\\") or path
    end
    if not path then
    elseif path:find("[/\\]$") then
      -- Mark the ignored directory as `excluded`, so we can process it further within `propagate_down`
      return code == CODES.ignored and CODES.excluded or code, path:sub(1, -2)
    else
      return code, path
    end
    ---@diagnostic disable-next-line: missing-return
  end
end

---@param cwd Url
---@return string?
local function root(cwd)
  local is_worktree = function(url)
    local file, head = io.open(tostring(url)), nil
    if file then
      head = file:read(8)
      file:close()
    end
    return head == "gitdir: "
  end

  repeat
    local next = cwd:join(".git")
    local cha = fs.cha(next)
    if cha and (cha.is_dir or is_worktree(next)) then
      return tostring(cwd)
    end
    cwd = cwd.parent
  until not cwd
end

---@param changed Changes
---@return Changes
local function bubble_up(changed)
  local new, empty = {}, Url("")
  for path, code in pairs(changed) do
    if code ~= CODES.ignored then
      local url = Url(path).parent
      while url and url ~= empty do
        local s = tostring(url)
        new[s] = (new[s] or CODES.unknown) > code and new[s] or code
        url = url.parent
      end
    end
  end
  return new
end

---@param excluded string[]
---@param cwd Url
---@param repo Url
---@return Changes
local function propagate_down(excluded, cwd, repo)
  local new, rel = {}, cwd:strip_prefix(repo)
  for _, path in ipairs(excluded) do
    if rel:starts_with(path) then
      -- If `cwd` is a subdirectory of an excluded directory, also mark it as `excluded`
      new[tostring(cwd)] = CODES.excluded
    elseif cwd == repo:join(path).parent then
      -- If `path` is a direct subdirectory of `cwd`, mark it as `ignored`
      new[path] = CODES.ignored
    else
      -- Skipping, we only care about `cwd` itself and its direct subdirectories for maximum performance
    end
  end
  return new
end

---@param cwd string
---@param repo string
---@param changed Changes
local add = ya.sync(function(st, cwd, repo, changed)
  ---@cast st State

  st.dirs[cwd] = repo
  st.repos[repo] = st.repos[repo] or {}
  for path, code in pairs(changed) do
    if code == CODES.unknown then
      st.repos[repo][path] = nil
    elseif code == CODES.excluded then
      -- Mark the directory with a special value `excluded` so that it can be distinguished during UI rendering
      st.dirs[path] = CODES.excluded
    else
      st.repos[repo][path] = code
    end
  end
  -- TODO: remove this
  if ui.render then
    ui.render()
  else
    ya.render()
  end
end)

---@param cwd string
local remove = ya.sync(function(st, cwd)
  ---@cast st State

  local repo = st.dirs[cwd]
  if not repo then
    return
  end

  -- TODO: remove this
  if ui.render then
    ui.render()
  else
    ya.render()
  end
  st.dirs[cwd] = nil
  if not st.repos[repo] then
    return
  end

  for _, r in pairs(st.dirs) do
    if r == repo then
      return
    end
  end
  st.repos[repo] = nil
end)

---@param st State
---@param opts Options
local function setup(st, opts)
  st.dirs = {}
  st.repos = {}

  opts = opts or {}
  opts.order = opts.order or 1500

  local t = th.git or {}
  local styles = {
    [CODES.ignored] = t.ignored and ui.Style(t.ignored) or ui.Style():fg("darkgray"),
    [CODES.untracked] = t.untracked and ui.Style(t.untracked) or ui.Style():fg("magenta"),
    [CODES.modified] = t.modified and ui.Style(t.modified) or ui.Style():fg("yellow"),
    [CODES.added] = t.added and ui.Style(t.added) or ui.Style():fg("green"),
    [CODES.deleted] = t.deleted and ui.Style(t.deleted) or ui.Style():fg("red"),
    [CODES.updated] = t.updated and ui.Style(t.updated) or ui.Style():fg("yellow"),
  }
  local signs = {
    [CODES.ignored] = t.ignored_sign or "",
    [CODES.untracked] = t.untracked_sign or "?",
    [CODES.modified] = t.modified_sign or "",
    [CODES.added] = t.added_sign or "",
    [CODES.deleted] = t.deleted_sign or "",
    [CODES.updated] = t.updated_sign or "",
  }

  Linemode:children_add(function(self)
    local url = self._file.url
    local repo = st.dirs[tostring(url.base or url.parent)]
    local code
    if repo then
      code = repo == CODES.excluded and CODES.ignored or st.repos[repo][tostring(url):sub(#repo + 2)]
    end

    if not code or signs[code] == "" then
      return ""
    elseif self._file.is_hovered then
      return ui.Line { " ", signs[code] }
    else
      return ui.Line { " ", ui.Span(signs[code]):style(styles[code]) }
    end
  end, opts.order)
end

---@type UnstableFetcher
local function fetch(_, job)
  local cwd = job.files[1].url.base or job.files[1].url.parent
  local repo = root(cwd)
  if not repo then
    remove(tostring(cwd))
    return true
  end

  local paths = {}
  for _, file in ipairs(job.files) do
    paths[#paths + 1] = tostring(file.url)
  end

  -- stylua: ignore
  local output, err = Command("git")
      :cwd(tostring(cwd))
      :arg({ "--no-optional-locks", "-c", "core.quotePath=", "status", "--porcelain", "-unormal", "--no-renames",
        "--ignored=matching" })
      :arg(paths)
      :stdout(Command.PIPED)
      :output()
  if not output then
    return true, Err("Cannot spawn `git` command, error: %s", err)
  end

  local changed, excluded = {}, {}
  for line in output.stdout:gmatch("[^\r\n]+") do
    local code, path = match(line)
    if code == CODES.excluded then
      excluded[#excluded + 1] = path
    else
      changed[path] = code
    end
  end

  if job.files[1].cha.is_dir then
    ya.dict_merge(changed, bubble_up(changed))
  end
  ya.dict_merge(changed, propagate_down(excluded, cwd, Url(repo)))

  -- Reset the status of any files that don't appear in the output of `git status` to `unknown`,
  -- so that cleaning up outdated statuses from `st.repos`
  for _, path in ipairs(paths) do
    local s = path:sub(#repo + 2)
    changed[s] = changed[s] or CODES.unknown
  end

  add(tostring(cwd), repo, changed)

  return false
end

return { setup = setup, fetch = fetch }