Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions autoload/plugin_manager/cmd/status.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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

Binary file added dbg3.out
Binary file not shown.
7 changes: 6 additions & 1 deletion doc/plugin_manager.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions tests/status.vader
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading