vim-patch.sh (33642B)
1 #!/usr/bin/env bash 2 3 set -e 4 set -u 5 # Use privileged mode, which e.g. skips using CDPATH. 6 set -p 7 # https://www.shellcheck.net/wiki/SC2031 8 shopt -s lastpipe 9 10 # Ensure that the user has a bash that supports -A 11 if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then 12 >&2 echo "error: script requires bash 4+ (you have ${BASH_VERSION})." 13 exit 1 14 fi 15 16 readonly NVIM_SOURCE_DIR="${NVIM_SOURCE_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" 17 readonly VIM_SOURCE_DIR_DEFAULT="${NVIM_SOURCE_DIR}/.vim-src" 18 readonly VIM_SOURCE_DIR="${VIM_SOURCE_DIR:-${VIM_SOURCE_DIR_DEFAULT}}" 19 BASENAME="$(basename "${0}")" 20 readonly BASENAME 21 readonly BRANCH_PREFIX="vim-" 22 23 CREATED_FILES=() 24 25 usage() { 26 echo "Port Vim patches to Neovim" 27 echo "https://neovim.io/doc/user/dev_vimpatch.html" 28 echo 29 echo "Usage: ${BASENAME} [-h | -l | -p vim-revision | -r pr-number]" 30 echo 31 echo "Options:" 32 echo " -h Show this message and exit." 33 echo " -l [git-log opts] List missing Vim patches." 34 echo " -L [git-log opts] List missing Vim patches (for scripts)." 35 echo " -m {vim-revision} List previous (older) missing Vim patches." 36 echo " -M List all merged patch-numbers (since current v:version) in ascending order." 37 echo " -n List possible N/A Vim patches." 38 echo " -p {vim-revision} Download and generate a Vim patch. vim-revision" 39 echo " can be a Vim version (8.1.xxx) or a Git hash." 40 echo " -P {vim-revision} Download, generate and apply a Vim patch." 41 echo " -g {vim-revision} Download a Vim patch." 42 echo " -s [pr args] Create a vim-patch pull request." 43 echo " -r {pr-number} Review a vim-patch pull request." 44 echo " -V Clone the Vim source code to \$VIM_SOURCE_DIR." 45 echo 46 echo " \$VIM_SOURCE_DIR controls where Vim sources are found" 47 echo " (default: '${VIM_SOURCE_DIR_DEFAULT}')" 48 echo 49 echo "Examples:" 50 echo 51 echo " - List missing patches for a given file (in the Vim source):" 52 echo " $0 -l -- src/edit.c" 53 } 54 55 msg_ok() { 56 printf '\e[32m✔\e[0m %s\n' "$@" 57 } 58 59 msg_err() { 60 printf '\e[31m✘\e[0m %s\n' "$@" >&2 61 } 62 63 # Checks if a program is in the user's PATH, and is executable. 64 check_executable() { 65 test -x "$(command -v "${1}")" 66 } 67 68 require_executable() { 69 if ! check_executable "${1}"; then 70 >&2 echo "${BASENAME}: '${1}' not found in PATH or not executable." 71 exit 1 72 fi 73 } 74 75 clean_files() { 76 if [[ ${#CREATED_FILES[@]} -eq 0 ]]; then 77 return 78 fi 79 80 echo 81 echo "Created files:" 82 local file 83 for file in "${CREATED_FILES[@]}"; do 84 echo " • ${file}" 85 done 86 87 read -p "Delete these files (Y/n)? " -n 1 -r reply 88 echo 89 if [[ "${reply}" == n ]]; then 90 echo "You can use 'git clean' to remove these files when you're done." 91 else 92 rm -- "${CREATED_FILES[@]}" 93 fi 94 } 95 96 get_vim_sources() { 97 require_executable git 98 99 if [[ ! -d ${VIM_SOURCE_DIR} ]]; then 100 echo "Cloning Vim into: ${VIM_SOURCE_DIR}" 101 git clone https://github.com/vim/vim.git "${VIM_SOURCE_DIR}" 102 cd "${VIM_SOURCE_DIR}" 103 elif [[ "${1-}" == update ]]; then 104 cd "${VIM_SOURCE_DIR}" 105 if ! [ -d ".git" ] \ 106 && ! [ "$(git rev-parse --show-toplevel)" = "${VIM_SOURCE_DIR}" ]; then 107 msg_err "${VIM_SOURCE_DIR} does not appear to be a git repository." 108 echo " Please remove it and try again." 109 exit 1 110 fi 111 echo "Updating Vim sources: ${VIM_SOURCE_DIR}" 112 if git pull --ff; then 113 msg_ok "Updated Vim sources." 114 else 115 msg_err "Could not update Vim sources; ignoring error." 116 fi 117 else 118 cd "${VIM_SOURCE_DIR}" 119 fi 120 } 121 122 commit_message() { 123 if [[ "${vim_message}" == "vim-patch:${vim_version}:"* ]]; then 124 printf '%s\n\n%s\n\n%s' "${vim_message}" "${vim_commit_url}" "${vim_coauthors}" 125 else 126 printf 'vim-patch:%s: %s\n\n%s\n\n%s' "${vim_version:0:7}" "${vim_message}" "${vim_commit_url}" "${vim_coauthors}" 127 fi 128 } 129 130 find_git_remote() { 131 local git_remote 132 if [[ "${1-}" == fork ]]; then 133 git_remote=$(git remote -v | awk '$2 !~ /github.com[:\/]neovim\/neovim/ && $3 == "(fetch)" {print $1; exit}') 134 else 135 git_remote=$(git remote -v | awk '$2 ~ /github.com[:\/]neovim\/neovim/ && $3 == "(fetch)" {print $1; exit}') 136 fi 137 if [[ -z "$git_remote" ]]; then 138 git_remote="origin" 139 fi 140 echo "$git_remote" 141 } 142 143 # Assign variables for a given Vim tag, patch version, or commit. 144 # Might exit in case it cannot be found, after updating Vim sources. 145 assign_commit_details() { 146 local vim_commit_ref 147 if [[ ${1} =~ v?[0-9]\.[0-9]\.[0-9]{3,4} ]]; then 148 # Interpret parameter as version number (tag). 149 if [[ "${1:0:1}" == v ]]; then 150 vim_version="${1:1}" 151 vim_tag="${1}" 152 else 153 vim_version="${1}" 154 vim_tag="v${1}" 155 fi 156 vim_commit_ref="$vim_tag" 157 local munge_commit_line=true 158 else 159 # Interpret parameter as commit hash. 160 vim_version="${1:0:7}" 161 vim_tag= 162 vim_commit_ref="$vim_version" 163 local munge_commit_line=false 164 fi 165 166 local get_vim_commit_cmd="git -C ${VIM_SOURCE_DIR} log -1 --format=%H ${vim_commit_ref} --" 167 vim_commit=$($get_vim_commit_cmd 2>&1) || { 168 # Update Vim sources. 169 get_vim_sources update 170 vim_commit=$($get_vim_commit_cmd 2>&1) || { 171 >&2 msg_err "Couldn't find Vim revision '${vim_commit_ref}': git error: ${vim_commit}." 172 exit 3 173 } 174 } 175 176 vim_commit_url="https://github.com/vim/vim/commit/${vim_commit}" 177 vim_message="$(git -C "${VIM_SOURCE_DIR}" log -1 --pretty='format:%B' "${vim_commit}" \ 178 | sed -Ee 's/([^A-Za-z0-9])(#[0-9]{1,})/\1vim\/vim\2/g')" 179 local vim_coauthor0 180 vim_coauthor0="$(git -C "${VIM_SOURCE_DIR}" log -1 --pretty='format:Co-authored-by: %an <%ae>' "${vim_commit}")" 181 # Extract co-authors from the commit message. 182 vim_coauthors="$(echo "${vim_message}" | (grep -E '^Co-[Aa]uthored-[Bb]y: ' || true) | (grep -Fxv "${vim_coauthor0}" || true))" 183 vim_coauthors="$(echo "${vim_coauthor0}"; echo "${vim_coauthors}")" 184 # Remove Co-authored-by and Signed-off-by lines from the commit message. 185 vim_message="$(echo "${vim_message}" | grep -Ev '^(Co-[Aa]uthored|Signed-[Oo]ff)-[Bb]y: ')" 186 if [[ ${munge_commit_line} == "true" ]]; then 187 # Remove first line of commit message. 188 vim_message="$(echo "${vim_message}" | sed -Ee '1s/^patch /vim-patch:/')" 189 fi 190 patch_file="vim-${vim_version}.patch" 191 } 192 193 # Patch surgery 194 preprocess_patch() { 195 local file="$1" 196 local nvim="nvim -u NONE -n -i NONE --headless" 197 198 # Remove Filelist, README 199 local na_files='Filelist\|README.*' 200 2>/dev/null $nvim --cmd 'set dir=/tmp' +'g@^diff --git [ab]/\<\%('"${na_files}"'\)\>@exe "norm! d/\\v(^diff)|%$\r"' +w +q "$file" 201 202 # Remove *.proto, Make*, INSTALL*, gui_*, beval.*, some if_*, gvim, libvterm, tee, VisVim, xpm, xxd 203 local na_src='auto\|configure.*\|GvimExt\|hardcopy.*\|libvterm\|proto\|tee\|VisVim\|xpm\|xxd\|Make.*\|INSTALL.*\|beval.*\|gui.*\|if_cscop\|if_lua\|if_mzsch\|if_olepp\|if_ole\|if_perl\|if_py\|if_ruby\|if_tcl\|if_xcmdsrv' 204 2>/dev/null $nvim --cmd 'set dir=/tmp' +'g@^diff --git [ab]/src/\S*\<\%(testdir/\)\@<!\%('"${na_src}"'\)\>@exe "norm! d/\\v(^diff)|%$\r"' +w +q "$file" 205 206 # Remove runtime/print/ 207 local na_rt='print\/.*' 208 2>/dev/null $nvim --cmd 'set dir=/tmp' +'g@^diff --git [ab]/runtime/\<\%('"${na_rt}"'\)\>@exe "norm! d/\\v(^diff)|%$\r"' +w +q "$file" 209 210 # Remove unwanted Vim doc files. 211 local na_doc='channel\.txt\|if_cscop\.txt\|netbeans\.txt\|os_\w\+\.txt\|print\.txt\|term\.txt\|testing\.txt\|todo\.txt\|vim9\.txt\|tags\|test_urls\.vim' 212 2>/dev/null $nvim --cmd 'set dir=/tmp' +'g@^diff --git [ab]/runtime/doc/\<\%('"${na_doc}"'\)\>@exe "norm! d/\\v(^diff)|%$\r"' +w +q "$file" 213 214 # Remove "Last change ..." changes in doc files. 215 2>/dev/null $nvim --cmd 'set dir=/tmp' +'%s/^@@.*\n.*For Vim version.*Last change.*\n.*For Vim version.*Last change.*//' +w +q "$file" 216 217 # Remove gui, setup, screen dumps, testdir/Make_*.mak files 218 local na_src_testdir='gui_.*\|Make_amiga\.mak\|Make_dos\.mak\|Make_ming\.mak\|Make_vms\.mms\|dumps/.*\.dump\|setup_gui\.vim' 219 2>/dev/null $nvim --cmd 'set dir=/tmp' +'g@^diff --git [ab]/src/testdir/\<\%('"${na_src_testdir}"'\)\>@exe "norm! d/\\v(^diff)|%$\r"' +w +q "$file" 220 221 # Remove testdir/test_*.vim files 222 local na_src_testdir='balloon.*\|behave\.vim\|channel.*\|crypt\.vim\|cscope\.vim\|gui.*\|hardcopy\.vim\|job_fails\.vim\|json\.vim\|listener\.vim\|mzscheme\.vim\|netbeans.*\|paste\.vim\|popupwin.*\|python2\.vim\|pyx2\.vim\|restricted\.vim\|shortpathname\.vim\|sound\.vim\|tcl\.vim\|terminal.*\|xxd\.vim' 223 2>/dev/null $nvim --cmd 'set dir=/tmp' +'g@^diff --git [ab]/src/testdir/\<test_\%('"${na_src_testdir}"'\)\>@exe "norm! d/\\v(^diff)|%$\r"' +w +q "$file" 224 225 # Remove runtime/*/testdir/ files 226 local na_runtime_testdir='.\+' 227 2>/dev/null $nvim --cmd 'set dir=/tmp' +'g@^diff --git [ab]/runtime/\f\+/testdir/\%('${na_runtime_testdir}'\)\>@exe "norm! d/\\v(^diff)|%$\r"' +w +q "$file" 228 229 # Remove N/A src/*.[ch] files: sound.c, version.c 230 local na_src_c='sound\|version' 231 2>/dev/null $nvim --cmd 'set dir=/tmp' +'g@^diff --git [ab]/src/\<\%('"${na_src_c}"'\)\.[ch]\>@exe "norm! d/\\v(^diff)|%$\r"' +w +q "$file" 232 233 # Remove some *.po files. #5622 234 # Also remove vim.pot which is updated on almost every source change. 235 local na_po='sjiscorr\.c\|ja\.sjis\.po\|ko\.po\|pl\.cp1250\.po\|pl\.po\|ru\.cp1251\.po\|uk\.cp1251\.po\|zh_CN\.cp936\.po\|zh_CN\.po\|zh_TW\.po\|vim\.pot' 236 2>/dev/null $nvim --cmd 'set dir=/tmp' +'g@^diff --git [ab]/src/po/\<\%('${na_po}'\)\>@exe "norm! d/\\v(^diff)|%$\r"' +w +q "$file" 237 238 # Remove vimrc_example.vim 239 local na_vimrcexample='vimrc_example\.vim' 240 2>/dev/null $nvim --cmd 'set dir=/tmp' +'g@^diff --git [ab]/runtime/\<\%('${na_vimrcexample}'\)\>@exe "norm! d/\\v(^diff)|%$\r"' +w +q "$file" 241 242 # Rename src/testdir/ paths to test/old/testdir/ 243 LC_ALL=C sed -Ee 's/( [ab])\/src\/testdir/\1\/test\/old\/testdir/g' \ 244 "$file" > "$file".tmp && mv "$file".tmp "$file" 245 246 # Rename src/ paths to src/nvim/ 247 LC_ALL=C sed -Ee 's/( [ab]\/src)/\1\/nvim/g' \ 248 "$file" > "$file".tmp && mv "$file".tmp "$file" 249 250 # Rename evalfunc.c to eval/funcs.c 251 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/evalfunc\.c/\1\/eval\/funcs.c/g' \ 252 "$file" > "$file".tmp && mv "$file".tmp "$file" 253 254 # Rename list.c to eval/list.c 255 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/list\.c/\1\/eval\/list.c/g' \ 256 "$file" > "$file".tmp && mv "$file".tmp "$file" 257 258 # Rename evalvars.c to eval/vars.c 259 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/evalvars\.c/\1\/eval\/vars.c/g' \ 260 "$file" > "$file".tmp && mv "$file".tmp "$file" 261 262 # Rename userfunc.c to eval/userfunc.c 263 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/userfunc\.c/\1\/eval\/userfunc.c/g' \ 264 "$file" > "$file".tmp && mv "$file".tmp "$file" 265 266 # Rename evalbuffer.c to eval/buffer.c 267 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/evalbuffer\.c/\1\/eval\/buffer.c/g' \ 268 "$file" > "$file".tmp && mv "$file".tmp "$file" 269 270 # Rename evalwindow.c to eval/window.c 271 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/evalwindow\.c/\1\/eval\/window.c/g' \ 272 "$file" > "$file".tmp && mv "$file".tmp "$file" 273 274 # Rename cindent.c to indent_c.c 275 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/cindent\.c/\1\/indent_c.c/g' \ 276 "$file" > "$file".tmp && mv "$file".tmp "$file" 277 278 # Rename map.c to mapping.c 279 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/map\.c/\1\/mapping.c/g' \ 280 "$file" > "$file".tmp && mv "$file".tmp "$file" 281 282 # Rename profiler.c to profile.c 283 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/profiler\.c/\1\/profile.c/g' \ 284 "$file" > "$file".tmp && mv "$file".tmp "$file" 285 286 # Rename regexp_(bt|nfa).c to regexp.c 287 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/regexp_(bt|nfa)\.c/\1\/regexp.c/g' \ 288 "$file" > "$file".tmp && mv "$file".tmp "$file" 289 290 # Rename scriptfile.c to runtime.c 291 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/scriptfile\.c/\1\/runtime.c/g' \ 292 "$file" > "$file".tmp && mv "$file".tmp "$file" 293 294 # Rename session.c to ex_session.c 295 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/session\.c/\1\/ex_session.c/g' \ 296 "$file" > "$file".tmp && mv "$file".tmp "$file" 297 298 # Rename highlight.c to highlight_group.c 299 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/highlight\.c/\1\/highlight_group.c/g' \ 300 "$file" > "$file".tmp && mv "$file".tmp "$file" 301 302 # Rename locale.c to os/lang.c 303 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/locale\.c/\1\/os\/lang.c/g' \ 304 "$file" > "$file".tmp && mv "$file".tmp "$file" 305 306 # Rename keymap.h to keycodes.h 307 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/keymap\.h/\1\/keycodes.h/g' \ 308 "$file" > "$file".tmp && mv "$file".tmp "$file" 309 310 # Rename term.c to keycodes.c 311 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/term\.c/\1\/keycodes.c/g' \ 312 "$file" > "$file".tmp && mv "$file".tmp "$file" 313 314 # Rename option.h to option_vars.h 315 LC_ALL=C sed -Ee 's/( [ab]\/src\/nvim)\/option\.h/\1\/option_vars.h/g' \ 316 "$file" > "$file".tmp && mv "$file".tmp "$file" 317 318 # Rename runtime/doc/eval.txt to runtime/doc/vimeval.txt 319 LC_ALL=C sed -Ee 's/( [ab]\/runtime\/doc)\/eval\.txt/\1\/vimeval.txt/g' \ 320 "$file" > "$file".tmp && mv "$file".tmp "$file" 321 322 # Rename <lang>.txt to l10n-<lang>.txt 323 LC_ALL=C sed -Ee 's/( [ab]\/runtime\/doc)\/(arabic|hebrew|russian|vietnamese)\.txt/\1\/l10n-\2.txt/g' \ 324 "$file" > "$file".tmp && mv "$file".tmp "$file" 325 326 # Rename version*.txt to news.txt 327 LC_ALL=C sed -Ee 's/( [ab]\/runtime\/doc)\/version[0-9]+\.txt/\1\/news.txt/g' \ 328 "$file" > "$file".tmp && mv "$file".tmp "$file" 329 330 # Rename sponsor.txt to intro.txt 331 LC_ALL=C sed -Ee 's/( [ab]\/runtime\/doc)\/sponsor\.txt/\1\/intro.txt/g' \ 332 "$file" > "$file".tmp && mv "$file".tmp "$file" 333 334 # Rename path to check_colors.vim 335 LC_ALL=C sed -Ee 's/( [ab]\/runtime)\/colors\/(tools\/check_colors\.vim)/\1\/\2/g' \ 336 "$file" > "$file".tmp && mv "$file".tmp "$file" 337 } 338 339 uncrustify_patch() { 340 git diff --quiet || { 341 >&2 echo 'Vim source working tree dirty, aborting.' 342 exit 1 343 } 344 345 local patch_path="$NVIM_SOURCE_DIR"/build/vim_patch 346 rm -rf "$patch_path" 347 mkdir -p "$patch_path"/{a,b} 348 349 local commit="$1" 350 for file in $(git diff-tree --name-only --no-commit-id -r --diff-filter=a "$commit"); do 351 git --work-tree="$patch_path"/a checkout --quiet "$commit"~ -- "$file" 352 done 353 for file in $(git diff-tree --name-only --no-commit-id -r --diff-filter=d "$commit"); do 354 git --work-tree="$patch_path"/b checkout --quiet "$commit" -- "$file" 355 done 356 git reset --quiet --hard HEAD 357 358 # If the difference are drastic enough uncrustify may need to be used more 359 # than once. This is obviously a bug that needs to be fixed on uncrustify's 360 # end, but in the meantime this workaround is sufficient. 361 for _ in {1..2}; do 362 "$NVIM_SOURCE_DIR"/build/usr/bin/uncrustify -c "$NVIM_SOURCE_DIR"/src/uncrustify.cfg -q --replace --no-backup "$patch_path"/{a,b}/src/*.[ch] 363 done 364 365 (cd "$patch_path" && (git --no-pager diff --no-index --no-prefix --patch --unified=5 --color=never a/ b/ || true)) 366 } 367 368 get_vimpatch() { 369 get_vim_sources 370 371 assign_commit_details "${1}" 372 373 msg_ok "Found Vim revision '${vim_commit}'." 374 375 local patch_content 376 if check_executable "$NVIM_SOURCE_DIR"/build/usr/bin/uncrustify; then 377 patch_content="$(uncrustify_patch "${vim_commit}")" 378 else 379 patch_content="$(git --no-pager show --unified=5 --color=never -1 --pretty=medium "${vim_commit}")" 380 fi 381 382 cd "${NVIM_SOURCE_DIR}" 383 384 printf "Creating patch...\n" 385 echo "$patch_content" > "${NVIM_SOURCE_DIR}/${patch_file}" 386 387 printf "Pre-processing patch...\n" 388 preprocess_patch "${NVIM_SOURCE_DIR}/${patch_file}" 389 390 msg_ok "Saved patch to '${NVIM_SOURCE_DIR}/${patch_file}'." 391 } 392 393 stage_patch() { 394 get_vimpatch "$1" 395 local try_apply="${2:-}" 396 397 local nvim_remote 398 nvim_remote="$(find_git_remote)" 399 local checked_out_branch 400 checked_out_branch="$(git rev-parse --abbrev-ref HEAD)" 401 402 if [[ "${checked_out_branch}" == ${BRANCH_PREFIX}* ]]; then 403 msg_ok "Current branch '${checked_out_branch}' seems to be a vim-patch" 404 echo " branch; not creating a new branch." 405 else 406 printf '\nFetching "%s/master".\n' "${nvim_remote}" 407 if output="$(git fetch "$nvim_remote" master 2>&1)"; then 408 msg_ok "$output" 409 else 410 msg_err "$output" 411 exit 1 412 fi 413 414 local nvim_branch="${BRANCH_PREFIX}${vim_version}" 415 echo 416 echo "Creating new branch '${nvim_branch}' based on '${nvim_remote}/master'." 417 cd "${NVIM_SOURCE_DIR}" 418 if output="$(git checkout -b "$nvim_branch" "$nvim_remote/master" 2>&1)"; then 419 msg_ok "$output" 420 else 421 msg_err "$output" 422 exit 1 423 fi 424 fi 425 426 printf "\nCreating empty commit with correct commit message.\n" 427 if output="$(commit_message | git commit --allow-empty --file 2>&1 -)"; then 428 msg_ok "$output" 429 else 430 msg_err "$output" 431 exit 1 432 fi 433 434 local ret=0 435 if test -n "$try_apply" ; then 436 if ! check_executable patch; then 437 printf "\n" 438 msg_err "'patch' command not found\n" 439 else 440 printf "\nApplying patch...\n" 441 patch -p1 --fuzz=1 --no-backup-if-mismatch < "${patch_file}" || ret=$? 442 fi 443 printf "\nInstructions:\n Proceed to port the patch.\n" 444 else 445 printf '\nInstructions:\n Proceed to port the patch.\n Try the "patch" command (or use "%s -P ..." next time):\n patch -p1 < %s\n' "${BASENAME}" "${patch_file}" 446 fi 447 448 printf ' 449 Stage your changes ("git add ..."), then use "git commit --amend" to commit. 450 451 To port more patches (if any) related to %s, 452 run "%s" again. 453 * Do this only for _related_ patches (otherwise it increases the 454 size of the pull request, making it harder to review) 455 456 When you are done, try "%s -s" to create the pull request, 457 or "%s -s --draft" to create a draft pull request. 458 459 See the wiki for more information: 460 * https://neovim.io/doc/user/dev_vimpatch.html 461 ' "${vim_version}" "${BASENAME}" "${BASENAME}" "${BASENAME}" 462 return $ret 463 } 464 465 gh_pr() { 466 local pr_title 467 local pr_body 468 pr_title="$1" 469 pr_body="$2" 470 shift 2 471 gh pr create --title "${pr_title}" --body "${pr_body}" "$@" 472 } 473 474 git_hub_pr() { 475 local pr_message 476 pr_message="$(printf '%s\n\n%s\n' "$1" "$2")" 477 shift 2 478 git hub pull new -m "${pr_message}" "$@" 479 } 480 481 submit_pr() { 482 require_executable git 483 local push_first 484 push_first=1 485 local submit_fn 486 if check_executable gh; then 487 submit_fn="gh_pr" 488 elif check_executable git-hub; then 489 push_first=0 490 submit_fn="git_hub_pr" 491 else 492 >&2 echo "${BASENAME}: 'gh' or 'git-hub' not found in PATH or not executable." 493 >&2 echo " Get it here: https://cli.github.com/" 494 exit 1 495 fi 496 497 cd "${NVIM_SOURCE_DIR}" 498 local checked_out_branch 499 checked_out_branch="$(git rev-parse --abbrev-ref HEAD)" 500 if [[ "${checked_out_branch}" != ${BRANCH_PREFIX}* ]]; then 501 msg_err "Current branch '${checked_out_branch}' doesn't seem to be a vim-patch branch." 502 exit 1 503 fi 504 505 local nvim_remote 506 nvim_remote="$(find_git_remote)" 507 local pr_body 508 pr_body="$(git log --grep=vim-patch --reverse --format='#### %s%n%n%b%n' "${nvim_remote}"/master..HEAD)" 509 local patches 510 # Extract just the "vim-patch:X.Y.ZZZZ" or "vim-patch:sha" portion of each log 511 patches=("$(git log --grep=vim-patch --reverse --format='%s' "${nvim_remote}"/master..HEAD | sed 's/: .*//')") 512 # shellcheck disable=SC2206 513 patches=(${patches[@]//vim-patch:}) # Remove 'vim-patch:' prefix for each item in array. 514 local pr_title="${patches[*]}" # Create space-separated string from array. 515 pr_title="${pr_title// /,}" # Replace spaces with commas. 516 pr_title="$(printf 'vim-patch:%s' "${pr_title#,}")" 517 518 if [[ $push_first -ne 0 ]]; then 519 local push_remote 520 push_remote="$(git config --get branch."${checked_out_branch}".pushRemote || true)" 521 if [[ -z "$push_remote" ]]; then 522 push_remote="$(git config --get remote.pushDefault || true)" 523 if [[ -z "$push_remote" ]]; then 524 push_remote="$(git config --get branch."${checked_out_branch}".remote || true)" 525 if [[ -z "$push_remote" ]] || [[ "$push_remote" == "$nvim_remote" ]]; then 526 push_remote="$(find_git_remote fork)" 527 fi 528 fi 529 fi 530 echo "Pushing to '${push_remote}/${checked_out_branch}'." 531 if output="$(git push "$push_remote" "$checked_out_branch" 2>&1)"; then 532 msg_ok "$output" 533 else 534 msg_err "$output" 535 exit 1 536 fi 537 538 echo 539 fi 540 541 echo "Creating pull request." 542 if output="$($submit_fn "$pr_title" "$pr_body" "$@" 2>&1)"; then 543 msg_ok "$output" 544 else 545 msg_err "$output" 546 exit 1 547 fi 548 549 echo 550 echo "Cleaning up files." 551 local patch_file 552 for patch_file in "${patches[@]}"; do 553 patch_file="vim-${patch_file}.patch" 554 if [[ ! -f "${NVIM_SOURCE_DIR}/${patch_file}" ]]; then 555 continue 556 fi 557 rm -- "${NVIM_SOURCE_DIR}/${patch_file}" 558 msg_ok "Removed '${NVIM_SOURCE_DIR}/${patch_file}'." 559 done 560 } 561 562 # Gets all Vim commits since the "start" commit. 563 list_vim_commits() { ( 564 cd "${VIM_SOURCE_DIR}" && git log --reverse v8.1.0000..HEAD "$@" 565 ) } 566 567 # Prints all (sorted) "vim-patch:xxx" tokens found in the Nvim git log. 568 list_vimpatch_tokens() { 569 # Use sed…{7,7} to normalize (internal) Git hashes (for tokens caches). 570 git -C "${NVIM_SOURCE_DIR}" log -E --grep='vim-patch:[^ ,{]{7,}' \ 571 | grep -oE 'vim-patch:[^ ,{:]{7,}' \ 572 | sort \ 573 | uniq \ 574 | sed -nEe 's/^(vim-patch:([0-9]+\.[^ ]+|[0-9a-z]{7,7})).*/\1/p' 575 } 576 577 # Prints all merged patches (since current v:version) in ascending order. 578 # 579 # Search "vim-patch:xxx" tokens in the Nvim git log. 580 # Ignore tokens older than current v:version. 581 # Left-pad the patch number of "vim-patch:xxx" for stable sort + dedupe. 582 # Filter reverted Vim tokens. 583 list_vimpatch_numbers() { 584 local patch_pat='(8\.[12]|9\.[0-9])\.[0-9]{1,4}' 585 diff "${NVIM_SOURCE_DIR}/scripts/vimpatch_token_reverts.txt" <( 586 git -C "${NVIM_SOURCE_DIR}" log --format="%s%n%b" -E --grep="^[* ]*vim-patch:${patch_pat}" | 587 grep -oE "^[* ]*vim-patch:${patch_pat}" | 588 sed -nEe 's/^[* ]*vim-patch:('"${patch_pat}"').*$/\1/p' | 589 awk '{split($0, a, "."); printf "%d.%d.%04d\n", a[1], a[2], a[3]}' | 590 sort | 591 uniq ) | 592 grep -e '^> ' | 593 sed -e 's/^> //' 594 } 595 596 declare -A tokens 597 declare -A vim_commit_tags 598 599 _set_tokens_and_tags() { 600 set +u # Avoid "unbound variable" with bash < 4.4 below. 601 if [[ -n "${tokens[*]}" ]]; then 602 return 603 fi 604 set -u 605 606 # Find all "vim-patch:xxx" tokens in the Nvim git log. 607 for token in $(list_vimpatch_tokens); do 608 tokens[$token]=1 609 done 610 611 # Create an associative array mapping Vim commits to tags. 612 eval "vim_commit_tags=( 613 $(git -C "${VIM_SOURCE_DIR}" show-ref --tags --dereference \ 614 | sed -nEe 's/^([0-9a-f]+) refs\/tags\/(v[0-9.]+)(\^\{\})?$/["\1"]="\2"/p') 615 )" 616 # Exit in case of errors from the above eval (empty vim_commit_tags). 617 if ! (( "${#vim_commit_tags[@]}" )); then 618 msg_err "Could not get Vim commits/tags." 619 exit 1 620 fi 621 } 622 623 # Prints a newline-delimited list of Vim commits, for use by scripts. 624 # "$1": use extended format? (with subject) 625 # "$@" is passed to list_vim_commits, as extra arguments to git-log. 626 list_missing_vimpatches() { 627 local -a missing_vim_patches=() 628 _set_missing_vimpatches "$@" 629 set +u # Avoid "unbound variable" with bash < 4.4 below. 630 for line in "${missing_vim_patches[@]}"; do 631 printf '%s\n' "$line" 632 done 633 set -u 634 } 635 636 # Sets / appends to missing_vim_patches (useful to avoid a subshell when 637 # used multiple times to cache tokens/vim_commit_tags). 638 # "$1": use extended format? (with subject) 639 # "$@": extra arguments to git-log. 640 _set_missing_vimpatches() { 641 local token vim_commit vim_tag patch_number 642 declare -a git_log_args 643 644 local extended_format=$1; shift 645 if [[ "$extended_format" == 1 ]]; then 646 git_log_args=("--format=%H %s") 647 else 648 git_log_args=("--format=%H") 649 fi 650 651 # Massage arguments for git-log. 652 declare -A git_log_replacements=( 653 [^\(.*/\)?src/nvim/\(.*\)]="\${BASH_REMATCH[1]}src/\${BASH_REMATCH[2]}" 654 [^\(.*/\)?test/old/\(.*\)]="\${BASH_REMATCH[1]}src/\${BASH_REMATCH[2]}" 655 [^\(.*/\)?\.vim-src/\(.*\)]="\${BASH_REMATCH[2]}" 656 ) 657 local i j 658 for i in "$@"; do 659 for j in "${!git_log_replacements[@]}"; do 660 if [[ "$i" =~ $j ]]; then 661 eval "git_log_args+=(${git_log_replacements[$j]})" 662 continue 2 663 fi 664 done 665 git_log_args+=("$i") 666 done 667 668 _set_tokens_and_tags 669 670 # Get missing Vim commits 671 set +u # Avoid "unbound variable" with bash < 4.4 below. 672 local vim_commit info 673 while IFS=' ' read -r line; do 674 # Check for vim-patch:<commit_hash> (usually runtime updates). 675 token="vim-patch:${line:0:7}" 676 if [[ "${tokens[$token]-}" ]]; then 677 continue 678 fi 679 680 # Get commit hash, and optional info from line. This is used in 681 # extended mode, and when using e.g. '--format' manually. 682 vim_commit=${line%% *} 683 if [[ "$vim_commit" == "$line" ]]; then 684 info= 685 else 686 info=${line#* } 687 if [[ -n $info ]]; then 688 # Remove any "patch 8.1.0902: " prefixes, and prefix with ": ". 689 info=": ${info#patch*: }" 690 fi 691 fi 692 693 vim_tag="${vim_commit_tags[$vim_commit]-}" 694 if [[ -n "$vim_tag" ]]; then 695 # Check for vim-patch:<tag> (not commit hash). 696 patch_number="vim-patch:${vim_tag:1}" # "v7.4.0001" => "7.4.0001" 697 if [[ "${tokens[$patch_number]-}" ]]; then 698 continue 699 fi 700 missing_vim_patches+=("$vim_tag$info") 701 else 702 missing_vim_patches+=("$vim_commit$info") 703 fi 704 done < <(list_vim_commits "${git_log_args[@]}") 705 set -u 706 } 707 708 # Prints a human-formatted list of Vim commits, with instructional messages. 709 # Passes "$@" onto list_missing_vimpatches (args for git-log). 710 show_vimpatches() { 711 get_vim_sources update 712 printf "Vim patches missing from Neovim:\n" 713 714 local -A runtime_commits 715 for commit in $(git -C "${VIM_SOURCE_DIR}" log --format="%H %D" -- runtime | sed -Ee 's/,\? tag: / /g'); do 716 runtime_commits[$commit]=1 717 done 718 719 list_missing_vimpatches 1 "$@" | while read -r vim_commit; do 720 if [[ "${runtime_commits[$vim_commit]-}" ]]; then 721 printf ' • %s (+runtime)\n' "${vim_commit}" 722 else 723 printf ' • %s\n' "${vim_commit}" 724 fi 725 done 726 727 cat << EOF 728 729 Instructions: 730 To port one of the above patches to Neovim, execute this script with the patch revision as argument and follow the instructions, e.g. 731 '${BASENAME} -p v8.1.1234', or '${BASENAME} -P v8.1.1234' 732 733 NOTE: Please port the _oldest_ patch if you possibly can. 734 You can use '${BASENAME} -l path/to/file' to see what patches are missing for a file. 735 EOF 736 } 737 738 list_missing_previous_vimpatches_for_patch() { 739 local for_vim_patch="${1}" 740 local vim_commit vim_tag 741 assign_commit_details "${for_vim_patch}" 742 743 local file 744 local -a missing_list 745 local -a fnames 746 while IFS= read -r line ; do 747 fnames+=("$line") 748 done < <(git -C "${VIM_SOURCE_DIR}" diff-tree --no-commit-id --name-only -r "${vim_commit}" -- . ':!src/version.c') 749 local i=0 750 local n=${#fnames[@]} 751 printf '=== getting missing patches for %d files ===\n' "$n" 752 if [[ -z "${vim_tag}" ]]; then 753 printf 'NOTE: "%s" is not a Vim tag - listing all oldest missing patches\n' "${for_vim_patch}" >&2 754 fi 755 for fname in "${fnames[@]}"; do 756 i=$(( i+1 )) 757 printf '[%.*d/%d] %s: ' "${#n}" "$i" "$n" "$fname" 758 759 local -a missing_vim_patches=() 760 _set_missing_vimpatches 1 -- "${fname}" 761 762 set +u # Avoid "unbound variable" with bash < 4.4 below. 763 for missing_vim_commit_info in "${missing_vim_patches[@]}"; do 764 if [[ -z "${missing_vim_commit_info}" ]]; then 765 printf -- "-\r" 766 else 767 printf -- "-\r" 768 local missing_vim_commit="${missing_vim_commit_info%%:*}" 769 if [[ -z "${vim_tag}" ]] || [[ "${missing_vim_commit}" < "${vim_tag}" ]]; then 770 printf -- "%s\n" "$missing_vim_commit_info" 771 missing_list+=("$missing_vim_commit_info") 772 else 773 printf -- "-\r" 774 fi 775 fi 776 done 777 set -u 778 done 779 780 set +u # Avoid "unbound variable" with bash < 4.4 below. 781 if [[ -z "${missing_list[*]}" ]]; then 782 msg_ok 'no missing previous Vim patches' 783 set -u 784 return 0 785 fi 786 set -u 787 788 local -a missing_unique 789 local stat 790 while IFS= read -r line; do 791 local commit="${line%%:*}" 792 stat="$(git -C "${VIM_SOURCE_DIR}" show --format= --shortstat "${commit}")" 793 missing_unique+=("$(printf '%s\n %s' "$line" "$stat")") 794 done < <(printf '%s\n' "${missing_list[@]}" | sort -u) 795 796 msg_err "$(printf '%d missing previous Vim patches:' ${#missing_unique[@]})" 797 printf ' - %s\n' "${missing_unique[@]}" 798 return 1 799 } 800 801 review_commit() { 802 local nvim_commit_url="${1}" 803 local nvim_patch_url="${nvim_commit_url}.patch" 804 805 local git_patch_prefix='Subject: \[PATCH\] ' 806 local nvim_patch 807 nvim_patch="$(curl -Ssf "${nvim_patch_url}")" 808 local vim_version 809 vim_version="$(head -n 4 <<< "${nvim_patch}" | sed -nEe 's/'"${git_patch_prefix}"'vim-patch:([a-z0-9.]*)(:.*){0,1}$/\1/p')" 810 811 echo 812 if [[ -n "${vim_version}" ]]; then 813 msg_ok "Detected Vim patch '${vim_version}'." 814 else 815 msg_err "Could not detect the Vim patch number." 816 echo " This script assumes that the PR contains only commits" 817 echo " with 'vim-patch:XXX' in their title." 818 echo 819 printf -- '%s\n\n' "$(head -n 4 <<< "${nvim_patch}")" 820 local reply 821 read -p "Continue reviewing (y/N)? " -n 1 -r reply 822 if [[ "${reply}" == y ]]; then 823 echo 824 return 825 fi 826 exit 1 827 fi 828 829 assign_commit_details "${vim_version}" 830 831 echo 832 echo "Creating files." 833 echo "${nvim_patch}" > "${NVIM_SOURCE_DIR}/n${patch_file}" 834 msg_ok "Saved pull request diff to '${NVIM_SOURCE_DIR}/n${patch_file}'." 835 CREATED_FILES+=("${NVIM_SOURCE_DIR}/n${patch_file}") 836 837 local nvim="nvim -u NONE -n -i NONE --headless" 838 2>/dev/null $nvim --cmd 'set dir=/tmp' +'1,/^$/g/^ /-1join' +w +q "${NVIM_SOURCE_DIR}/n${patch_file}" 839 840 local expected_commit_message 841 expected_commit_message="$(commit_message)" 842 local message_length 843 message_length="$(wc -l <<< "${expected_commit_message}")" 844 local commit_message 845 commit_message="$(tail -n +4 "${NVIM_SOURCE_DIR}/n${patch_file}" | head -n "${message_length}")" 846 if [[ "${commit_message#"$git_patch_prefix"}" == "${expected_commit_message}" ]]; then 847 msg_ok "Found expected commit message." 848 else 849 msg_err "Wrong commit message." 850 echo " Expected:" 851 echo "${expected_commit_message}" 852 echo " Actual:" 853 echo "${commit_message#"$git_patch_prefix"}" 854 fi 855 856 get_vimpatch "${vim_version}" 857 CREATED_FILES+=("${NVIM_SOURCE_DIR}/${patch_file}") 858 859 echo 860 echo "Launching nvim." 861 nvim -c "cd ${NVIM_SOURCE_DIR}" \ 862 -O "${NVIM_SOURCE_DIR}/${patch_file}" "${NVIM_SOURCE_DIR}/n${patch_file}" 863 } 864 865 review_pr() { 866 require_executable curl 867 require_executable nvim 868 require_executable jq 869 870 get_vim_sources 871 872 local pr="${1}" 873 echo 874 echo "Downloading data for pull request #${pr}." 875 876 local -a pr_commit_urls 877 while IFS= read -r pr_commit_url; do 878 pr_commit_urls+=("$pr_commit_url") 879 done < <(curl -Ssf "https://api.github.com/repos/neovim/neovim/pulls/${pr}/commits" \ 880 | jq -r '.[].html_url') 881 882 echo "Found ${#pr_commit_urls[@]} commit(s)." 883 884 local pr_commit_url 885 local reply 886 for pr_commit_url in "${pr_commit_urls[@]}"; do 887 review_commit "${pr_commit_url}" 888 if [[ "${pr_commit_url}" != "${pr_commit_urls[-1]}" ]]; then 889 read -p "Continue with next commit (Y/n)? " -n 1 -r reply 890 echo 891 if [[ "${reply}" == n ]]; then 892 break 893 fi 894 fi 895 done 896 897 clean_files 898 } 899 900 is_na_patch() { 901 local patch=$1 902 local NA_REGEXP="$NVIM_SOURCE_DIR/scripts/vim_na_regexp.txt" 903 local NA_FILELIST="$NVIM_SOURCE_DIR/scripts/vim_na_files.txt" 904 905 local FILES_REMAINING 906 FILES_REMAINING="$(diff <(git -C "${VIM_SOURCE_DIR}" diff-tree --no-commit-id --name-only -r "$patch" | grep -v -f "$NA_REGEXP") "$NA_FILELIST" | 907 grep '^<')" || true 908 test -z "$FILES_REMAINING" && return 0 909 if test "$FILES_REMAINING" == "$(printf "< src/version.c\n")"; then 910 local VERSION_LNUM 911 VERSION_LNUM=$(git -C "${VIM_SOURCE_DIR}" diff-tree --no-commit-id --numstat -r "$patch" -- src/version.c | grep -c '^2\s\+0') 912 test "$VERSION_LNUM" -ne 1 && return 1 913 local VERSION_VNUM 914 VERSION_VNUM="$(git -C "${VIM_SOURCE_DIR}" diff-tree --no-commit-id -U1 -r "$patch" -- src/version.c | 915 grep -Pzc '[ +]\/\*\*\/\n\+\s+[0-9]+,\n[ +]\/\*\*\/\n')" || true 916 test "$VERSION_VNUM" -eq 1 && return 0 917 fi 918 return 1 919 } 920 921 list_na_patches() { 922 list_missing_vimpatches 0 | while read -r patch; do 923 if is_na_patch "$patch"; then 924 GIT_MSG="$(git -C "${VIM_SOURCE_DIR}" log -1 --oneline "$patch")" 925 if (echo "$patch" | grep -q '^v[0-9]\.[0-9]\.[0-9]') && (echo "${GIT_MSG}" | grep -q ' patch [0-9]\.'); then 926 # shellcheck disable=SC2001 927 echo "vim-patch:$(echo "${GIT_MSG}" | sed 's/^[0-9a-zA-Z]\+ patch //')" 928 else 929 echo "vim-patch:${GIT_MSG}" 930 fi 931 fi 932 done 933 } 934 935 while getopts "hlLmnMVp:P:g:r:s" opt; do 936 case ${opt} in 937 h) 938 usage 939 exit 0 940 ;; 941 l) 942 shift # remove opt 943 show_vimpatches "$@" 944 exit 0 945 ;; 946 L) 947 shift # remove opt 948 list_missing_vimpatches 0 "$@" 949 exit 0 950 ;; 951 M) 952 list_vimpatch_numbers 953 exit 0 954 ;; 955 m) 956 shift # remove opt 957 list_missing_previous_vimpatches_for_patch "$@" 958 exit 0 959 ;; 960 n) 961 list_na_patches 962 exit 0 963 ;; 964 p) 965 stage_patch "${OPTARG}" 966 exit 967 ;; 968 P) 969 stage_patch "${OPTARG}" TRY_APPLY 970 exit 0 971 ;; 972 g) 973 get_vimpatch "${OPTARG}" 974 exit 0 975 ;; 976 r) 977 review_pr "${OPTARG}" 978 exit 0 979 ;; 980 s) 981 shift # remove opt 982 submit_pr "$@" 983 exit 0 984 ;; 985 V) 986 get_vim_sources update 987 exit 0 988 ;; 989 *) 990 exit 1 991 ;; 992 esac 993 done 994 995 usage 996 997 # vim: et sw=2