diff --git a/CHANGELOG.md b/CHANGELOG.md index 3895fc5..4192bd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to the Vim Plugin Manager will be documented in this file. ## [Unreleased] +### Added +- `:PluginManager status` reports detached plugins as "Detached" with an + actionable re-pin hint (nearest reachable tag, computed offline) instead + of silently rendering them as "Up-to-date". A detached plugin cannot be + pulled - update skips it - so the status now surfaces the decision the + user must make: re-declare a pin in the vimrc or reattach a branch. + ### Performance - `git#collect_status_local`: worst case reduced from 9 git subprocesses per module to 7 (6 when a branch is declared in `.gitmodules`). The diff --git a/autoload/plugin_manager/cmd/status.vim b/autoload/plugin_manager/cmd/status.vim index 2b3b0c4..d4b64f7 100644 --- a/autoload/plugin_manager/cmd/status.vim +++ b/autoload/plugin_manager/cmd/status.vim @@ -128,6 +128,7 @@ function! s:status_symbol(status) abort \ 'Missing': 'fail', \ 'Ahead': 'info', \ 'Custom branch': 'info', + \ 'Detached': 'warn', \ } let l:key = get(l:keyword_map, a:status, 'info') return plugin_manager#ui#get_status_glyph(l:key) @@ -157,6 +158,15 @@ function! s:get_module_status_info(module) abort let l:update_status = plugin_manager#git#collect_status_local(l:path) + " A detached module must never render as Up-to-date: it cannot be + " pulled (update skips it) and it needs an explicit decision from the + " user: re-declare a pin, or reattach a branch. + if l:update_status.branch ==# 'detached' + let l:info.status = 'Detached' + let l:info.details = s:detached_hint(l:path) + return l:info + endif + if l:update_status.different_branch && l:update_status.branch !=# 'detached' let l:info.status = 'Custom branch' let l:info.details = l:update_status.branch @@ -182,3 +192,20 @@ function! s:get_module_status_info(module) abort return l:info endfunction +" Build the actionable hint for a detached module (no vimrc pin declared). +" Offline: the nearest reachable tag (git describe --tags --abbrev=0) is +" the re-pin candidate - if the module was once pinned, re-declaring that +" tag in the vimrc re-asserts it on every update. +function! s:detached_hint(path) abort + let l:res = plugin_manager#git#execute('git describe --tags --abbrev=0', + \ a:path, 0, 0) + if l:res.success + let l:tag = substitute(l:res.output, '\n', '', 'g') + if !empty(l:tag) + return 'nearest tag ' . l:tag + \ . ' - declare a pin to re-assert it or reattach a branch' + endif + endif + return 'declare a pin or reattach a branch' +endfunction + diff --git a/dbg3.out b/dbg3.out new file mode 100644 index 0000000..a997842 Binary files /dev/null and b/dbg3.out differ diff --git a/doc/plugin_manager.txt b/doc/plugin_manager.txt index 4c36b18..055c723 100644 --- a/doc/plugin_manager.txt +++ b/doc/plugin_manager.txt @@ -317,7 +317,12 @@ The plugin provides the following commands: :PluginManager list List all installed plugins -:PluginManager status Show status of installed plugins +:PluginManager status Show status of installed plugins. + A plugin whose HEAD is detached (e.g. it was + manually checked out at a tag) is reported + as "Detached" with a re-pin hint: declare a + {'tag': ...} pin in the vimrc to re-assert + it on every update, or reattach a branch. :PluginManager update [plugin] Update all plugins or a specific one. Plugins pinned by 'tag' or 'commit' in the diff --git a/tests/status.vader b/tests/status.vader index 959240b..11f8fe4 100644 --- a/tests/status.vader +++ b/tests/status.vader @@ -41,3 +41,72 @@ Execute (status renders block instantly and resolves missing plugin): Assert full =~# 'myplugin', 'plugin line should contain myplugin' Assert full =~# 'Missing', 'missing plugin should show Missing' Assert full =~# 'checked', 'footer should report plugins checked' + +Execute (detached module shows Detached with the nearest-tag re-pin hint): + " A module manually checked out at a tag (no vimrc pin declared) used to + " fall through to 'Up-to-date' in status. It must be reported as + " Detached with an actionable hint (nearest tag), never as Up-to-date. + let g:plugin_manager_test_force_sync = 1 + let g:_pm_stat_mod = g:_pm_stat_tmp . '/pack/plugins/start/detached' + call mkdir(g:_pm_stat_mod, 'p') + call system('git init -q ' . shellescape(g:_pm_stat_mod)) + call system('git -C ' . shellescape(g:_pm_stat_mod) . ' symbolic-ref HEAD refs/heads/main') + call system('git -C ' . shellescape(g:_pm_stat_mod) . ' config user.email "t@t.com"') + call system('git -C ' . shellescape(g:_pm_stat_mod) . ' config user.name "T"') + call writefile(['v1'], g:_pm_stat_mod . '/f') + call system('git -C ' . shellescape(g:_pm_stat_mod) . ' add .') + call system('git -C ' . shellescape(g:_pm_stat_mod) . ' commit -qm v1') + call system('git -C ' . shellescape(g:_pm_stat_mod) . ' tag v0.9.9') + call writefile(['v2'], g:_pm_stat_mod . '/f') + call system('git -C ' . shellescape(g:_pm_stat_mod) . ' commit -qam v2') + call system('git -C ' . shellescape(g:_pm_stat_mod) . ' remote add origin ' . shellescape(g:_pm_stat_mod)) + " Detach at the tip past the tag: no vimrc pin declared. + call system('git -C ' . shellescape(g:_pm_stat_mod) . ' checkout -q --detach HEAD') + call writefile([ + \ '[submodule "detached"]', + \ ' path = pack/plugins/start/detached', + \ ' url = https://github.com/user/detached.git', + \ ], g:_pm_stat_tmp . '/.gitmodules') + call plugin_manager#git#refresh_modules_cache() + + call plugin_manager#cmd#status#execute() + let sbuf = bufnr('PluginManager') + let full = join(getbufline(sbuf, 1, '$'), "\n") + Assert full =~# 'Detached', + \ 'detached module must be reported as Detached, got: ' . full + Assert full =~# 'nearest tag v0.9.9', + \ 'the hint should carry the nearest tag, got: ' . full + Assert full !~# '\cUp-to-date', + \ 'a detached module must never render as Up-to-date' + unlet g:plugin_manager_test_force_sync + unlet g:_pm_stat_mod + +Execute (detached module without any tag gets the plain reattach hint): + " Before re-runs per case: rebuild the fixture from scratch, without tags. + let g:plugin_manager_test_force_sync = 1 + let g:_pm_stat_mod2 = g:_pm_stat_tmp . '/pack/plugins/start/detached' + call mkdir(g:_pm_stat_mod2, 'p') + call system('git init -q ' . shellescape(g:_pm_stat_mod2)) + call system('git -C ' . shellescape(g:_pm_stat_mod2) . ' symbolic-ref HEAD refs/heads/main') + call system('git -C ' . shellescape(g:_pm_stat_mod2) . ' config user.email "t@t.com"') + call system('git -C ' . shellescape(g:_pm_stat_mod2) . ' config user.name "T"') + call writefile(['v1'], g:_pm_stat_mod2 . '/f') + call system('git -C ' . shellescape(g:_pm_stat_mod2) . ' add .') + call system('git -C ' . shellescape(g:_pm_stat_mod2) . ' commit -qm v1') + call system('git -C ' . shellescape(g:_pm_stat_mod2) . ' remote add origin ' . shellescape(g:_pm_stat_mod2)) + call system('git -C ' . shellescape(g:_pm_stat_mod2) . ' checkout -q --detach HEAD') + call writefile([ + \ '[submodule "detached"]', + \ ' path = pack/plugins/start/detached', + \ ' url = https://github.com/user/detached.git', + \ ], g:_pm_stat_tmp . '/.gitmodules') + call plugin_manager#git#refresh_modules_cache() + call plugin_manager#cmd#status#execute() + let sbuf = bufnr('PluginManager') + let full = join(getbufline(sbuf, 1, '$'), "\n") + Assert full =~# 'Detached', + \ 'detached module must be reported as Detached, got: ' . full + Assert full =~# 'declare a pin or reattach', + \ 'tagless detached module should get the plain hint, got: ' . full + unlet g:plugin_manager_test_force_sync + unlet g:_pm_stat_mod2