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
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,39 @@

All notable changes to the Vim Plugin Manager will be documented in this file.

## [Unreleased]

### Fixed
- **No silent failures left** (#7): every degraded outcome writes a WARN
line (always logged, `debug_mode`-independent) and sidebar labels tell
the truth:
- `git#add_submodule`: the pointer commit result is checked; a failure
warns and fails the add instead of returning success.
- `remove`: `git submodule deinit` and `commit_removal` results are
checked; a failed pointer commit downgrades the op to
"Removed (pointer commit failed)" instead of a plain "Removed".
- `remove`: a module path that is already absolute is no longer
prefixed with `vim_dir` twice (filesystem-search removals).
- `update`: unreachable pin target warns, not sidebar-only.
- `update`: `stash_if_needed` returns 0 when `git stash push` fails
(previously returned 1, causing a phantom `stash pop`).
- `update`: auto-commit failures downgrade the op to
"Updated (auto-commit failed)" and add a truthful footer line.
- `vimrc`: unparsable declarations log at WARN instead of the
debug-gated level.
- `helptags`: a real `:helptags` failure is reported as
"Helptags failed" + WARN, no longer as the "skip / No doc directory".
- `check`: empty catch around `on_done` now warns.
- `async`: callback errors write a WARN in addition to `:messages`.
- `add` (local): post-install exec failure warns.
- `declare`: invalid URL format warns (both sync and async batch paths).
- `backup`: `cp` and `git add` results are checked and warn.
- `util#ensure_directory`: mkdir failures warn.

### Added
- `tests/log_warnings.vader`: 15 regression tests asserting the WARN line
exists with `debug_mode` unset (contract from issue #5).

## [2.2.7] - 2026-09-19

### Fixed
Expand Down
5 changes: 4 additions & 1 deletion autoload/plugin_manager/async.vim
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,10 @@ function! s:process_job_completion(job_id) abort
\ 'cmd': l:job.cmd
\ })
catch
" Handle callback errors
" Handle callback errors: surface on :messages and in the log (a
" swallowed callback error must never be invisible).
call plugin_manager#core#log#warn('async',
\ 'async callback error: ' . v:exception)
echohl ErrorMsg
echomsg "Error in async callback: " . v:exception
echohl None
Expand Down
3 changes: 3 additions & 0 deletions autoload/plugin_manager/cmd/add.vim
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ function! s:install_local_plugin(path, options) abort
if !empty(get(a:options, 'exec', ''))
let l:result = plugin_manager#core#util#run_in_dir(a:options.exec, l:plugin_dir)
if !l:result.success
call plugin_manager#ui#log_detail('add',
\ 'post-install exec failed: ' . a:options.exec . ' - '
\ . l:result.output, 'warn')
call plugin_manager#ui#complete_operation(l:op_id, 'fail', 'Exec failed')
return 0
endif
Expand Down
13 changes: 11 additions & 2 deletions autoload/plugin_manager/cmd/backup.vim
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,17 @@ function! s:backup_vimrc_file() abort
if plugin_manager#core#util#file_exists(l:vimrc_path)
let l:copy_cmd = 'cp ' . shellescape(l:vimrc_path) . ' ' . shellescape(l:local_vimrc)
let l:vim_dir = plugin_manager#core#util#get_config('vim_dir', '')
call plugin_manager#core#util#run_in_dir(l:copy_cmd, '')
call plugin_manager#git#execute('git add ' . shellescape(l:local_vimrc), l:vim_dir, 0, 0)
let l:copy_result = plugin_manager#core#util#run_in_dir(l:copy_cmd, '')
if !l:copy_result.success
call plugin_manager#ui#log_detail('backup',
\ 'backup cp failed: ' . l:copy_result.output, 'warn')
endif
let l:add_result = plugin_manager#git#execute(
\ 'git add ' . shellescape(l:local_vimrc), l:vim_dir, 0, 0)
if !l:add_result.success
call plugin_manager#ui#log_detail('backup',
\ 'backup git add failed: ' . l:add_result.output, 'warn')
endif
endif
endfunction

Expand Down
5 changes: 4 additions & 1 deletion autoload/plugin_manager/cmd/check.vim
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,10 @@ function! s:finish(plugins, opts) abort
try
call a:opts.on_done(a:plugins)
catch
" Do not let callback failures bubble up
" Do not let callback failures bubble up, but leave a trace: a
" swallowed callback error must never be invisible.
call plugin_manager#core#log#warn('check',
\ 'on_done callback failed: ' . v:exception)
endtry
endif
endfunction
4 changes: 4 additions & 0 deletions autoload/plugin_manager/cmd/declare.vim
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ function! s:process_declarations_async() abort
if empty(l:full_url) || empty(l:name)
let l:op_id = plugin_manager#ui#start_operation(
\ empty(l:name) ? fnamemodify(l:plugin.url, ':t') : l:name, 'Processing')
call plugin_manager#ui#log_detail('declare',
\ 'invalid URL format: ' . l:plugin.url, 'warn')
call plugin_manager#ui#complete_operation(l:op_id, 'fail', 'Invalid URL format')
let l:ctx.errors += 1
continue
Expand Down Expand Up @@ -266,6 +268,8 @@ function! s:process_plugin(url, options) abort
let l:full_url = plugin_manager#core#util#convert_to_full_url(a:url)
if empty(l:full_url)
let l:plugin_name = fnamemodify(a:url, ':t')
call plugin_manager#ui#log_detail('declare',
\ 'invalid URL format: ' . a:url, 'warn')
let l:op_id = plugin_manager#ui#start_operation(l:plugin_name, 'Processing')
call plugin_manager#ui#complete_operation(l:op_id, 'fail', 'Invalid URL format')
return 'error'
Expand Down
10 changes: 10 additions & 0 deletions autoload/plugin_manager/cmd/helptags.vim
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ function! s:generate_for_plugin(plugin_name, plugin_path, ...) abort
if s:generate_helptag(a:plugin_path)
call plugin_manager#ui#complete_operation(l:op_id, 'ok', 'Helptags generated')
return 1
elseif isdirectory(a:plugin_path . '/doc')
" The doc directory exists but :helptags failed: that is a failure,
" not a skip - label it truthfully (details in the log).
call plugin_manager#ui#complete_operation(l:op_id, 'fail',
\ 'Helptags failed (see log)')
return 0
else
call plugin_manager#ui#complete_operation(l:op_id, 'skip', 'No doc directory')
return 0
Expand All @@ -140,6 +146,10 @@ function! s:generate_helptag(plugin_path) abort
execute 'helptags ' . fnameescape(l:doc_path)
return 1
catch
" A failed :helptags used to be indistinguishable from a missing doc
" directory: warn so the real error is visible in a default setup.
call plugin_manager#core#log#warn('helptags',
\ 'helptags failed in ' . a:plugin_path . ': ' . v:exception)
return 0
endtry
endif
Expand Down
54 changes: 42 additions & 12 deletions autoload/plugin_manager/cmd/remove.vim
Original file line number Diff line number Diff line change
Expand Up @@ -163,34 +163,49 @@ function! s:remove_module(module_name, module_path) abort
let l:module_info = s:get_module_metadata(a:module_path)

" git submodule deinit and git rm take the repo-relative path as argument
" but must run inside the repo root (vim_dir).
call plugin_manager#git#execute(
\ 'git submodule deinit -f ' . shellescape(a:module_path), l:vim_dir, 0, 0)
" but must run inside the repo root (vim_dir). Discovery may hand us an
" absolute path (filesystem search): normalize so the vim_dir prefix is
" never applied twice.
let l:rel_path = plugin_manager#core#util#make_relative_path(a:module_path)

let l:deinit_result = plugin_manager#git#execute(
\ 'git submodule deinit -f ' . shellescape(l:rel_path), l:vim_dir, 0, 0)
if !l:deinit_result.success
call plugin_manager#ui#log_detail('remove',
\ 'submodule deinit failed for ' . l:rel_path . ': '
\ . l:deinit_result.output, 'warn')
endif
let l:result = plugin_manager#git#execute(
\ 'git rm -f ' . shellescape(a:module_path), l:vim_dir, 0, 0)
\ 'git rm -f ' . shellescape(l:rel_path), l:vim_dir, 0, 0)

if !l:result.success
" Fallback: delete the working tree directory directly using absolute path
let l:abs_path = empty(l:vim_dir) ? a:module_path : (l:vim_dir . '/' . a:module_path)
let l:abs_path = empty(l:vim_dir) ? l:rel_path : (l:vim_dir . '/' . l:rel_path)
call plugin_manager#ui#log_detail('remove', 'git rm failed, removing path manually: ' . l:abs_path, 'warn')
call plugin_manager#core#util#remove_path(l:abs_path)
endif

" Remove the cached git metadata for this submodule (absolute path)
let l:git_modules_path = l:vim_dir . '/.git/modules/' . a:module_path
let l:git_modules_path = l:vim_dir . '/.git/modules/' . l:rel_path
if plugin_manager#core#util#dir_exists(l:git_modules_path)
call plugin_manager#core#util#remove_path(l:git_modules_path)
endif

" Removal outcome: the working tree directory must be gone. Report the
" truth instead of an unconditional 'ok'.
let l:abs_module = empty(l:vim_dir) ? a:module_path : (l:vim_dir . '/' . a:module_path)
let l:abs_module = empty(l:vim_dir) ? l:rel_path : (l:vim_dir . '/' . l:rel_path)
let l:success = !plugin_manager#core#util#dir_exists(l:abs_module)

if l:success
call s:commit_removal(a:module_name, l:module_info)
call plugin_manager#ui#complete_operation(l:op_id, 'ok', 'Removed')
call plugin_manager#ui#footer([plugin_manager#ui#success('Plugin removed')])
if s:commit_removal(a:module_name, l:module_info)
call plugin_manager#ui#complete_operation(l:op_id, 'ok', 'Removed')
call plugin_manager#ui#footer([plugin_manager#ui#success('Plugin removed')])
else
call plugin_manager#ui#complete_operation(l:op_id, 'warn',
\ 'Removed (pointer commit failed: see log)')
call plugin_manager#ui#footer([plugin_manager#ui#warning(
\ 'Plugin removed, but the pointer commit failed (see log)')])
endif
else
call plugin_manager#ui#complete_operation(l:op_id, 'fail',
\ 'Removal incomplete: ' . l:abs_module . ' still exists (see log)')
Expand All @@ -216,6 +231,9 @@ function! s:get_module_metadata(module_path) abort
return {}
endfunction

" Stage the .gitmodules update and record the removal as its own history
" entry. Returns 1 when a commit was created, 0 when every commit attempt
" failed (the failure is logged: the pointer state is degraded).
function! s:commit_removal(module_name, module_info) abort
let l:commit_msg = "Remove " . a:module_name . " plugin"
let l:vim_dir = plugin_manager#core#util#get_config('vim_dir', '')
Expand All @@ -225,13 +243,25 @@ function! s:commit_removal(module_name, module_info) abort
endif

" Stage .gitmodules (updated by git rm); run in vim_dir for repo-root scope.
call plugin_manager#git#execute('git add .gitmodules', l:vim_dir, 0, 0)
let l:add_result = plugin_manager#git#execute(
\ 'git add .gitmodules', l:vim_dir, 0, 0)
if !l:add_result.success
call plugin_manager#ui#log_detail('remove',
\ 'git add .gitmodules failed: ' . l:add_result.output, 'warn')
endif
" Try to commit; if nothing to commit (already removed via rm), create an
" empty commit so the removal is recorded as a separate history entry.
let l:result = plugin_manager#git#execute(
\ 'git commit -m ' . shellescape(l:commit_msg), l:vim_dir, 0, 0)
if !l:result.success
call plugin_manager#git#execute(
let l:result = plugin_manager#git#execute(
\ 'git commit --allow-empty -m ' . shellescape(l:commit_msg), l:vim_dir, 0, 0)
endif
if !l:result.success
call plugin_manager#ui#log_detail('remove',
\ 'pointer commit failed for ' . a:module_name . ': '
\ . l:result.output, 'warn')
return 0
endif
return 1
endfunction
61 changes: 51 additions & 10 deletions autoload/plugin_manager/cmd/update.vim
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ function! s:on_update_complete(ctx, result) abort
call plugin_manager#ui#complete_operation(l:op_id, 'ok', 'Updated')
call plugin_manager#cmd#helptags#execute(0, l:module_name, 1)
let l:module_path = get(a:ctx.current_module, 'path', '')
call s:commit_update_async(l:module_name, l:module_path)
if s:commit_update_async(l:module_name, l:module_path)
call plugin_manager#ui#complete_operation(l:op_id, 'warn',
\ 'Updated (auto-commit failed: see log)')
endif
else
call plugin_manager#ui#complete_operation(l:op_id, 'info', 'Up-to-date')
endif
Expand Down Expand Up @@ -239,6 +242,9 @@ function! s:sync_pinned(ctx, module, pin, current_commit, op_id) abort
let l:res = plugin_manager#git#execute(
\ 'git rev-parse ' . shellescape(l:ref . '^{commit}'), l:module_path, 0, 0)
if !l:res.success
" The sidebar alone hides this from :PluginManagerViewLog: warn too.
call plugin_manager#core#log#warn('update',
\ 'Pin target not found: ' . l:ref . ' in ' . l:module_path)
call plugin_manager#ui#complete_operation(a:op_id, 'fail',
\ 'Pin target not found: ' . l:ref)
call s:pin_done(a:ctx, a:module, 0)
Expand Down Expand Up @@ -299,7 +305,10 @@ function! s:pin_done(ctx, module, changed) abort
elseif a:changed
call plugin_manager#cmd#helptags#execute(0, a:module.short_name, 1)
let l:module_path = get(get(a:ctx, 'current_module', {}), 'path', '')
call s:commit_update_async(a:module.short_name, l:module_path)
if s:commit_update_async(a:module.short_name, l:module_path)
call plugin_manager#ui#complete_operation(s:op_id_for(a:ctx, a:module),
\ 'warn', 'Updated (auto-commit failed: see log)')
endif
endif
endfunction

Expand Down Expand Up @@ -485,25 +494,37 @@ function! s:maybe_finalize(ctx) abort
endfunction

function! s:finalize_update_all(ctx) abort
let l:commit_failed = 0
if !empty(a:ctx.updated_modules)
if plugin_manager#core#util#should_auto_commit()
let l:vd = plugin_manager#core#util#get_config('vim_dir', '')
call s:log_silent_failure('git add .gitmodules',
let l:commit_failed = s:log_silent_failure('git add .gitmodules',
\ plugin_manager#git#execute('git add .gitmodules', l:vd, 0, 0))
for l:module in a:ctx.updated_modules
call s:log_silent_failure('git add ' . l:module.path,
let l:commit_failed = s:log_silent_failure('git add ' . l:module.path,
\ plugin_manager#git#execute(
\ 'git add ' . shellescape(l:module.path), l:vd, 0, 0))
\ || l:commit_failed
endfor
call s:log_silent_failure('commit',
let l:commit_failed = s:log_silent_failure('commit',
\ plugin_manager#git#execute(
\ 'git commit -m "Update Modules"', l:vd, 0, 0))
\ || l:commit_failed
endif
for l:module in a:ctx.updated_modules
call plugin_manager#cmd#helptags#execute(0, l:module.short_name, 1)
endfor
endif

" Truthful completion: the plugins updated, but the pointer commit is the
" part that persists the update - a failure must not read as a clean win.
if l:commit_failed
for l:module in a:ctx.updated_modules
call plugin_manager#ui#complete_operation(a:ctx.ops[l:module.short_name],
\ 'warn', 'Updated (auto-commit failed: see log)')
endfor
endif

let l:n = len(a:ctx.updated_modules)
let l:total = len(a:ctx.valid_modules)
let l:failed = len(get(a:ctx, 'fetch_failed', {}))
Expand All @@ -520,6 +541,10 @@ function! s:finalize_update_all(ctx) abort
call add(l:footer, plugin_manager#ui#warning(
\ l:failed . ' of ' . l:total . ' plugins failed to fetch'))
endif
if l:commit_failed
call add(l:footer, plugin_manager#ui#warning(
\ 'auto-commit failed (see log): the update is not recorded in git'))
endif
call plugin_manager#ui#footer(l:footer)
endfunction

Expand All @@ -529,11 +554,14 @@ endfunction

" Log silent git failures (git#execute with throw_on_error=0): a failed
" pointer add/commit must leave a trace in the log, not vanish.
" Returns 1 when the step failed, 0 otherwise.
function! s:log_silent_failure(step, res) abort
if !a:res.success
call plugin_manager#ui#log_detail('update',
\ 'auto-commit ' . a:step . ' failed: ' . a:res.output, 'warn')
return 1
endif
return 0
endfunction

" Stash local changes if any exist. Returns 1 if a stash was created, 0 otherwise.
Expand All @@ -545,7 +573,14 @@ function! s:stash_if_needed(module_path) abort
if !l:status.success || empty(trim(l:status.output))
return 0
endif
call plugin_manager#git#execute('git stash push -u -q', a:module_path, 0, 0)
let l:stash_result = plugin_manager#git#execute('git stash push -u -q', a:module_path, 0, 0)
if !l:stash_result.success
" Never claim a stash exists when the push failed: the caller would
" otherwise pop an unrelated stash after the pull.
call plugin_manager#ui#log_detail('update',
\ 'stash push failed in ' . a:module_path . ': ' . l:stash_result.output, 'warn')
return 0
endif
return 1
endfunction

Expand All @@ -562,21 +597,27 @@ function! s:stash_pop(module_path, op_id) abort
endif
endfunction

" Auto-commit the pointer for a single updated module (mirror of
" s:finalize_update_all). Returns 1 when any step failed so the caller can
" re-complete the operation with a truthful warn.
function! s:commit_update_async(module_name, module_path) abort
if !plugin_manager#core#util#should_auto_commit()
return
return 0
endif
let l:vim_dir = plugin_manager#core#util#get_config('vim_dir', '')
" Three separate calls (mirrors s:finalize_update_all): a compound command
" would only scope the first git -C, and the module name must stay escaped
" so it can never break out of the commit message quoting.
call s:log_silent_failure('git add .gitmodules',
let l:failed = s:log_silent_failure('git add .gitmodules',
\ plugin_manager#git#execute('git add .gitmodules', l:vim_dir, 0, 0))
call s:log_silent_failure('git add ' . a:module_path,
let l:failed = s:log_silent_failure('git add ' . a:module_path,
\ plugin_manager#git#execute(
\ 'git add ' . shellescape(a:module_path), l:vim_dir, 0, 0))
call s:log_silent_failure('commit',
\ || l:failed
let l:failed = s:log_silent_failure('commit',
\ plugin_manager#git#execute(
\ 'git commit -m ' . shellescape('Update Module: ' . a:module_name),
\ l:vim_dir, 0, 0))
\ || l:failed
return l:failed
endfunction
4 changes: 4 additions & 0 deletions autoload/plugin_manager/core/util.vim
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ function! plugin_manager#core#util#ensure_directory(dir) abort
call mkdir(l:dir, 'p')
return 1
catch
" A silent mkdir failure cascades into confusing downstream errors:
" warn so the real cause is visible.
call plugin_manager#core#log#warn('util',
\ 'mkdir failed: ' . l:dir . ': ' . v:exception)
return 0
endtry
endif
Expand Down
Loading
Loading