mirror of
https://github.com/RGBCube/nu_scripts
synced 2025-08-01 06:37:46 +00:00
Improve completer speed for git checkout
(#1054)
Improvement: - Faster to give result (calling `git` less, telling `git` to return simpler-to-parse output). - Context aware. For example: + After `git checkout branch-name`, the rest arguments must be file paths (`git checkout` only accepts one "branch"). + After `--` are file paths. `git checkout a-branch -- a-file` The current completer for `git checkout` is slow because: - Running `git` too many times. - Parse many results and end up not use.  
This commit is contained in:
parent
d8a3d123ca
commit
1e7fdd4427
1 changed files with 173 additions and 39 deletions
|
@ -1,3 +1,131 @@
|
||||||
|
module git-completion-utils {
|
||||||
|
export const GIT_SKIPABLE_FLAGS = ['-v', '--version', '-h', '--help', '-p', '--paginate', '-P', '--no-pager', '--no-replace-objects', '--bare']
|
||||||
|
|
||||||
|
# Helper function to append token if non-empty
|
||||||
|
def append-non-empty [token: string]: list<string> -> list<string> {
|
||||||
|
if ($token | is-empty) { $in } else { $in | append $token }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Split a string to list of args, taking quotes into account.
|
||||||
|
# Code is copied and modified from https://github.com/nushell/nushell/issues/14582#issuecomment-2542596272
|
||||||
|
export def args-split []: string -> list<string> {
|
||||||
|
# Define our states
|
||||||
|
const STATE_NORMAL = 0
|
||||||
|
const STATE_IN_SINGLE_QUOTE = 1
|
||||||
|
const STATE_IN_DOUBLE_QUOTE = 2
|
||||||
|
const STATE_ESCAPE = 3
|
||||||
|
const WHITESPACES = [" " "\t" "\n" "\r"]
|
||||||
|
|
||||||
|
# Initialize variables
|
||||||
|
mut state = $STATE_NORMAL
|
||||||
|
mut current_token = ""
|
||||||
|
mut result: list<string> = []
|
||||||
|
mut prev_state = $STATE_NORMAL
|
||||||
|
|
||||||
|
# Process each character
|
||||||
|
for char in ($in | split chars) {
|
||||||
|
if $state == $STATE_ESCAPE {
|
||||||
|
# Handle escaped character
|
||||||
|
$current_token = $current_token + $char
|
||||||
|
$state = $prev_state
|
||||||
|
} else if $char == '\' {
|
||||||
|
# Enter escape state
|
||||||
|
$prev_state = $state
|
||||||
|
$state = $STATE_ESCAPE
|
||||||
|
} else if $state == $STATE_NORMAL {
|
||||||
|
if $char == "'" {
|
||||||
|
$state = $STATE_IN_SINGLE_QUOTE
|
||||||
|
} else if $char == '"' {
|
||||||
|
$state = $STATE_IN_DOUBLE_QUOTE
|
||||||
|
} else if ($char in $WHITESPACES) {
|
||||||
|
# Whitespace in normal state means token boundary
|
||||||
|
$result = $result | append-non-empty $current_token
|
||||||
|
$current_token = ""
|
||||||
|
} else {
|
||||||
|
$current_token = $current_token + $char
|
||||||
|
}
|
||||||
|
} else if $state == $STATE_IN_SINGLE_QUOTE {
|
||||||
|
if $char == "'" {
|
||||||
|
$state = $STATE_NORMAL
|
||||||
|
} else {
|
||||||
|
$current_token = $current_token + $char
|
||||||
|
}
|
||||||
|
} else if $state == $STATE_IN_DOUBLE_QUOTE {
|
||||||
|
if $char == '"' {
|
||||||
|
$state = $STATE_NORMAL
|
||||||
|
} else {
|
||||||
|
$current_token = $current_token + $char
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Handle the last token
|
||||||
|
$result = $result | append-non-empty $current_token
|
||||||
|
# Return the result
|
||||||
|
$result
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get changed files which can be restored by `git checkout --`
|
||||||
|
export def get-changed-files []: nothing -> list<string> {
|
||||||
|
^git status -uno --porcelain=2 | lines
|
||||||
|
| where $it =~ '^1 [.MD]{2}'
|
||||||
|
| each { split row ' ' -n 9 | last }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get files which can be retrieved from a branch/commit by `git checkout <tree-ish>`
|
||||||
|
export def get-checkoutable-files []: nothing -> list<string> {
|
||||||
|
# Relevant statuses are .M", "MM", "MD", ".D", "UU"
|
||||||
|
^git status -uno --porcelain=2 | lines
|
||||||
|
| where $it =~ '^1 ([.MD]{2}|UU)'
|
||||||
|
| each { split row ' ' -n 9 | last }
|
||||||
|
}
|
||||||
|
|
||||||
|
export def get-all-git-branches []: nothing -> list<string> {
|
||||||
|
^git branch -a --format '%(refname:lstrip=2)%09%(upstream:lstrip=2)' | lines | str trim | filter { not ($in ends-with 'HEAD' ) }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract remote branches which do not have local counterpart
|
||||||
|
export def extract-remote-branches-nonlocal-short [current: string]: list<string> -> list<string> {
|
||||||
|
# Input is a list of lines, like:
|
||||||
|
# ╭────┬────────────────────────────────────────────────╮
|
||||||
|
# │ 0 │ feature/awesome-1 origin/feature/awesome-1 │
|
||||||
|
# │ 1 │ fix/bug-1 origin/fix/bug-1 │
|
||||||
|
# │ 2 │ main origin/main │
|
||||||
|
# │ 3 │ origin/HEAD │
|
||||||
|
# │ 4 │ origin/feature/awesome-1 │
|
||||||
|
# │ 5 │ origin/fix/bug-1 │
|
||||||
|
# │ 6 │ origin/feature/awesome-2 │
|
||||||
|
# │ 7 │ origin/main │
|
||||||
|
# │ 8 │ upstream/main │
|
||||||
|
# │ 9 │ upstream/awesome-3 │
|
||||||
|
# ╰────┴────────────────────────────────────────────────╯
|
||||||
|
# and we pick ['feature/awesome-2', 'awesome-3']
|
||||||
|
let lines = $in
|
||||||
|
let long_current = if ($current | is-empty) { '' } else { $'origin/($current)' }
|
||||||
|
let branches = $lines | filter { ($in != $long_current) and not ($in starts-with $"($current)\t") }
|
||||||
|
let tracked_remotes = $branches | find --no-highlight "\t" | each { split row "\t" -n 2 | get 1 }
|
||||||
|
let floating_remotes = $lines | filter { "\t" not-in $in and $in not-in $tracked_remotes }
|
||||||
|
$floating_remotes | each {
|
||||||
|
let v = $in | split row -n 2 '/' | get 1
|
||||||
|
if $v != $current { [$v] } else []
|
||||||
|
} | flatten
|
||||||
|
}
|
||||||
|
|
||||||
|
export def extract-mergable-sources [current: string]: list<string> -> list<record<value: string, description: string>> {
|
||||||
|
let lines = $in
|
||||||
|
let long_current = if ($current | is-empty) { '' } else { $'origin/($current)' }
|
||||||
|
let branches = $lines | filter { ($in != $long_current) and not ($in starts-with $"($current)\t") }
|
||||||
|
let git_table: list<record<n: string, u: string>> = $branches | each {|v| if "\t" in $v { $v | split row "\t" -n 2 | {n: $in.0, u: $in.1 } } else {n: $v, u: null } }
|
||||||
|
let siblings = $git_table | where u == null and n starts-with 'origin/' | get n | str substring 7..
|
||||||
|
let remote_branches = $git_table | filter {|r| $r.u == null and not ($r.n starts-with 'origin/') } | get n
|
||||||
|
[...($siblings | wrap value | insert description Local), ...($remote_branches | wrap value | insert description Remote)]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get local branches, remote branches which can be passed to `git merge`
|
||||||
|
export def get-mergable-sources []: nothing -> list<record<value: string, description: string>> {
|
||||||
|
let current = (^git branch --show-current) # Can be empty if in detached HEAD
|
||||||
|
(get-all-git-branches | extract-mergable-sources $current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def "nu-complete git available upstream" [] {
|
def "nu-complete git available upstream" [] {
|
||||||
^git branch --no-color -a | lines | each { |line| $line | str replace '* ' "" | str trim }
|
^git branch --no-color -a | lines | each { |line| $line | str replace '* ' "" | str trim }
|
||||||
|
@ -32,56 +160,53 @@ def "nu-complete git remote branches with prefix" [] {
|
||||||
^git branch --no-color -r | lines | parse -r '^\*?(\s*|\s*\S* -> )(?P<branch>\S*$)' | get branch | uniq
|
^git branch --no-color -r | lines | parse -r '^\*?(\s*|\s*\S* -> )(?P<branch>\S*$)' | get branch | uniq
|
||||||
}
|
}
|
||||||
|
|
||||||
# Yield remote branches *without* prefix which do not have a local counterpart.
|
|
||||||
# E.g. `upstream/feature-a` as `feature-a` to checkout and track in one command
|
|
||||||
# with `git checkout` or `git switch`.
|
|
||||||
def "nu-complete git remote branches nonlocal without prefix" [] {
|
|
||||||
# Get regex to strip remotes prefixes. It will look like `(origin|upstream)`
|
|
||||||
# for the two remotes `origin` and `upstream`.
|
|
||||||
let remotes_regex = (["(", ((nu-complete git remotes | each {|r| [$r, '/'] | str join}) | str join "|"), ")"] | str join)
|
|
||||||
let local_branches = (nu-complete git local branches)
|
|
||||||
^git branch --no-color -r | lines | parse -r (['^[\* ]+', $remotes_regex, '?(?P<branch>\S+)'] | flatten | str join) | get branch | uniq | where {|branch| $branch != "HEAD"} | where {|branch| $branch not-in $local_branches }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Yield local and remote branch names which can be passed to `git merge`
|
# Yield local and remote branch names which can be passed to `git merge`
|
||||||
def "nu-complete git mergable sources" [] {
|
def "nu-complete git mergable sources" [] {
|
||||||
let current = (^git branch --show-current)
|
use git-completion-utils *
|
||||||
let long_current = $'origin/($current)'
|
(get-mergable-sources)
|
||||||
let git_table = ^git branch -a --format '%(refname:lstrip=2)%09%(upstream:lstrip=2)' | lines | str trim | where { ($in != $long_current) and not ($in starts-with $"($current)\t") and not ($in ends-with 'HEAD') } | each {|v| if "\t" in $v { $v | split row "\t" -n 2 | {'n': $in.0, 'u': $in.1 } } else {'n': $v, 'u': null } }
|
|
||||||
let siblings = $git_table | where u == null and n starts-with 'origin/' | get n | str substring 7..
|
|
||||||
let remote_branches = $git_table | filter {|r| $r.u == null and not ($r.n starts-with 'origin/') } | get n
|
|
||||||
[...($siblings | wrap value | insert description Local), ...($remote_branches | wrap value | insert description Remote)]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def "nu-complete git switch" [] {
|
def "nu-complete git switch" [] {
|
||||||
(nu-complete git local branches)
|
use git-completion-utils *
|
||||||
| parse "{value}"
|
let current = (^git branch --show-current) # Can be empty if in detached HEAD
|
||||||
| insert description "local branch"
|
let local_branches = ^git branch --format '%(refname:short)' | lines | filter { $in != $current } | wrap value | insert description 'Local branch'
|
||||||
| append (nu-complete git remote branches nonlocal without prefix
|
let remote_branches = (get-all-git-branches | extract-remote-branches-nonlocal-short $current) | wrap value | insert description 'Remote branch'
|
||||||
| parse "{value}"
|
[...$local_branches, ...$remote_branches]
|
||||||
| insert description "remote branch")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def "nu-complete git checkout" [] {
|
def "nu-complete git checkout" [context: string, position?:int] {
|
||||||
let table_of_checkouts = (nu-complete git local branches)
|
use git-completion-utils *
|
||||||
| parse "{value}"
|
let preceding = $context | str substring ..$position
|
||||||
| insert description "local branch"
|
# See what user typed before, like 'git checkout a-branch a-path'.
|
||||||
| append (nu-complete git remote branches nonlocal without prefix
|
# We exclude some flags from previous tokens, to detect if a branch name has been used as the first argument.
|
||||||
| parse "{value}"
|
# FIXME: This method is still naive, though.
|
||||||
| insert description "remote branch")
|
let prev_tokens = $preceding | str trim | args-split | where ($it not-in $GIT_SKIPABLE_FLAGS)
|
||||||
| append (nu-complete git remote branches with prefix
|
# In these scenarios, we suggest only file paths, not branch:
|
||||||
| parse "{value}"
|
# - After '--'
|
||||||
| insert description "remote branch")
|
# - First arg is a branch
|
||||||
| append (nu-complete git files | where description != "Untracked" | select value | insert description "git file")
|
# If before '--' is just 'git checkout' (or its alias), we suggest "dirty" files only (user is about to reset file).
|
||||||
| append (nu-complete git commits all)
|
if $prev_tokens.2? == '--' {
|
||||||
|
return (get-changed-files)
|
||||||
return {
|
}
|
||||||
|
if '--' in $prev_tokens {
|
||||||
|
return (get-checkoutable-files)
|
||||||
|
}
|
||||||
|
# Already typed first argument.
|
||||||
|
if ($prev_tokens | length) > 2 and $preceding ends-with ' ' {
|
||||||
|
return (get-checkoutable-files)
|
||||||
|
}
|
||||||
|
# The first argument can be local branches, remote branches, files and commits
|
||||||
|
# Get local and remote branches
|
||||||
|
let branches = (get-mergable-sources) | insert style {|row| if $row.description == 'Local' { 'blue' } else 'blue_italic' } | update description { $in + ' branch' }
|
||||||
|
let files = (get-checkoutable-files) | wrap value | insert description 'File' | insert style green
|
||||||
|
let commits = ^git rev-list -n 400 --remotes --oneline | lines | split column -n 2 ' ' value description | insert style light_cyan_dimmed
|
||||||
|
{
|
||||||
options: {
|
options: {
|
||||||
case_sensitive: false,
|
case_sensitive: false,
|
||||||
completion_algorithm: prefix,
|
completion_algorithm: prefix,
|
||||||
sort: false,
|
sort: false,
|
||||||
},
|
},
|
||||||
completions: $table_of_checkouts
|
completions: [...$branches, ...$files, ...$commits]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -836,5 +961,14 @@ export extern "git grep" [
|
||||||
]
|
]
|
||||||
|
|
||||||
export extern "git" [
|
export extern "git" [
|
||||||
command?: string@"nu-complete git subcommands" # subcommands
|
command?: string@"nu-complete git subcommands" # Subcommands
|
||||||
|
--version(-v) # Prints the Git suite version that the git program came from
|
||||||
|
--help(-h) # Prints the synopsis and a list of the most commonly used commands
|
||||||
|
--html-path # Print the path, without trailing slash, where Git’s HTML documentation is installed and exit
|
||||||
|
--man-path # Print the manpath (see man(1)) for the man pages for this version of Git and exit
|
||||||
|
--info-path # Print the path where the Info files documenting this version of Git are installed and exit
|
||||||
|
--paginate(-p) # Pipe all output into less (or if set, $env.PAGER) if standard output is a terminal
|
||||||
|
--no-pager(-P) # Do not pipe Git output into a pager
|
||||||
|
--no-replace-objects # Do not use replacement refs to replace Git objects
|
||||||
|
--bare # Treat the repository as a bare repository
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue