From 2f6880c0a9df63582d8b0d34e743ef49a9d16342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 14:03:48 +0200 Subject: [PATCH 01/20] add luarocks --- .gitignore | 4 ++++ gh-co.nvim-0.0.4-1.rockspec | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 .gitignore create mode 100644 gh-co.nvim-0.0.4-1.rockspec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0771553 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/luarocks +/lua +/lua_modules +/.luarocks diff --git a/gh-co.nvim-0.0.4-1.rockspec b/gh-co.nvim-0.0.4-1.rockspec new file mode 100644 index 0000000..7adba28 --- /dev/null +++ b/gh-co.nvim-0.0.4-1.rockspec @@ -0,0 +1,25 @@ +rockspec_format = "3.0" +package = "gh-co.nvim" +version = "0.0.4-1" +source = { + url = "git+ssh://git@github.com/comatory/gh-co.nvim.git" +} +description = { + summary = "Github CODEOWNERS Neovim plugin", + detailed = "Displays the code owners for current buffer, all opened buffers or lists owners by providing git SHAs", + homepage = "https://github.com/comatory/gh-co.nvim", + license = "CC0 1.0 Universal" +} +dependencies = { + "lua ~> 5.1" +} +build = { + type = "builtin", + modules = { + ["gh-co.co"] = "lua/gh-co/co.lua", + ["gh-co.fs"] = "lua/gh-co/fs.lua", + ["gh-co.git"] = "lua/gh-co/git.lua", + ["gh-co.init"] = "lua/gh-co/init.lua", + ["gh-co.syntax"] = "lua/gh-co/syntax.lua" + } +} From 1fffeeb3a293b28efccda3de8c828f715de04033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 14:33:31 +0200 Subject: [PATCH 02/20] add linter --- .luacheckrc | 7 +++++++ lua/gh-co/fs.lua | 4 +++- readme.MD | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .luacheckrc diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..a0f46cf --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,7 @@ +std = "luajit" +globals = {"vim"} + +ignore = { + -- unused loop variable + "213" +} diff --git a/lua/gh-co/fs.lua b/lua/gh-co/fs.lua index 17ed8be..6676f6c 100644 --- a/lua/gh-co/fs.lua +++ b/lua/gh-co/fs.lua @@ -60,7 +60,9 @@ FS.getCodeownersFilePath = function() local rootDirContents = vim.fs.dir(rootDirName) - assert(rootDirContents, "Not able to detect project root directory. Maybe you should run nvim from the root of the project.") + assert(rootDirContents, [[ + Not able to detect project root directory. Maybe you should run nvim from the root of the project. + ]]) local githubDirName = nil local docsDirName = nil diff --git a/readme.MD b/readme.MD index f6feb07..0d35669 100644 --- a/readme.MD +++ b/readme.MD @@ -57,3 +57,19 @@ Show codeowners for files in all buffers. ### `:GhCoGitWho ` Show codeowners for files affected by commit SHA. + +## Development + +Project is using luarocks to manage dependencies. After cloning the repo run: + +```bash +luarocks install --deps-only gh-co.nvim-0.0.4-1.rockspec +``` + +### Lint + +Run `PATH="./lua_modules/bin:PATH" luacheck lua` to lint the codebase + +### Test + +TBD From 162484bb1271c16b0eb9f53d2fbd9d5c6767bb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 14:38:59 +0200 Subject: [PATCH 03/20] add workflow --- .github/workflows/pr.yml | 26 ++++++++++++++++++++++++++ gh-co.nvim-0.0.4-1.rockspec | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pr.yml diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..d3b46a7 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,26 @@ +name: PR + +on: + pull_request: + branches: [ master ] + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Lua + uses: leafo/gh-actions-lua@v10 + with: + luaVersion: "5.4" + + - name: Setup LuaRocks + uses: leafo/gh-actions-luarocks@v4 + + - name: Install dependencies + run: luarocks install --deps-only gh-co.nvim-0.0.4-1.rockspec + + - name: Run linter + run: PATH="./lua_modules/bin:$PATH" luacheck lua \ No newline at end of file diff --git a/gh-co.nvim-0.0.4-1.rockspec b/gh-co.nvim-0.0.4-1.rockspec index 7adba28..6a9855e 100644 --- a/gh-co.nvim-0.0.4-1.rockspec +++ b/gh-co.nvim-0.0.4-1.rockspec @@ -11,7 +11,7 @@ description = { license = "CC0 1.0 Universal" } dependencies = { - "lua ~> 5.1" + "lua >= 5.1" } build = { type = "builtin", From 7429723e5d1bc645feff9c76d5b918ae6075e5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 14:49:20 +0200 Subject: [PATCH 04/20] fixup! add linter --- gh-co.nvim-0.0.4-1.rockspec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gh-co.nvim-0.0.4-1.rockspec b/gh-co.nvim-0.0.4-1.rockspec index 6a9855e..2183310 100644 --- a/gh-co.nvim-0.0.4-1.rockspec +++ b/gh-co.nvim-0.0.4-1.rockspec @@ -11,7 +11,8 @@ description = { license = "CC0 1.0 Universal" } dependencies = { - "lua >= 5.1" + "lua >= 5.1", + "luacheck" } build = { type = "builtin", From 0be34b42422b54a0e6ead18201c3e6bb1e9c262c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 15:01:37 +0200 Subject: [PATCH 05/20] setup tests --- .github/workflows/pr.yml | 9 ++++++--- .gitignore | 1 - .luacheckrc | 4 +++- gh-co.nvim-0.0.4-1.rockspec | 9 ++++++++- lua/gh-co/co.test.lua | 7 +++++++ lua/setup.lua | 7 +++++++ 6 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 lua/gh-co/co.test.lua create mode 100644 lua/setup.lua diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d3b46a7..5586997 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -5,7 +5,7 @@ on: branches: [ master ] jobs: - lint: + checks: runs-on: ubuntu-latest steps: @@ -22,5 +22,8 @@ jobs: - name: Install dependencies run: luarocks install --deps-only gh-co.nvim-0.0.4-1.rockspec - - name: Run linter - run: PATH="./lua_modules/bin:$PATH" luacheck lua \ No newline at end of file + - name: Lint + run: PATH="./lua_modules/bin:$PATH" luacheck lua + + - name: Test + run: luarocks test diff --git a/.gitignore b/.gitignore index 0771553..d14386a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ /luarocks -/lua /lua_modules /.luarocks diff --git a/.luacheckrc b/.luacheckrc index a0f46cf..0259f39 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -3,5 +3,7 @@ globals = {"vim"} ignore = { -- unused loop variable - "213" + "213", + -- ignore unused test functions + "[Tt]est[%w_]+", } diff --git a/gh-co.nvim-0.0.4-1.rockspec b/gh-co.nvim-0.0.4-1.rockspec index 2183310..f0aaec9 100644 --- a/gh-co.nvim-0.0.4-1.rockspec +++ b/gh-co.nvim-0.0.4-1.rockspec @@ -12,7 +12,10 @@ description = { } dependencies = { "lua >= 5.1", - "luacheck" + "luacheck", +} +test_dependencies = { + "luaunit >= 3.4" } build = { type = "builtin", @@ -24,3 +27,7 @@ build = { ["gh-co.syntax"] = "lua/gh-co/syntax.lua" } } +test = { + type = "command", + command = "lua -l lua/setup lua/gh-co/co.test.lua -o TAP" +} diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua new file mode 100644 index 0000000..cbab7b9 --- /dev/null +++ b/lua/gh-co/co.test.lua @@ -0,0 +1,7 @@ +local lu = require("luaunit") + +local function TestCodeowners() + lu.assertEquals(1, 1) +end + +os.exit(lu.LuaUnit.run()) diff --git a/lua/setup.lua b/lua/setup.lua new file mode 100644 index 0000000..0dbfcdb --- /dev/null +++ b/lua/setup.lua @@ -0,0 +1,7 @@ +local version = _VERSION:match("%d+%.%d+") + +package.path = 'lua_modules/share/lua/' .. version .. + '/?.lua;lua_modules/share/lua/' .. version .. + '/?/init.lua;' .. package.path +package.cpath = 'lua_modules/lib/lua/' .. version .. + '/?.so;' .. package.cpath From 795bde683dce9ea90d7e8813258445f2bd6745c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 15:28:58 +0200 Subject: [PATCH 06/20] add codeowners test --- lua/gh-co/co.test.lua | 76 +++++++++++++++++++++++++++++++++++++++++-- readme.MD | 2 +- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua index cbab7b9..2218790 100644 --- a/lua/gh-co/co.test.lua +++ b/lua/gh-co/co.test.lua @@ -1,7 +1,79 @@ +-- Add local lua path for testing +package.path = "./lua/?.lua;./lua/?/init.lua;" .. package.path + local lu = require("luaunit") -local function TestCodeowners() - lu.assertEquals(1, 1) +-- Create minimal vim global for testing +vim = { + split = function(s, sep) + local result = {} + local pattern = "([^" .. sep .. "]*)" + for match in string.gmatch(s .. sep, pattern .. sep) do + table.insert(result, match) + end + return result + end, + fs = { + dirname = function() return "/test/.github" end, + find = function() return {"/test/.github"} end, + dir = function(path) + -- Mock directory iterator - return .github directory for root, CODEOWNERS for .github + if path:match("%.github$") then + local called = false + return function() + if not called then + called = true + return "CODEOWNERS", "file" + end + return nil + end + else + -- Root directory - return .github directory + local called = false + return function() + if not called then + called = true + return ".github", "directory" + end + return nil + end + end + end + }, + loop = { + cwd = function() return "/test" end + }, + api = { + nvim_buf_get_name = function() return "" end + }, + fn = { + bufnr = function() return 0 end, + buflisted = function() return 0 end + } +} + +local CO = require("gh-co.co") + +TestCO = {} + +function TestCO:setUp() + -- Mock the FS.openCodeownersFileAsLines to return empty iterator for each test + self.FS = require("gh-co.fs") + self.originalOpenCodeownersFileAsLines = self.FS.openCodeownersFileAsLines + self.FS.openCodeownersFileAsLines = function() + return function() return nil end + end +end + +function TestCO:tearDown() + -- Restore original function + self.FS.openCodeownersFileAsLines = self.originalOpenCodeownersFileAsLines +end + +function TestCO:testMatchFilesToCodeownerEmpty() -- luacheck: ignore 212 + -- Test empty file paths returns empty list + local result = CO.matchFilesToCodeowner({}) + lu.assertEquals(result, {}) end os.exit(lu.LuaUnit.run()) diff --git a/readme.MD b/readme.MD index 0d35669..86fc182 100644 --- a/readme.MD +++ b/readme.MD @@ -72,4 +72,4 @@ Run `PATH="./lua_modules/bin:PATH" luacheck lua` to lint the codebase ### Test -TBD +Run `luarocks test` From c0cf8ec006ce0e1521651e716558b87e45bcb90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 15:51:33 +0200 Subject: [PATCH 07/20] test global patterns --- lua/gh-co/co.test.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua index 2218790..d6d312b 100644 --- a/lua/gh-co/co.test.lua +++ b/lua/gh-co/co.test.lua @@ -76,4 +76,19 @@ function TestCO:testMatchFilesToCodeownerEmpty() -- luacheck: ignore 212 lu.assertEquals(result, {}) end +function TestCO:testGlobalPattern() -- luacheck: ignore 212 + -- Test * pattern matches all files and assigns global owners + self.FS.openCodeownersFileAsLines = function() + local lines = {"* @global-owner1 @global-owner2"} + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + local result = CO.matchFilesToCodeowner({"README.md", "src/main.js"}) + lu.assertEquals(result, {"@global-owner1", "@global-owner2"}) +end + os.exit(lu.LuaUnit.run()) From f47f4d144443243926e07ab1eb1a6cb0a29fc5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 15:52:19 +0200 Subject: [PATCH 08/20] test extensions --- lua/gh-co/co.test.lua | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua index d6d312b..2ea43cc 100644 --- a/lua/gh-co/co.test.lua +++ b/lua/gh-co/co.test.lua @@ -91,4 +91,49 @@ function TestCO:testGlobalPattern() -- luacheck: ignore 212 lu.assertEquals(result, {"@global-owner1", "@global-owner2"}) end +function TestCO:testJavaScriptPattern() -- luacheck: ignore 212 + -- Test *.js pattern matches JavaScript files + self.FS.openCodeownersFileAsLines = function() + local lines = {"*.js @js-owner"} + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + local result = CO.matchFilesToCodeowner({"app.js", "utils.js"}) + lu.assertEquals(result, {"@js-owner"}) +end + +function TestCO:testGoPattern() -- luacheck: ignore 212 + -- Test *.go pattern matches Go files + self.FS.openCodeownersFileAsLines = function() + local lines = {"*.go docs@example.com"} + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + local result = CO.matchFilesToCodeowner({"main.go", "server.go"}) + lu.assertEquals(result, {"docs@example.com"}) +end + +function TestCO:testTxtPattern() -- luacheck: ignore 212 + -- Test *.txt pattern matches text files with team owner + self.FS.openCodeownersFileAsLines = function() + local lines = {"*.txt @octo-org/octocats"} + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + local result = CO.matchFilesToCodeowner({"README.txt", "notes.txt"}) + lu.assertEquals(result, {"@octo-org/octocats"}) +end + os.exit(lu.LuaUnit.run()) From 04882c2aa1c243195bd78f568f64dfce13cafb58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 15:54:55 +0200 Subject: [PATCH 09/20] test nested paths --- lua/gh-co/co.test.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua index 2ea43cc..bee4621 100644 --- a/lua/gh-co/co.test.lua +++ b/lua/gh-co/co.test.lua @@ -136,4 +136,19 @@ function TestCO:testTxtPattern() -- luacheck: ignore 212 lu.assertEquals(result, {"@octo-org/octocats"}) end +function TestCO:testBuildLogsDirectoryPattern() -- luacheck: ignore 212 + -- Test /build/logs/ pattern matches files in specific directory + self.FS.openCodeownersFileAsLines = function() + local lines = {"/build/logs/ @doctocat"} + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + local result = CO.matchFilesToCodeowner({"/build/logs/app.log", "/build/logs/error.log"}) + lu.assertEquals(result, {"@doctocat"}) +end + os.exit(lu.LuaUnit.run()) From 9512daf8a1aaf3e9c3454b88d124133caa4370c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 16:03:25 +0200 Subject: [PATCH 10/20] test & fix glob patterns and specifity --- lua/gh-co/co.lua | 23 ++++++++++++----- lua/gh-co/co.test.lua | 60 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/lua/gh-co/co.lua b/lua/gh-co/co.lua index 52ddb84..fa811a5 100644 --- a/lua/gh-co/co.lua +++ b/lua/gh-co/co.lua @@ -7,7 +7,15 @@ local function isComment(pathPattern) end local function buildEscapedPattern(rawPattern) - return string.gsub(rawPattern, "%-", "%%-") + -- Escape Lua pattern special characters except * + local escaped = string.gsub(rawPattern, "([%-%+%?%(%)])", "%%%1") + -- Convert * to match any character except / + escaped = string.gsub(escaped, "%*", "[^/]*") + -- Anchor pattern to match from start if it doesn't begin with / + if not string.match(escaped, "^/") then + escaped = escaped .. "$" + end + return escaped end -- matches file path substrings @@ -15,15 +23,16 @@ local function isMatch(filePath, pathPattern) if pathPattern == nil or pathPattern == "" then return false end if isComment(pathPattern) then return false end - return string.match(filePath, buildEscapedPattern(pathPattern)) ~= nil + local pattern = buildEscapedPattern(pathPattern) + return string.match(filePath, pattern) ~= nil end --- Detects `*` pattern +-- Detects `*` pattern (global match - only exact "*") local function isGlobalMatch(pathPattern) if pathPattern == nil or pathPattern == "" then return false end if isComment(pathPattern) then return false end - return string.match(pathPattern, "*") ~= nil + return pathPattern == "*" end local function collectCodeowners(group) @@ -74,10 +83,10 @@ CO.matchFilesToCodeowner = function(filePaths) local pathPattern = split[1] for _, filePath in ipairs(filePaths) do - if isMatch(filePath, pathPattern) then - table.insert(matches, { pathPattern = pathPattern, codeowners = collectCodeowners(split) }) - elseif isGlobalMatch(pathPattern) then + if isGlobalMatch(pathPattern) then globalCodeowners = collectCodeowners(split) + elseif isMatch(filePath, pathPattern) then + table.insert(matches, { pathPattern = pathPattern, codeowners = collectCodeowners(split) }) end end end diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua index bee4621..9fe7259 100644 --- a/lua/gh-co/co.test.lua +++ b/lua/gh-co/co.test.lua @@ -151,4 +151,64 @@ function TestCO:testBuildLogsDirectoryPattern() -- luacheck: ignore 212 lu.assertEquals(result, {"@doctocat"}) end +function TestCO:testDocsWildcardPattern() -- luacheck: ignore 212 + -- Test docs/* pattern matches files directly in docs directory (not subdirectories) + self.FS.openCodeownersFileAsLines = function() + local lines = {"docs/* docs@example.com"} + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + local result = CO.matchFilesToCodeowner({"docs/README.md", "docs/guide.txt"}) + lu.assertEquals(result, {"docs@example.com"}) +end + +function TestCO:testDocsWildcardDoesNotMatchSubdirectories() -- luacheck: ignore 212 + -- Test docs/* pattern does NOT match files in subdirectories + self.FS.openCodeownersFileAsLines = function() + local lines = {"docs/* docs@example.com"} + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + -- Debug: Test what the pattern should do + -- docs/* should match "docs/readme.md" but NOT "docs/sub/readme.md" + local result = CO.matchFilesToCodeowner({"docs/sub/readme.md"}) + lu.assertEquals(result, {}) +end + +function TestCO:testCombinedWithGlobalPattern() -- luacheck: ignore 212 + -- Test that when both specific and global patterns exist, specific patterns take precedence + self.FS.openCodeownersFileAsLines = function() + local lines = { + "* @global-owner", + "*.js @js-owner", + "docs/* docs@example.com" + } + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + -- JS files should match specific owner (*.js overrides *) + local result1 = CO.matchFilesToCodeowner({"app.js"}) + lu.assertEquals(result1, {"@js-owner"}) + + -- Docs files should match docs owner (docs/* overrides *) + local result2 = CO.matchFilesToCodeowner({"docs/README.md"}) + lu.assertEquals(result2, {"docs@example.com"}) + + -- Files with no specific pattern should fall back to global owner + local result3 = CO.matchFilesToCodeowner({"README.py"}) + lu.assertEquals(result3, {"@global-owner"}) +end + os.exit(lu.LuaUnit.run()) From 4d00a853dca6db1d77aeddf7181bf1712ec8dae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 16:04:43 +0200 Subject: [PATCH 11/20] test & fix paths with trailing slash --- lua/gh-co/co.lua | 11 +++++++++-- lua/gh-co/co.test.lua | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lua/gh-co/co.lua b/lua/gh-co/co.lua index fa811a5..8f6cee4 100644 --- a/lua/gh-co/co.lua +++ b/lua/gh-co/co.lua @@ -11,10 +11,17 @@ local function buildEscapedPattern(rawPattern) local escaped = string.gsub(rawPattern, "([%-%+%?%(%)])", "%%%1") -- Convert * to match any character except / escaped = string.gsub(escaped, "%*", "[^/]*") - -- Anchor pattern to match from start if it doesn't begin with / - if not string.match(escaped, "^/") then + + -- Handle trailing slash - directory patterns should match everything within + if string.match(escaped, "/$") then + -- Remove trailing slash and match anything that starts with this path + escaped = string.gsub(escaped, "/$", "/") + -- Don't anchor with $ - allow matching subdirectories + elseif not string.match(escaped, "^/") then + -- Anchor non-directory patterns to match exactly escaped = escaped .. "$" end + return escaped end diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua index 9fe7259..96cd76f 100644 --- a/lua/gh-co/co.test.lua +++ b/lua/gh-co/co.test.lua @@ -211,4 +211,19 @@ function TestCO:testCombinedWithGlobalPattern() -- luacheck: ignore 212 lu.assertEquals(result3, {"@global-owner"}) end +function TestCO:testAppsDirectoryPattern() -- luacheck: ignore 212 + -- Test apps/ pattern matches files in apps directory + self.FS.openCodeownersFileAsLines = function() + local lines = {"apps/ @octocat"} + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + local result = CO.matchFilesToCodeowner({"apps/web/index.js", "apps/mobile/main.kt"}) + lu.assertEquals(result, {"@octocat"}) +end + os.exit(lu.LuaUnit.run()) From 9762cda3af028e994791e42dd54f58ec0115c5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 16:05:46 +0200 Subject: [PATCH 12/20] test root + sub dirs --- lua/gh-co/co.test.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua index 96cd76f..a55ec5f 100644 --- a/lua/gh-co/co.test.lua +++ b/lua/gh-co/co.test.lua @@ -226,4 +226,19 @@ function TestCO:testAppsDirectoryPattern() -- luacheck: ignore 212 lu.assertEquals(result, {"@octocat"}) end +function TestCO:testRootDocsDirectoryPattern() -- luacheck: ignore 212 + -- Test /docs/ pattern matches files in root docs directory + self.FS.openCodeownersFileAsLines = function() + local lines = {"/docs/ @doctocat"} + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + local result = CO.matchFilesToCodeowner({"/docs/api.md", "/docs/guides/setup.md"}) + lu.assertEquals(result, {"@doctocat"}) +end + os.exit(lu.LuaUnit.run()) From 1d4285baec9d5c40c8b9cdc4b487b390bccec229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 16:06:17 +0200 Subject: [PATCH 13/20] test multiple owners --- lua/gh-co/co.test.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua index a55ec5f..cf0b842 100644 --- a/lua/gh-co/co.test.lua +++ b/lua/gh-co/co.test.lua @@ -241,4 +241,19 @@ function TestCO:testRootDocsDirectoryPattern() -- luacheck: ignore 212 lu.assertEquals(result, {"@doctocat"}) end +function TestCO:testRootScriptsDirectoryPattern() -- luacheck: ignore 212 + -- Test /scripts/ pattern with multiple owners + self.FS.openCodeownersFileAsLines = function() + local lines = {"/scripts/ @doctocat @octocat"} + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + local result = CO.matchFilesToCodeowner({"/scripts/deploy.sh", "/scripts/test.py"}) + lu.assertEquals(result, {"@doctocat", "@octocat"}) +end + os.exit(lu.LuaUnit.run()) From a68b784de5b107c880007f02bb749084d5cd6f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 16:09:23 +0200 Subject: [PATCH 14/20] test & fix `**` pattern with directories --- lua/gh-co/co.lua | 15 ++++++++++++++- lua/gh-co/co.test.lua | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lua/gh-co/co.lua b/lua/gh-co/co.lua index 8f6cee4..5f8f5b9 100644 --- a/lua/gh-co/co.lua +++ b/lua/gh-co/co.lua @@ -9,9 +9,22 @@ end local function buildEscapedPattern(rawPattern) -- Escape Lua pattern special characters except * local escaped = string.gsub(rawPattern, "([%-%+%?%(%)])", "%%%1") - -- Convert * to match any character except / + + -- Handle ** first (before single *) - use placeholder to avoid conflicts + escaped = string.gsub(escaped, "%*%*", "__DOUBLESTAR__") + + -- Convert remaining * to match any character except / escaped = string.gsub(escaped, "%*", "[^/]*") + -- Replace placeholder with pattern that matches any path including / + escaped = string.gsub(escaped, "__DOUBLESTAR__", ".*") + + -- Special handling for **/name patterns - they should match directories + if string.match(rawPattern, "%*%*/[^/]+$") then + -- **/logs should match files within logs directories + escaped = escaped .. "/" + end + -- Handle trailing slash - directory patterns should match everything within if string.match(escaped, "/$") then -- Remove trailing slash and match anything that starts with this path diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua index cf0b842..8fbbc24 100644 --- a/lua/gh-co/co.test.lua +++ b/lua/gh-co/co.test.lua @@ -256,4 +256,19 @@ function TestCO:testRootScriptsDirectoryPattern() -- luacheck: ignore 212 lu.assertEquals(result, {"@doctocat", "@octocat"}) end +function TestCO:testDoubleStarLogsPattern() -- luacheck: ignore 212 + -- Test **/logs pattern matches any logs directory at any depth + self.FS.openCodeownersFileAsLines = function() + local lines = {"**/logs @octocat"} + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + local result = CO.matchFilesToCodeowner({"build/logs/error.log", "app/server/logs/access.log", "logs/debug.log"}) + lu.assertEquals(result, {"@octocat"}) +end + os.exit(lu.LuaUnit.run()) From bf8c52b9f7785d14dd0bbf76b683dab4ccdf595a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 16:13:25 +0200 Subject: [PATCH 15/20] test & fix left-out codeowners path --- lua/gh-co/co.lua | 14 ++++++++++++-- lua/gh-co/co.test.lua | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lua/gh-co/co.lua b/lua/gh-co/co.lua index 5f8f5b9..7c4981e 100644 --- a/lua/gh-co/co.lua +++ b/lua/gh-co/co.lua @@ -116,8 +116,18 @@ CO.matchFilesToCodeowner = function(filePaths) sortMatches(matches) - local codeownersList = mapCodeowners(matches) - + -- Only use the most specific pattern(s) - those with the longest pathPattern + local maxLength = #matches[1].pathPattern + local mostSpecificMatches = {} + for _, match in ipairs(matches) do + if #match.pathPattern == maxLength then + table.insert(mostSpecificMatches, match) + else + break -- Since sorted by length, we can break early + end + end + + local codeownersList = mapCodeowners(mostSpecificMatches) return codeownersList end diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua index 8fbbc24..dc48a00 100644 --- a/lua/gh-co/co.test.lua +++ b/lua/gh-co/co.test.lua @@ -271,4 +271,23 @@ function TestCO:testDoubleStarLogsPattern() -- luacheck: ignore 212 lu.assertEquals(result, {"@octocat"}) end +function TestCO:testAppsWithEmptyGithubSubdirectory() -- luacheck: ignore 212 + -- Test /apps/ with empty /apps/github (no owners) + self.FS.openCodeownersFileAsLines = function() + local lines = { + "/apps/ @octocat", + "/apps/github" + } + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + -- Files in /apps/github should have no owners (empty pattern overrides /apps/) + local githubResult = CO.matchFilesToCodeowner({"/apps/github/readme.md"}) + lu.assertEquals(githubResult, {}) +end + os.exit(lu.LuaUnit.run()) From f257168656a172246d898fcdb10d942d7f17d397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 16:14:45 +0200 Subject: [PATCH 16/20] add owner overrides --- lua/gh-co/co.test.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua index dc48a00..4632425 100644 --- a/lua/gh-co/co.test.lua +++ b/lua/gh-co/co.test.lua @@ -290,4 +290,23 @@ function TestCO:testAppsWithEmptyGithubSubdirectory() -- luacheck: ignore 212 lu.assertEquals(githubResult, {}) end +function TestCO:testAppsWithGithubSubdirectoryOwner() -- luacheck: ignore 212 + -- Test /apps/ with /apps/github having different owner + self.FS.openCodeownersFileAsLines = function() + local lines = { + "/apps/ @octocat", + "/apps/github @doctocat" + } + local i = 0 + return function() + i = i + 1 + return lines[i] + end + end + + -- Files in /apps/github should have @doctocat (overrides /apps/) + local githubResult = CO.matchFilesToCodeowner({"/apps/github/readme.md"}) + lu.assertEquals(githubResult, {"@doctocat"}) +end + os.exit(lu.LuaUnit.run()) From 789eb98e54613f5b8c1d06b07015cd01ae8fcd55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 16:18:05 +0200 Subject: [PATCH 17/20] fix lint issues --- lua/gh-co/co.lua | 14 +++++++------- lua/gh-co/co.test.lua | 32 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lua/gh-co/co.lua b/lua/gh-co/co.lua index 7c4981e..8e87de9 100644 --- a/lua/gh-co/co.lua +++ b/lua/gh-co/co.lua @@ -9,22 +9,22 @@ end local function buildEscapedPattern(rawPattern) -- Escape Lua pattern special characters except * local escaped = string.gsub(rawPattern, "([%-%+%?%(%)])", "%%%1") - + -- Handle ** first (before single *) - use placeholder to avoid conflicts escaped = string.gsub(escaped, "%*%*", "__DOUBLESTAR__") - + -- Convert remaining * to match any character except / escaped = string.gsub(escaped, "%*", "[^/]*") - + -- Replace placeholder with pattern that matches any path including / escaped = string.gsub(escaped, "__DOUBLESTAR__", ".*") - + -- Special handling for **/name patterns - they should match directories if string.match(rawPattern, "%*%*/[^/]+$") then -- **/logs should match files within logs directories escaped = escaped .. "/" end - + -- Handle trailing slash - directory patterns should match everything within if string.match(escaped, "/$") then -- Remove trailing slash and match anything that starts with this path @@ -34,7 +34,7 @@ local function buildEscapedPattern(rawPattern) -- Anchor non-directory patterns to match exactly escaped = escaped .. "$" end - + return escaped end @@ -126,7 +126,7 @@ CO.matchFilesToCodeowner = function(filePaths) break -- Since sorted by length, we can break early end end - + local codeownersList = mapCodeowners(mostSpecificMatches) return codeownersList end diff --git a/lua/gh-co/co.test.lua b/lua/gh-co/co.test.lua index 4632425..a69a8a3 100644 --- a/lua/gh-co/co.test.lua +++ b/lua/gh-co/co.test.lua @@ -86,7 +86,7 @@ function TestCO:testGlobalPattern() -- luacheck: ignore 212 return lines[i] end end - + local result = CO.matchFilesToCodeowner({"README.md", "src/main.js"}) lu.assertEquals(result, {"@global-owner1", "@global-owner2"}) end @@ -101,7 +101,7 @@ function TestCO:testJavaScriptPattern() -- luacheck: ignore 212 return lines[i] end end - + local result = CO.matchFilesToCodeowner({"app.js", "utils.js"}) lu.assertEquals(result, {"@js-owner"}) end @@ -116,7 +116,7 @@ function TestCO:testGoPattern() -- luacheck: ignore 212 return lines[i] end end - + local result = CO.matchFilesToCodeowner({"main.go", "server.go"}) lu.assertEquals(result, {"docs@example.com"}) end @@ -131,7 +131,7 @@ function TestCO:testTxtPattern() -- luacheck: ignore 212 return lines[i] end end - + local result = CO.matchFilesToCodeowner({"README.txt", "notes.txt"}) lu.assertEquals(result, {"@octo-org/octocats"}) end @@ -146,7 +146,7 @@ function TestCO:testBuildLogsDirectoryPattern() -- luacheck: ignore 212 return lines[i] end end - + local result = CO.matchFilesToCodeowner({"/build/logs/app.log", "/build/logs/error.log"}) lu.assertEquals(result, {"@doctocat"}) end @@ -161,7 +161,7 @@ function TestCO:testDocsWildcardPattern() -- luacheck: ignore 212 return lines[i] end end - + local result = CO.matchFilesToCodeowner({"docs/README.md", "docs/guide.txt"}) lu.assertEquals(result, {"docs@example.com"}) end @@ -176,7 +176,7 @@ function TestCO:testDocsWildcardDoesNotMatchSubdirectories() -- luacheck: ignore return lines[i] end end - + -- Debug: Test what the pattern should do -- docs/* should match "docs/readme.md" but NOT "docs/sub/readme.md" local result = CO.matchFilesToCodeowner({"docs/sub/readme.md"}) @@ -197,15 +197,15 @@ function TestCO:testCombinedWithGlobalPattern() -- luacheck: ignore 212 return lines[i] end end - + -- JS files should match specific owner (*.js overrides *) local result1 = CO.matchFilesToCodeowner({"app.js"}) lu.assertEquals(result1, {"@js-owner"}) - + -- Docs files should match docs owner (docs/* overrides *) local result2 = CO.matchFilesToCodeowner({"docs/README.md"}) lu.assertEquals(result2, {"docs@example.com"}) - + -- Files with no specific pattern should fall back to global owner local result3 = CO.matchFilesToCodeowner({"README.py"}) lu.assertEquals(result3, {"@global-owner"}) @@ -221,7 +221,7 @@ function TestCO:testAppsDirectoryPattern() -- luacheck: ignore 212 return lines[i] end end - + local result = CO.matchFilesToCodeowner({"apps/web/index.js", "apps/mobile/main.kt"}) lu.assertEquals(result, {"@octocat"}) end @@ -236,7 +236,7 @@ function TestCO:testRootDocsDirectoryPattern() -- luacheck: ignore 212 return lines[i] end end - + local result = CO.matchFilesToCodeowner({"/docs/api.md", "/docs/guides/setup.md"}) lu.assertEquals(result, {"@doctocat"}) end @@ -251,7 +251,7 @@ function TestCO:testRootScriptsDirectoryPattern() -- luacheck: ignore 212 return lines[i] end end - + local result = CO.matchFilesToCodeowner({"/scripts/deploy.sh", "/scripts/test.py"}) lu.assertEquals(result, {"@doctocat", "@octocat"}) end @@ -266,7 +266,7 @@ function TestCO:testDoubleStarLogsPattern() -- luacheck: ignore 212 return lines[i] end end - + local result = CO.matchFilesToCodeowner({"build/logs/error.log", "app/server/logs/access.log", "logs/debug.log"}) lu.assertEquals(result, {"@octocat"}) end @@ -284,7 +284,7 @@ function TestCO:testAppsWithEmptyGithubSubdirectory() -- luacheck: ignore 212 return lines[i] end end - + -- Files in /apps/github should have no owners (empty pattern overrides /apps/) local githubResult = CO.matchFilesToCodeowner({"/apps/github/readme.md"}) lu.assertEquals(githubResult, {}) @@ -303,7 +303,7 @@ function TestCO:testAppsWithGithubSubdirectoryOwner() -- luacheck: ignore 212 return lines[i] end end - + -- Files in /apps/github should have @doctocat (overrides /apps/) local githubResult = CO.matchFilesToCodeowner({"/apps/github/readme.md"}) lu.assertEquals(githubResult, {"@doctocat"}) From 7b4ed254e72349da690044c43de94fc2d7d9d1cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 20:28:46 +0200 Subject: [PATCH 18/20] fix highlighting --- lua/gh-co/syntax.lua | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lua/gh-co/syntax.lua b/lua/gh-co/syntax.lua index c48937c..41be4fc 100644 --- a/lua/gh-co/syntax.lua +++ b/lua/gh-co/syntax.lua @@ -56,23 +56,18 @@ local function highlight_line(bufnr, line_num, line_content) for i = 2, #parts do local owner = parts[i] - local owner_start = remaining_line:find(vim.pesc(owner), 1, true) + local owner_start = remaining_line:find(owner, 1, true) if owner_start then local actual_start = offset + owner_start - 1 local actual_end = actual_start + #owner -- Determine highlight group based on owner format - if owner:match("^@[%w_%-]+/[%w_%-]+$") or owner:match("^@[%w_%-]+$") then + if owner:find("@") then vim.api.nvim_buf_set_extmark(bufnr, ns_id, line_num, actual_start, { end_col = actual_end, hl_group = "CodeownersOwner" }) - elseif owner:match("^[%w%.%%_%+%-]+@[%w%.%-]+%.[%w]+$") then - vim.api.nvim_buf_set_extmark(bufnr, ns_id, line_num, actual_start, { - end_col = actual_end, - hl_group = "CodeownersEmail" - }) end -- Update offset for next owner From 39d6cc79f918ddf910e274eb282542ee7b28f6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 20:50:51 +0200 Subject: [PATCH 19/20] add treesitter support --- lua/gh-co/syntax.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lua/gh-co/syntax.lua b/lua/gh-co/syntax.lua index 41be4fc..8fd0296 100644 --- a/lua/gh-co/syntax.lua +++ b/lua/gh-co/syntax.lua @@ -5,8 +5,14 @@ local function setup_highlight_groups() vim.api.nvim_set_hl(0, "CodeownersComment", { link = "Comment" }) vim.api.nvim_set_hl(0, "CodeownersPath", { link = "Identifier" }) vim.api.nvim_set_hl(0, "CodeownersGlobalPath", { link = "Special" }) - vim.api.nvim_set_hl(0, "CodeownersOwner", { link = "String" }) - vim.api.nvim_set_hl(0, "CodeownersEmail", { link = "Constant" }) + + -- Try to use treesitter highlight groups for better theming + local has_treesitter = pcall(require, 'nvim-treesitter') + if has_treesitter and vim.fn.hlexists("@string.special") == 1 then + vim.api.nvim_set_hl(0, "CodeownersOwner", { link = "@string.special" }) + else + vim.api.nvim_set_hl(0, "CodeownersOwner", { link = "String" }) + end end local function highlight_line(bufnr, line_num, line_content) From f8cc75a8c8f1d134901f95125fbaa2f127907784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Fri, 8 Aug 2025 20:56:54 +0200 Subject: [PATCH 20/20] v0.0.5 --- .github/workflows/pr.yml | 2 +- CHANGELOG.md | 7 +++++++ gh-co.nvim-0.0.5-1.rockspec | 33 +++++++++++++++++++++++++++++++++ readme.MD | 2 +- 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 gh-co.nvim-0.0.5-1.rockspec diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5586997..e251fe9 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -20,7 +20,7 @@ jobs: uses: leafo/gh-actions-luarocks@v4 - name: Install dependencies - run: luarocks install --deps-only gh-co.nvim-0.0.4-1.rockspec + run: luarocks install --deps-only gh-co.nvim-0.0.5-1.rockspec - name: Lint run: PATH="./lua_modules/bin:$PATH" luacheck lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d36247..261603d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ +## v0.0.5 + +- add luarocks & unit tests +- fix incompatibility with CODEOWNERS spec, the plugin can now work with various patterns, can handle paths marked with no owner(s) or wildcard patterns +- fix highlighting, add treesitter support + ## v0.0.4 + - syntax highlighting for `CODEOWNERS` file ## v0.0.3 diff --git a/gh-co.nvim-0.0.5-1.rockspec b/gh-co.nvim-0.0.5-1.rockspec new file mode 100644 index 0000000..0edc1cd --- /dev/null +++ b/gh-co.nvim-0.0.5-1.rockspec @@ -0,0 +1,33 @@ +rockspec_format = "3.0" +package = "gh-co.nvim" +version = "0.0.5-1" +source = { + url = "git+ssh://git@github.com/comatory/gh-co.nvim.git" +} +description = { + summary = "Github CODEOWNERS Neovim plugin", + detailed = "Displays the code owners for current buffer, all opened buffers or lists owners by providing git SHAs", + homepage = "https://github.com/comatory/gh-co.nvim", + license = "CC0 1.0 Universal" +} +dependencies = { + "lua >= 5.1", + "luacheck", +} +test_dependencies = { + "luaunit >= 3.4" +} +build = { + type = "builtin", + modules = { + ["gh-co.co"] = "lua/gh-co/co.lua", + ["gh-co.fs"] = "lua/gh-co/fs.lua", + ["gh-co.git"] = "lua/gh-co/git.lua", + ["gh-co.init"] = "lua/gh-co/init.lua", + ["gh-co.syntax"] = "lua/gh-co/syntax.lua" + } +} +test = { + type = "command", + command = "lua -l lua/setup lua/gh-co/co.test.lua -o TAP" +} diff --git a/readme.MD b/readme.MD index 86fc182..87ad948 100644 --- a/readme.MD +++ b/readme.MD @@ -63,7 +63,7 @@ Show codeowners for files affected by commit SHA. Project is using luarocks to manage dependencies. After cloning the repo run: ```bash -luarocks install --deps-only gh-co.nvim-0.0.4-1.rockspec +luarocks install --deps-only gh-co.nvim-0.0.5-1.rockspec ``` ### Lint