changeset 136:286e5b3095d0

Allow restricting tag generation to files listed by custom commands This adds a new setting, g:gutentags_file_list_command, which specifies command(s) to use to list files for which tags should be generated, instead of recursively examining all files within the project root. This is useful in projects using source control to restrict tag generation to only files tracked in the repository. This setting is conceptually similar to CtrlP's ctrlp_user_command option. This implements the feature requested in https://github.com/ludovicchabant/vim-gutentags/issues/90
author Stephen Kent <smkent@smkent.net>
date Fri, 22 Jul 2016 19:25:05 -0700
parents 4c9e2de7d46a
children 98cec968205b
files autoload/gutentags.vim autoload/gutentags/cscope.vim autoload/gutentags/ctags.vim doc/gutentags.txt plat/unix/update_scopedb.sh plat/unix/update_tags.sh plat/win32/update_scopedb.cmd plat/win32/update_tags.cmd plugin/gutentags.vim
diffstat 9 files changed, 156 insertions(+), 12 deletions(-) [+]
line wrap: on
line diff
--- a/autoload/gutentags.vim	Fri Jul 22 19:25:01 2016 -0700
+++ b/autoload/gutentags.vim	Fri Jul 22 19:25:05 2016 -0700
@@ -78,6 +78,29 @@
     let s:known_projects[a:path] = l:result
 endfunction
 
+function! gutentags#validate_cmd(cmd) abort
+    if !empty(a:cmd) && executable(split(a:cmd)[0])
+        return a:cmd
+    endif
+    return ""
+endfunction
+
+function! gutentags#get_project_file_list_cmd(path) abort
+    if type(g:gutentags_file_list_command) == type("")
+        return gutentags#validate_cmd(g:gutentags_file_list_command)
+    elseif type(g:gutentags_file_list_command) == type({})
+        let l:markers = get(g:gutentags_file_list_command, 'markers', [])
+        if type(l:markers) == type({})
+            for [marker, file_list_cmd] in items(l:markers)
+                if getftype(a:path . '/' . marker) != ""
+                    return gutentags#validate_cmd(file_list_cmd)
+                endif
+            endfor
+        endif
+    endif
+    return ""
+endfunction
+
 " Finds the first directory with a project marker by walking up from the given
 " file path.
 function! gutentags#get_project_root(path) abort
--- a/autoload/gutentags/cscope.vim	Fri Jul 22 19:25:01 2016 -0700
+++ b/autoload/gutentags/cscope.vim	Fri Jul 22 19:25:05 2016 -0700
@@ -55,6 +55,11 @@
     let l:cmd .= ' -e ' . g:gutentags_cscope_executable
     let l:cmd .= ' -p ' . a:proj_dir
     let l:cmd .= ' -f ' . a:tags_file
+    let l:file_list_cmd =
+        \ gutentags#get_project_file_list_cmd(l:proj_dir)
+    if !empty(l:file_list_cmd)
+        let l:cmd .= ' -L "' . l:file_list_cmd . '"'
+    endif
     let l:cmd .= ' '
     let l:cmd .= gutentags#get_execute_cmd_suffix()
 
--- a/autoload/gutentags/ctags.vim	Fri Jul 22 19:25:01 2016 -0700
+++ b/autoload/gutentags/ctags.vim	Fri Jul 22 19:25:05 2016 -0700
@@ -79,10 +79,18 @@
                 let l:cur_file_path = fnamemodify(l:cur_file_path, ':.')
             endif
             let l:cmd .= ' -s "' . l:cur_file_path . '"'
+        else
+            let l:file_list_cmd = gutentags#get_project_file_list_cmd(l:actual_proj_dir)
+            if !empty(l:file_list_cmd)
+                let l:cmd .= ' -L ' . '"' . l:file_list_cmd. '"'
+            endif
         endif
-        " Pass the Gutentags recursive options file before the project
-        " options file, so that users can override --recursive.
-        let l:cmd .= ' -o "' . gutentags#get_res_file('ctags_recursive.options') . '"'
+        if empty(get(l:, 'file_list_cmd', ''))
+            " Pass the Gutentags recursive options file before the project
+            " options file, so that users can override --recursive.
+            " Omit --recursive if this project uses a file list command.
+            let l:cmd .= ' -o "' . gutentags#get_res_file('ctags_recursive.options') . '"'
+        endif
         let l:proj_options_file = a:proj_dir . '/' .
                     \g:gutentags_ctags_options_file
         if filereadable(l:proj_options_file)
--- a/doc/gutentags.txt	Fri Jul 22 19:25:01 2016 -0700
+++ b/doc/gutentags.txt	Fri Jul 22 19:25:05 2016 -0700
@@ -417,6 +417,40 @@
                         See |gutentags_ctags_executable_{filetype}| for more
                         information.
 
+                                            *gutentags_file_list_command*
+g:gutentags_file_list_command
+                        Specifies command(s) to use to list files for which
+                        tags should be generated, instead of recursively
+                        examining all files within the project root. When
+                        invoked, file list commands will execute in the
+                        project root directory.
+
+                        This setting is useful in projects using source
+                        control to restrict tag generation to only files
+                        tracked in the repository.
+
+                        This variable may be set in one of two ways. If
+                        set as a |String|, the specified command will be used to
+                        list files for all projects. For example: >
+
+                         let g:gutentags_file_list_command = 'find . -type f'
+<
+                        If set as a |Dictionary|, this variable should be set
+                        as a mapping of project root markers to the desired
+                        file list command for that root marker. (See
+                        |gutentags_project_root| for how Gutentags uses root
+                        markerts to locate the project.) For example: >
+
+                         let g:gutentags_file_list_command = {
+                             \ 'markers': {
+                                 \ '.git': 'git ls-files',
+                                 \ '.hg': 'hg locate',
+                                 \ },
+                             \ }
+<
+                        Note: If a custom ctags executable is specified, it
+                        must support the '-L' command line option in order to
+                        read the list of files to be examined.
 
 =============================================================================
 5. Project Settings                             *gutentags-project-settings*
--- a/plat/unix/update_scopedb.sh	Fri Jul 22 19:25:01 2016 -0700
+++ b/plat/unix/update_scopedb.sh	Fri Jul 22 19:25:05 2016 -0700
@@ -4,8 +4,10 @@
 
 PROG_NAME=$0
 CSCOPE_EXE=cscope
+CSCOPE_ARGS=
 DB_FILE=cscope.out
 PROJECT_ROOT=
+FILE_LIST_CMD=
 
 ShowUsage() {
     echo "Usage:"
@@ -14,11 +16,12 @@
     echo "    -e [exe=cscope]:      The cscope executable to run"
     echo "    -f [file=cscope.out]: The path to the ctags file to update"
     echo "    -p [dir=]:            The path to the project root"
+    echo "    -L [cmd=]:            The file list command to run"
     echo ""
 }
 
 
-while getopts "h?e:f:p:" opt; do
+while getopts "h?e:f:p:L:" opt; do
     case $opt in
         h|\?)
             ShowUsage
@@ -33,6 +36,9 @@
         p)
             PROJECT_ROOT=$OPTARG
             ;;
+        L)
+            FILE_LIST_CMD=$OPTARG
+            ;;
     esac
 done
 
@@ -47,16 +53,30 @@
 echo $$ > "$DB_FILE.lock"
 
 # Remove lock and temp file if script is stopped unexpectedly.
-trap 'rm -f "$DB_FILE.lock" "$DB_FILE.temp"' INT QUIT TERM EXIT
+trap 'rm -f "$DB_FILE.lock" "$DB_FILE.files" "$DB_FILE.temp"' INT QUIT TERM EXIT
 
 PREVIOUS_DIR=$(pwd)
 if [ -d "$PROJECT_ROOT" ]; then
     cd "$PROJECT_ROOT"
 fi
 
+if [ -n "${FILE_LIST_CMD}" ]; then
+    if [ "${PROJECT_ROOT}" = "." ]; then
+        $FILE_LIST_CMD > "${DB_FILE}.files"
+    else
+        # If using a tags cache directory, use absolute paths
+        $FILE_LIST_CMD | while read -r l; do
+            echo "${PROJECT_ROOT%/}/${l}"
+        done > "${DB_FILE}.files"
+    fi
+    CSCOPE_ARGS="${CSCOPE_ARGS} -i ${DB_FILE}.files"
+else
+    CSCOPE_ARGS="${CSCOPE_ARGS} -R"
+fi
+
 echo "Running cscope"
-echo "$CSCOPE_EXE -R -b -k -f \"$DB_FILE.temp\""
-"$CSCOPE_EXE" -R -v -b -k -f "$DB_FILE.temp"
+echo "$CSCOPE_EXE $CSCOPE_ARGS -b -k -f \"$DB_FILE.temp\""
+"$CSCOPE_EXE" $CSCOPE_ARGS -v -b -k -f "$DB_FILE.temp"
 
 if [ -d "$PROJECT_ROOT" ]; then
     cd "$PREVIOUS_DIR"
--- a/plat/unix/update_tags.sh	Fri Jul 22 19:25:01 2016 -0700
+++ b/plat/unix/update_tags.sh	Fri Jul 22 19:25:05 2016 -0700
@@ -7,6 +7,7 @@
 CTAGS_ARGS=
 TAGS_FILE=tags
 PROJECT_ROOT=
+FILE_LIST_CMD=
 UPDATED_SOURCE=
 PAUSE_BEFORE_EXIT=0
 
@@ -18,6 +19,7 @@
     echo "    -e [exe=ctags]: The ctags executable to run"
     echo "    -t [file=tags]: The path to the ctags file to update"
     echo "    -p [dir=]:      The path to the project root"
+    echo "    -L [cmd=]:      The file list command to run"
     echo "    -s [file=]:     The path to the source file that needs updating"
     echo "    -x [pattern=]:  A pattern of files to exclude"
     echo "    -o [options=]:  An options file to read additional options from"
@@ -26,7 +28,7 @@
 }
 
 
-while getopts "h?e:x:t:p:s:o:c" opt; do
+while getopts "h?e:x:t:p:L:s:o:c" opt; do
     case $opt in
         h|\?)
             ShowUsage
@@ -44,6 +46,9 @@
         p)
             PROJECT_ROOT=$OPTARG
             ;;
+        L)
+            FILE_LIST_CMD=$OPTARG
+            ;;
         s)
             UPDATED_SOURCE=$OPTARG
             ;;
@@ -67,7 +72,7 @@
 echo $$ > "$TAGS_FILE.lock"
 
 # Remove lock and temp file if script is stopped unexpectedly.
-trap 'errorcode=$?; rm -f "$TAGS_FILE.lock" "$TAGS_FILE.temp"; exit $errorcode' INT QUIT TERM EXIT
+trap 'errorcode=$?; rm -f "$TAGS_FILE.lock" "$TAGS_FILE.files" "$TAGS_FILE.temp"; exit $errorcode' INT QUIT TERM EXIT
 
 INDEX_WHOLE_PROJECT=1
 if [ -f "$TAGS_FILE" ]; then
@@ -82,6 +87,17 @@
 fi
 
 if [ $INDEX_WHOLE_PROJECT -eq 1 ]; then
+    if [ -n "${FILE_LIST_CMD}" ]; then
+        if [ "${PROJECT_ROOT}" = "." ]; then
+            $FILE_LIST_CMD > "${TAGS_FILE}.files"
+        else
+            # If using a tags cache directory, use absolute paths
+            $FILE_LIST_CMD | while read -r l; do
+                echo "${PROJECT_ROOT%/}/${l}"
+            done > "${TAGS_FILE}.files"
+        fi
+        CTAGS_ARGS="${CTAGS_ARGS} -L ${TAGS_FILE}.files"
+    fi
     echo "Running ctags on whole project"
     echo "$CTAGS_EXE -f \"$TAGS_FILE.temp\" $CTAGS_ARGS \"$PROJECT_ROOT\""
     $CTAGS_EXE -f "$TAGS_FILE.temp" $CTAGS_ARGS "$PROJECT_ROOT"
--- a/plat/win32/update_scopedb.cmd	Fri Jul 22 19:25:01 2016 -0700
+++ b/plat/win32/update_scopedb.cmd	Fri Jul 22 19:25:05 2016 -0700
@@ -6,7 +6,9 @@
 rem ==========================================
 
 set CSCOPE_EXE=cscope
+set CSCOPE_ARGS=
 set DB_FILE=cscope.out
+set FILE_LIST_CMD=
 
 :ParseArgs
 if [%1]==[] goto :DoneParseArgs
@@ -25,6 +27,11 @@
     shift
     goto :LoopParseArgs
 )
+if [%1]==[-L] (
+    set FILE_LIST_CMD=%~2
+    shift
+    goto :LoopParseArgs
+)
 echo Invalid Argument: %1
 goto :Usage
 
@@ -43,13 +50,25 @@
 echo locked > "%DB_FILE%.lock"
 
 echo Running cscope
-"%CSCOPE_EXE%" -R -b -k -f "%DB_FILE%"
+if NOT ["%FILE_LIST_CMD%"]==[""] (
+    if ["%PROJECT_ROOT%"]==["."] (
+        call %FILE_LIST_CMD% > %DB_FILE%.files
+    ) else (
+        rem Potentially useful:
+        rem http://stackoverflow.com/questions/9749071/cmd-iterate-stdin-piped-from-another-command
+        %FILE_LIST_CMD% | for /F "usebackq delims=" %%F in (`findstr "."`) do @echo %PROJECT_ROOT%\%%F > %DB_FILE%.files
+    )
+    set CSCOPE_ARGS=%CSCOPE_ARGS% -i %TAGS_FILE%.files
+) ELSE (
+    set CSCOPE_ARGS=%CSCOPE_ARGS% -R
+)
+"%CSCOPE_EXE%" %CSCOPE_ARGS% -b -k -f "%DB_FILE%"
 if ERRORLEVEL 1 (
     echo ERROR: Cscope executable returned non-zero code.
 )
 
 echo Unlocking db file
-del /F "%DB_FILE%.lock"
+del /F "%DB_FILE%.files" "%DB_FILE%.lock"
 if ERRORLEVEL 1 (
     echo ERROR: Unable to remove file lock.
 )
@@ -70,5 +89,6 @@
 echo    -e [exe=cscope]:     The cscope executable to run
 echo    -f [file=scope.out]: The path to the database file to create
 echo    -p [dir=]:           The path to the project root
+echo    -L [cmd=]:           The file list command to run
 echo.
 
--- a/plat/win32/update_tags.cmd	Fri Jul 22 19:25:01 2016 -0700
+++ b/plat/win32/update_tags.cmd	Fri Jul 22 19:25:05 2016 -0700
@@ -9,6 +9,7 @@
 set CTAGS_ARGS=
 set TAGS_FILE=tags
 set PROJECT_ROOT=
+set FILE_LIST_CMD=
 set UPDATED_SOURCE=
 set PAUSE_BEFORE_EXIT=0
 set LOG_FILE=
@@ -35,6 +36,11 @@
     shift
     goto :LoopParseArgs
 )
+if [%1]==[-L] (
+    set FILE_LIST_CMD=%~2
+    shift
+    goto :LoopParseArgs
+)
 if [%1]==[-s] (
     set UPDATED_SOURCE=%~2
     shift
@@ -85,6 +91,16 @@
 )
 if ["%INDEX_WHOLE_PROJECT%"]==["1"] (
     set CTAGS_ARGS=%CTAGS_ARGS% "%PROJECT_ROOT%"
+    if NOT ["%FILE_LIST_CMD%"]==[""] (
+        if ["%PROJECT_ROOT%"]==["."] (
+            call %FILE_LIST_CMD% > %TAGS_FILE%.files
+        ) else (
+            rem Potentially useful:
+            rem http://stackoverflow.com/questions/9749071/cmd-iterate-stdin-piped-from-another-command
+            %FILE_LIST_CMD% | for /F "usebackq delims=" %%F in (`findstr "."`) do @echo %PROJECT_ROOT%\%%F > %TAGS_FILE%.files
+        )
+        set CTAGS_ARGS=%CTAGS_ARGS% -L %TAGS_FILE%.files
+    )
 )
 
 echo Running ctags >> %LOG_FILE%
@@ -105,7 +121,7 @@
 
 :Unlock
 echo Unlocking tags file... >> %LOG_FILE%
-del /F "%TAGS_FILE%.lock"
+del /F "%TAGS_FILE.files" "%TAGS_FILE%.lock"
 if ERRORLEVEL 1 (
     echo ERROR: Unable to remove file lock. >> %LOG_FILE%
 )
@@ -129,6 +145,7 @@
 echo    -e [exe=ctags]: The ctags executable to run
 echo    -t [file=tags]: The path to the ctags file to update
 echo    -p [dir=]:      The path to the project root
+echo    -L [cmd=]:      The file list command to run
 echo    -s [file=]:     The path to the source file that needs updating
 echo    -l [log=]:      The log file to output to
 echo    -o [options=]:  An options file to read additional options from
--- a/plugin/gutentags.vim	Fri Jul 22 19:25:01 2016 -0700
+++ b/plugin/gutentags.vim	Fri Jul 22 19:25:05 2016 -0700
@@ -44,6 +44,7 @@
 let g:gutentags_generate_on_new = get(g:, 'gutentags_generate_on_new', 1)
 let g:gutentags_generate_on_missing = get(g:, 'gutentags_generate_on_missing', 1)
 let g:gutentags_generate_on_write = get(g:, 'gutentags_generate_on_write', 1)
+let g:gutentags_file_list_command = get(g:, 'gutentags_file_list_command', '')
 
 if !exists('g:gutentags_cache_dir')
     let g:gutentags_cache_dir = ''