-- vim: foldnestmax=5 foldminlines=1 local Screen = require('test.screen') local helpers = require('test.gs_helpers') local clear = helpers.clear local command = helpers.command local exec_capture = helpers.exec_capture local feed = helpers.feed local insert = helpers.insert local exec_lua = helpers.exec_lua local split = vim.split local get_buf_var = helpers.curbufmeths.get_var local fn = helpers.funcs local system = fn.system local expectf = helpers.expectf local write_to_file = helpers.write_to_file local edit = helpers.edit local cleanup = helpers.cleanup local test_file = helpers.test_file local git = helpers.git local scratch = helpers.scratch local newfile = helpers.newfile local match_dag = helpers.match_dag local match_lines = helpers.match_lines local n, p, np = helpers.n, helpers.p, helpers.np local match_debug_messages = helpers.match_debug_messages local setup_gitsigns = helpers.setup_gitsigns local setup_test_repo = helpers.setup_test_repo local test_config = helpers.test_config local check = helpers.check local eq = helpers.eq helpers.env() describe('gitsigns', function() local screen local config before_each(function() clear() screen = Screen.new(20, 17) screen:attach({ext_messages=true}) screen:set_default_attr_ids({ [1] = {foreground = Screen.colors.DarkBlue, background = Screen.colors.WebGray}; [2] = {background = Screen.colors.LightMagenta}; [3] = {background = Screen.colors.LightBlue}; [4] = {background = Screen.colors.LightCyan1, bold = true, foreground = Screen.colors.Blue1}; [5] = {foreground = Screen.colors.Brown}; [6] = {foreground = Screen.colors.Blue1, bold = true}; [7] = {bold = true}, [8] = {foreground = Screen.colors.White, background = Screen.colors.Red}; [9] = {foreground = Screen.colors.SeaGreen, bold = true}; [10] = {foreground = Screen.colors.Red}; }) -- Make gitisigns available exec_lua('package.path = ...', package.path) config = vim.deepcopy(test_config) command('cd '..system{"dirname", os.tmpname()}) end) after_each(function() cleanup() screen:detach() end) it('can run basic setup', function() setup_gitsigns() check { status = {}, signs = {} } end) it('gitdir watcher works on a fresh repo', function() local nvim_ver = exec_lua('return vim.version().minor') if nvim_ver == 8 then pending("v0.8.0 has some regression that's fixed it v0.9.0 dev") end screen:try_resize(20,6) setup_test_repo{no_add=true} -- Don't set this too low, or else the test will lock up config.watch_gitdir = {interval = 100} setup_gitsigns(config) edit(test_file) match_dag { 'attach(1): Attaching (trigger=BufReadPost)', p'run_job: git .* config user.name', p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', p('run_job: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard %-%-eol '..vim.pesc(test_file)), 'watch_gitdir(1): Watching git dir', p'run_job: git .* show :0:dummy.txt', 'update(1): updates: 1, jobs: 6' } check { status = {head='', added=18, changed=0, removed=0}, signs = {untracked=8} } git{"add", test_file} check { status = {head='', added=0, changed=0, removed=0}, signs = {} } end) it('can open files not in a git repo', function() setup_gitsigns(config) local tmpfile = os.tmpname() edit(tmpfile) match_debug_messages { 'attach(1): Attaching (trigger=BufReadPost)', np'run_job: git .* config user.name', np'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', n'new: Not in git repo', n'attach(1): Empty git obj', } command('Gitsigns clear_debug') insert('line') command("write") match_debug_messages { n'attach(1): Attaching (trigger=BufWritePost)', np'run_job: git .* config user.name', np'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', n'new: Not in git repo', n'attach(1): Empty git obj' } end) describe('when attaching', function() before_each(function() setup_test_repo() setup_gitsigns(config) end) it('can setup mappings', function() edit(test_file) expectf(function() local res = split(exec_capture('nmap '), '\n') table.sort(res) -- Check all keymaps get set match_lines(res, { n'n mhS *@lua require"gitsigns".stage_buffer()', n'n mhU *@lua require"gitsigns".reset_buffer_index()', n'n mhp *@lua require"gitsigns".preview_hunk()', n'n mhr *@lua require"gitsigns".reset_hunk()', n'n mhs *@lua require"gitsigns".stage_hunk()', n'n mhu *@lua require"gitsigns".undo_stage_hunk()', }) end) end) it('does not attach inside .git', function() edit(scratch..'/.git/index') match_debug_messages { 'attach(1): Attaching (trigger=BufReadPost)', n'new: In git dir', n'attach(1): Empty git obj' } end) it('doesn\'t attach to ignored files', function() write_to_file(scratch..'/.gitignore', {'dummy_ignored.txt'}) local ignored_file = scratch.."/dummy_ignored.txt" system{"touch", ignored_file} edit(ignored_file) match_debug_messages { 'attach(1): Attaching (trigger=BufReadPost)', np'run_job: git .* config user.name', np'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', np'run_job: git .* ls%-files .*/dummy_ignored.txt', n'attach(1): Cannot resolve file in repo', } check {status = {head='master'}} end) it('doesn\'t attach to non-existent files', function() edit(newfile) match_debug_messages { 'attach(1): Attaching (trigger=BufNewFile)', np'run_job: git .* config user.name', np'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', np('run_job: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard %-%-eol '..vim.pesc(newfile)), n'attach(1): Not a file', } check {status = {head='master'}} end) it('doesn\'t attach to non-existent files with non-existent sub-dirs', function() edit(scratch..'/does/not/exist') match_debug_messages { 'attach(1): Attaching (trigger=BufNewFile)', n'attach(1): Not a path', } helpers.pcall_err(get_buf_var, 'gitsigns_head') helpers.pcall_err(get_buf_var, 'gitsigns_status_dict') end) it('can run copen', function() command("copen") match_debug_messages { 'attach(2): Attaching (trigger=BufReadPost)', n'attach(2): Non-normal buffer', } end) it('can run get_hunks()', function() edit(test_file) insert("line1") feed("oline2") expectf(function() eq({{ head = '@@ -1,1 +1,2 @@', type = 'change', lines = { '-This', '+line1This', '+line2' }, added = { count = 2, start = 1, lines = { 'line1This', 'line2' } }, removed = { count = 1, start = 1, lines = { 'This'} }, }}, exec_lua[[return require'gitsigns'.get_hunks()]] ) end) end) end) describe('current line blame', function() before_each(function() config.current_line_blame = true config.current_line_blame_formatter = ' , - ' setup_gitsigns(config) end) local function blame_line_ui_test(autocrlf, file_ending) setup_test_repo() git{'config', 'core.autocrlf', autocrlf} if file_ending == 'dos' then system("printf 'This\r\nis\r\na\r\nwindows\r\nfile\r\n' > "..newfile) else system("printf 'This\nis\na\nwindows\nfile\n' > "..newfile) end git{'add', newfile} git{"commit", "-m", "commit on main"} edit(newfile) feed('gg') check { signs = {} } -- Wait until the virtual blame line appears screen:sleep(1000) screen:expect{grid=[[ ^{MATCH:This {6: tester, %d seco}}| is | a | windows | file | {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| ]]} end it('does handle dos fileformats', function() -- Add a file with windows line ending into the repo -- Disable autocrlf, so that the file keeps the \r\n file endings. blame_line_ui_test('false', 'dos') end) it('does handle autocrlf', function() blame_line_ui_test('true', 'dos') end) it('does handle unix', function() blame_line_ui_test('false', 'unix') end) end) -- TODO(lewis6991): All deprecated fields removed. Re-add when we have another deprecated field -- describe('configuration', function() -- it('handled deprecated fields', function() -- pending() -- -- config.current_line_blame_delay = 100 -- -- setup_gitsigns(config) -- -- eq(100, exec_lua([[return package.loaded['gitsigns.config'].config.current_line_blame_opts.delay]])) -- end) -- end) describe('on_attach()', function() it('can prevent attaching to a buffer', function() setup_test_repo{no_add=true} -- Functions can't be serialized over rpc so need to setup config -- remotely setup_gitsigns(config, [[ config.on_attach = function() return false end ]]) edit(test_file) match_debug_messages { 'attach(1): Attaching (trigger=BufReadPost)', np'run_job: git .* config user.name', np'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', np'run_job: git .* rev%-parse %-%-short HEAD', np'run_job: git .* %-%-git%-dir .* %-%-stage %-%-others %-%-exclude%-standard %-%-eol.*', n'attach(1): User on_attach() returned false', } end) end) describe('change_base()', function() it('works', function() setup_test_repo() edit(test_file) feed('oEDIT') command('write') git{'add', test_file} git{"commit", "-m", "commit on main"} -- Don't setup gitsigns until the repo has two commits setup_gitsigns(config) check { status = {head='master', added=0, changed=0, removed=0}, signs = {} } command('Gitsigns change_base ~') check { status = {head='master', added=1, changed=0, removed=0}, signs = {added=1} } end) end) local function testsuite(internal_diff) return function() before_each(function() config.diff_opts = { internal = internal_diff } setup_test_repo() end) it('apply basic signs', function() setup_gitsigns(config) edit(test_file) command("set signcolumn=yes") feed("dd") -- Top delete feed("j") feed("o") -- Add feed("2j") feed("x") -- Change feed("3j") feed("dd") -- Delete feed("j") feed("ddx") -- Change delete check { status = {head='master', added=1, changed=2, removed=3}, signs = {topdelete=1, changedelete=1, added=1, delete=1, changed=1} } end) it('can enable numhl', function() config.numhl = true setup_gitsigns(config) edit(test_file) command("set signcolumn=no") command("set number") feed("dd") -- Top delete feed("j") feed("o") -- Add feed("2j") feed("x") -- Change feed("3j") feed("dd") -- Delete feed("j") feed("ddx") -- Change delete -- screen:snapshot_util() screen:expect{grid=[[ {4: 1 }is | {5: 2 }a | {3: 3 } | {5: 4 }file | {2: 5 }sed | {5: 6 }for | {4: 7 }testing | {5: 8 }The | {2: 9 }^oesn't | {5: 10 }matter, | {5: 11 }it | {5: 12 }just | {5: 13 }needs | {5: 14 }to | {5: 15 }be | {5: 16 }static. | {6:~ }| ]]} end) it('attaches to newly created files', function() setup_gitsigns(config) edit(newfile) match_debug_messages{ 'attach(1): Attaching (trigger=BufNewFile)', np'run_job: git .* config user.name', np'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', np'run_job: git .* ls%-files .*', n'attach(1): Not a file', } command("write") local messages = { 'attach(1): Attaching (trigger=BufWritePost)', np"run_job: git .* config user.name", np'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', np'run_job: git .* ls%-files .*', n'watch_gitdir(1): Watching git dir', np'run_job: git .* show :0:newfile.txt' } if not internal_diff then table.insert(messages, np'run_job: git .* diff .* /tmp/lua_.* /tmp/lua_.*') end local jobs = internal_diff and 8 or 9 table.insert(messages, n("update(1): updates: 1, jobs: "..jobs)) match_debug_messages(messages) check { status = {head='master', added=1, changed=0, removed=0}, signs = {untracked=1} } end) it('can add untracked files to the index', function() setup_gitsigns(config) edit(newfile) feed("iline") check{ status = {head='master'}} command("write") check { status = {head='master', added=1, changed=0, removed=0}, signs = {untracked=1} } feed('mhs') -- Stage the file (add file to index) check { status = {head='master', added=0, changed=0, removed=0}, signs = {} } end) it('tracks files in new repos', function() setup_gitsigns(config) system{"touch", newfile} edit(newfile) feed("iEDIT") command("write") check { status = {head='master', added=1, changed=0, removed=0}, signs = {untracked=1} } git{"add", newfile} check { status = {head='master', added=0, changed=0, removed=0}, signs = {} } git{"reset"} check { status = {head='master', added=1, changed=0, removed=0}, signs = {untracked=1} } end) it('can detach from buffers', function() setup_gitsigns(config) edit(test_file) command("set signcolumn=yes") feed("dd") -- Top delete feed("j") feed("o") -- Add feed("2j") feed("x") -- Change feed("3j") feed("dd") -- Delete feed("j") feed("ddx") -- Change delete check { status = {head='master', added=1, changed=2, removed=3}, signs = {topdelete=1, added=1, changed=1, delete=1, changedelete=1} } command('Gitsigns detach') check { status = {}, signs = {} } end) it('can stages file with merge conflicts', function() setup_gitsigns(config) command("set signcolumn=yes") -- Edit a file and commit it on main branch edit(test_file) check{ status = {head='master', added=0, changed=0, removed=0} } feed('iedit') check{ status = {head='master', added=0, changed=1, removed=0} } command("write") command("bdelete") git{'add', test_file} git{"commit", "-m", "commit on main"} -- Create a branch, remove last commit, edit file again git{'checkout', '-B', 'abranch'} git{'reset', '--hard', 'HEAD~1'} edit(test_file) check{ status = {head='abranch', added=0, changed=0, removed=0} } feed('idiff') check{ status = {head='abranch', added=0, changed=1, removed=0} } command("write") command("bdelete") git{'add', test_file} git{"commit", "-m", "commit on branch"} git{"rebase", "master"} -- test_file should have a conflict edit(test_file) check { status = {head='HEAD(rebasing)', added=4, changed=1, removed=0}, signs = {changed=1, added=4} } exec_lua('require("gitsigns.actions").stage_hunk()') check { status = {head='HEAD(rebasing)', added=0, changed=0, removed=0}, signs = {} } end) it('handle files with spaces', function() setup_gitsigns(config) command("set signcolumn=yes") local spacefile = scratch..'/a b c d' write_to_file(spacefile, {'spaces', 'in', 'file'}) edit(spacefile) check { status = {head='master', added=3, removed=0, changed=0}, signs = {untracked=3} } git{'add', spacefile} edit(spacefile) check { status = {head='master', added=0, removed=0, changed=0}, signs = {} } end) end end -- Run regular config describe('diff-ext', testsuite(false)) -- Run with: -- - internal diff (ffi) -- - decoration provider describe('diff-int', testsuite(true)) it('can handle vimgrep', function() setup_test_repo() write_to_file(scratch..'/t1.txt', {'hello ben'}) write_to_file(scratch..'/t2.txt', {'hello ben'}) write_to_file(scratch..'/t3.txt', {'hello lewis'}) setup_gitsigns(config) helpers.exc_exec("vimgrep ben "..scratch..'/*') screen:expect{messages = {{ kind = "quickfix", content = { { "(1 of 2): hello ben" } }, }}} match_debug_messages { 'attach(2): attaching is disabled', n'attach(3): attaching is disabled', n'attach(4): attaching is disabled', n'attach(5): attaching is disabled', } end) it('show short SHA when detached head', function() setup_test_repo() git{"checkout", "--detach"} -- Disable debug_mode so the sha is calculated config.debug_mode = false setup_gitsigns(config) edit(test_file) -- SHA is not deterministic so just check it can be cast as a hex value expectf(function() helpers.neq(nil, tonumber('0x'..get_buf_var('gitsigns_head'))) end) end) it('handles a quick undo', function() setup_test_repo() setup_gitsigns(config) edit(test_file) -- This test isn't deterministic so run it a few times for _ = 1, 3 do feed("x") check { signs = {changed=1} } feed("u") check { signs = {} } end end) it('handles filenames with unicode characters', function() screen:try_resize(20,2) setup_test_repo() setup_gitsigns(config) local uni_filename = scratch..'/föobær' write_to_file(uni_filename, {'Lorem ipsum'}) git{"add", uni_filename} git{"commit", "-m", "another commit"} edit(uni_filename) screen:expect{grid=[[ ^Lorem ipsum | {6:~ }| ]]} feed 'x' screen:expect{grid=[[ {2:~ }^orem ipsum | {6:~ }| ]]} end) it('handle #521', function() screen:detach() screen:attach({ext_messages=false}) screen:try_resize(20,3) setup_test_repo() setup_gitsigns(config) edit(test_file) feed('dd') local function check_screen() screen:expect{grid=[[ {4:^ }^is | {1: }a | {1: }file | ]]} end check_screen() -- Write over the text with itself. This will remove all the signs but the -- calculated hunks won't change. exec_lua[[ local text = vim.api.nvim_buf_get_lines(0, 0, -1, false) vim.api.nvim_buf_set_lines(0, 0, -1, true, text) ]] check_screen() end) end)