From 290f95f05122f151f2e414ca433dd7f583da7734 Mon Sep 17 00:00:00 2001 From: log0u7 <70974447+log0u7@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:28:00 +0200 Subject: [PATCH 1/2] feat(status): report detached plugins with a re-pin hint A module whose HEAD is detached with no vimrc pin declared (e.g. a manual tag checkout during a maintenance session) used to render as 'Up-to-date' in status while update silently skips it - the user had no signal to act on. status now reports 'Detached' (warn glyph) with an offline hint: the nearest reachable tag (git describe --tags --abbrev=0, +1 subprocess for detached modules only) as the re-pin candidate, or the plain 'declare a pin or reattach a branch' fallback. Update messages untouched (pinned verbatim by tests/update.vader). tests/status.vader covers both hint variants (tag found / no tag). --- CHANGELOG.md | 7 +++ autoload/plugin_manager/cmd/status.vim | 27 ++++++++++ dbg3.out | Bin 0 -> 406 bytes dbg3.vim | 39 ++++++++++++++ doc/plugin_manager.txt | 7 ++- tests/status.vader | 69 +++++++++++++++++++++++++ 6 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 dbg3.out create mode 100644 dbg3.vim 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 0000000000000000000000000000000000000000..a997842731b75dc2a9323ed71b6a906186e948a6 GIT binary patch literal 406 zcmXR*%*jl#jaE-dElEtyNKH|XMvNW{SKXx0J+m`Qp# zfPxFuDg}+a)Wo9H;u3|D#B_x+13gPUO9fq^{^T5>utK6j0nimC`3gm;x{1ZbsYN9U znI#JOML>RH3COWP6-h;jdC3`?K#N;;Di|shfL&g!0Q5+5c4`V&8OU=+1LZqZkC5bW E0Im$5WB>pF literal 0 HcmV?d00001 diff --git a/dbg3.vim b/dbg3.vim new file mode 100644 index 0000000..43e0248 --- /dev/null +++ b/dbg3.vim @@ -0,0 +1,39 @@ +set nocompatible +set rtp^=/home/logout/projets/wt/vim-plugin-manager/detached-hint +let g:o = [] +try + let g:tmp = '/tmp/pm-dbg-det' + call delete(g:tmp, 'rf') + call mkdir(g:tmp . '/.git', 'p') + call writefile(['[submodule "detached"]', ' path = pack/plugins/start/detached', ' url = https://github.com/user/detached.git'], g:tmp . '/.gitmodules') + let g:plugin_manager_vim_dir = g:tmp + let g:plugin_manager_plugins_dir = g:tmp . '/pack/plugins' + let g:plugin_manager_test_force_sync = 1 + let g:m = g:tmp . '/pack/plugins/start/detached' + call mkdir(g:m, 'p') + call system('git init -q ' . shellescape(g:m)) + call system('git -C ' . shellescape(g:m) . ' symbolic-ref HEAD refs/heads/main') + call system('git -C ' . shellescape(g:m) . ' config user.email t@t') + call system('git -C ' . shellescape(g:m) . ' config user.name T') + call writefile(['v1'], g:m . '/f') + call system('git -C ' . shellescape(g:m) . ' add .') + call system('git -C ' . shellescape(g:m) . ' commit -qm v1') + call system('git -C ' . shellescape(g:m) . ' tag v0.9.9') + call writefile(['v2'], g:m . '/f') + call system('git -C ' . shellescape(g:m) . ' commit -qam v2') + call system('git -C ' . shellescape(g:m) . ' remote add origin ' . shellescape(g:m)) + call system('git -C ' . shellescape(g:m) . ' checkout -q --detach HEAD') + call add(g:o, 'valid=' . string(map(plugin_manager#git#valid_modules(), 'v:val.short_name'))) + call plugin_manager#cmd#status#execute() + call add(g:o, join(getbufline(bufnr('PluginManager'), 1, '$'), "\n")) + " now remove tag and re-run + call system('git -C ' . shellescape(g:m) . ' tag -d v0.9.9') + call plugin_manager#git#refresh_modules_cache() + call add(g:o, 'valid2=' . string(map(plugin_manager#git#valid_modules(), 'v:val.short_name'))) + call plugin_manager#cmd#status#execute() + call add(g:o, join(getbufline(bufnr('PluginManager'), 1, '$'), "\n")) +catch + call add(g:o, 'ERR: ' . v:exception . ' @ ' . v:throwpoint) +endtry +call writefile(g:o, 'dbg3.out') +qa! 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 From 6ee7965b1ed1cf3ecb6ece3303a0b5636c24d542 Mon Sep 17 00:00:00 2001 From: log0u7 <70974447+log0u7@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:28:09 +0200 Subject: [PATCH 2/2] chore: drop debug script --- dbg3.vim | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 dbg3.vim diff --git a/dbg3.vim b/dbg3.vim deleted file mode 100644 index 43e0248..0000000 --- a/dbg3.vim +++ /dev/null @@ -1,39 +0,0 @@ -set nocompatible -set rtp^=/home/logout/projets/wt/vim-plugin-manager/detached-hint -let g:o = [] -try - let g:tmp = '/tmp/pm-dbg-det' - call delete(g:tmp, 'rf') - call mkdir(g:tmp . '/.git', 'p') - call writefile(['[submodule "detached"]', ' path = pack/plugins/start/detached', ' url = https://github.com/user/detached.git'], g:tmp . '/.gitmodules') - let g:plugin_manager_vim_dir = g:tmp - let g:plugin_manager_plugins_dir = g:tmp . '/pack/plugins' - let g:plugin_manager_test_force_sync = 1 - let g:m = g:tmp . '/pack/plugins/start/detached' - call mkdir(g:m, 'p') - call system('git init -q ' . shellescape(g:m)) - call system('git -C ' . shellescape(g:m) . ' symbolic-ref HEAD refs/heads/main') - call system('git -C ' . shellescape(g:m) . ' config user.email t@t') - call system('git -C ' . shellescape(g:m) . ' config user.name T') - call writefile(['v1'], g:m . '/f') - call system('git -C ' . shellescape(g:m) . ' add .') - call system('git -C ' . shellescape(g:m) . ' commit -qm v1') - call system('git -C ' . shellescape(g:m) . ' tag v0.9.9') - call writefile(['v2'], g:m . '/f') - call system('git -C ' . shellescape(g:m) . ' commit -qam v2') - call system('git -C ' . shellescape(g:m) . ' remote add origin ' . shellescape(g:m)) - call system('git -C ' . shellescape(g:m) . ' checkout -q --detach HEAD') - call add(g:o, 'valid=' . string(map(plugin_manager#git#valid_modules(), 'v:val.short_name'))) - call plugin_manager#cmd#status#execute() - call add(g:o, join(getbufline(bufnr('PluginManager'), 1, '$'), "\n")) - " now remove tag and re-run - call system('git -C ' . shellescape(g:m) . ' tag -d v0.9.9') - call plugin_manager#git#refresh_modules_cache() - call add(g:o, 'valid2=' . string(map(plugin_manager#git#valid_modules(), 'v:val.short_name'))) - call plugin_manager#cmd#status#execute() - call add(g:o, join(getbufline(bufnr('PluginManager'), 1, '$'), "\n")) -catch - call add(g:o, 'ERR: ' . v:exception . ' @ ' . v:throwpoint) -endtry -call writefile(g:o, 'dbg3.out') -qa!