diff --git a/fish/includes/prompt.fish b/fish/includes/prompt.fish index a90328e..b1f5be8 100644 --- a/fish/includes/prompt.fish +++ b/fish/includes/prompt.fish @@ -9,6 +9,67 @@ function prompt-reset --description 'Reset fish prompt state used by this rc' git-id-prompt end +function __fishrc_github_owner_from_url --description 'Print GitHub owner from a remote URL' + set -l url $argv[1] + test -n "$url"; or return 1 + + set -l owner (string replace -r '^https?://[^/]+/([^/]+)/.*$' '$1' -- "$url") + if test "$owner" != "$url" + printf '%s\n' "$owner" + return 0 + end + + set owner (string replace -r '^ssh://[^@]+@[^/]+/([^/]+)/.*$' '$1' -- "$url") + if test "$owner" != "$url" + printf '%s\n' "$owner" + return 0 + end + + set owner (string replace -r '^[^@]+@[^:]+:([^/]+)/.*$' '$1' -- "$url") + if test "$owner" != "$url" + printf '%s\n' "$owner" + return 0 + end + + return 1 +end + +function __fishrc_git_remote_url --description 'Print a git remote URL from a remote name or URL' + set -l remote $argv[1] + test -n "$remote"; or return 1 + + if string match -qr '://|^[^@]+@[^:]+:' -- "$remote" + printf '%s\n' "$remote" + else + command git remote get-url "$remote" 2>/dev/null + end +end + +function __fishrc_prompt_pr_by_head --description 'Print PR number and state for an exact GitHub head owner and branch' + set -l head_owner $argv[1] + set -l head_branch $argv[2] + test -n "$head_owner"; and test -n "$head_branch"; or return 1 + + set -l pr_lines + set -l jq 'map(select(.state == "OPEN" or .state == "MERGED")) | sort_by(.updatedAt) | reverse | .[] | [.number, .state, .headRepositoryOwner.login, .headRefName] | @tsv' + if command -sq timeout + set pr_lines (command timeout 1s gh pr list --head "$head_branch" --state all --limit 50 --json number,state,updatedAt,headRefName,headRepositoryOwner --jq "$jq" 2>/dev/null) + else + set pr_lines (command gh pr list --head "$head_branch" --state all --limit 50 --json number,state,updatedAt,headRefName,headRepositoryOwner --jq "$jq" 2>/dev/null) + end + + set -l tab (printf '\t') + for line in $pr_lines + set -l fields (string split "$tab" -- "$line") + if test "$fields[3]" = "$head_owner"; and test "$fields[4]" = "$head_branch" + printf '%s\n%s\n' "$fields[1]" "$fields[2]" + return 0 + end + end + + return 1 +end + function __fishrc_prompt_pr_state --description 'Set GitHub PR prompt state for a branch' set -l branch $argv[1] test -n "$branch"; or return 1 @@ -18,7 +79,7 @@ function __fishrc_prompt_pr_state --description 'Set GitHub PR prompt state for if test -z "$repo_key" set repo_key (command jj root --ignore-working-copy 2>/dev/null) end - set -l cache_key "$repo_key:$branch" + set -l cache_key "$repo_key:$branch:pr-v2" set -l now (date +%s) if test "$__fishrc_prompt_pr_cache_key" = "$cache_key" @@ -39,11 +100,27 @@ function __fishrc_prompt_pr_state --description 'Set GitHub PR prompt state for end end - set -l pr_line + set -l repo_owner if command -sq timeout - set pr_line (command timeout 1s gh pr list --head "$branch" --state all --limit 20 --json number,state,updatedAt --jq 'map(select(.state == "OPEN" or .state == "MERGED")) | sort_by(.updatedAt) | reverse | .[0] | select(.number != null) | .number, .state' 2>/dev/null) + set repo_owner (command timeout 1s gh repo view --json owner --jq '.owner.login' 2>/dev/null) else - set pr_line (command gh pr list --head "$branch" --state all --limit 20 --json number,state,updatedAt --jq 'map(select(.state == "OPEN" or .state == "MERGED")) | sort_by(.updatedAt) | reverse | .[0] | select(.number != null) | .number, .state' 2>/dev/null) + set repo_owner (command gh repo view --json owner --jq '.owner.login' 2>/dev/null) + end + set repo_owner (string trim -- "$repo_owner") + test -n "$repo_owner"; or return 1 + + set -l pr_line + set pr_line (__fishrc_prompt_pr_by_head "$repo_owner" "$branch") + + set -l branch_remote (command git config --get "branch.$branch.remote" 2>/dev/null) + set -l branch_merge (command git config --get "branch.$branch.merge" 2>/dev/null) + set -l branch_head (string replace -r '^refs/heads/' '' -- "$branch_merge") + if test -z "$pr_line"; and test -n "$branch_remote"; and test -n "$branch_head"; and test "$branch_head" != "$branch_merge" + set -l remote_url (__fishrc_git_remote_url "$branch_remote") + set -l remote_owner (__fishrc_github_owner_from_url "$remote_url") + if test -n "$remote_owner" + set pr_line (__fishrc_prompt_pr_by_head "$remote_owner" "$branch_head") + end end set -l pr_number (string trim -- "$pr_line[1]") diff --git a/powershell.ps1 b/powershell.ps1 index c8df146..d71d308 100644 --- a/powershell.ps1 +++ b/powershell.ps1 @@ -646,6 +646,48 @@ function prompt-reset { git-id-prompt } +function Get-GithubOwnerFromRemoteUrl { + param([string]$Url) + if (-not $Url) { return $null } + + foreach ($pattern in @( + '^https?://[^/]+/([^/]+)/.*$', + '^ssh://[^@]+@[^/]+/([^/]+)/.*$', + '^[^@]+@[^:]+:([^/]+)/.*$' + )) { + if ($Url -match $pattern) { return $Matches[1] } + } + + return $null +} + +function Get-GitRemoteUrl { + param([string]$Remote) + if (-not $Remote) { return $null } + + if ($Remote -match '://|^[^@]+@[^:]+:') { return $Remote } + Invoke-RawGit remote get-url $Remote 2>$null | Select-Object -First 1 +} + +function Get-PromptPrByHead { + param( + [string]$HeadOwner, + [string]$HeadBranch + ) + if (-not $HeadOwner -or -not $HeadBranch) { return $null } + + $jq = 'map(select(.state == "OPEN" or .state == "MERGED")) | sort_by(.updatedAt) | reverse | .[] | [.number, .state, .headRepositoryOwner.login, .headRefName] | @tsv' + $prLines = Invoke-ExternalCommand gh pr list --head $HeadBranch --state all --limit 50 --json number,state,updatedAt,headRefName,headRepositoryOwner --jq $jq 2>$null + foreach ($line in $prLines) { + $fields = $line -split "`t", 4 + if ($fields.Count -eq 4 -and $fields[2] -eq $HeadOwner -and $fields[3] -eq $HeadBranch) { + return [pscustomobject]@{ Number = $fields[0]; State = $fields[1] } + } + } + + return $null +} + function Get-PromptPrState { param([string]$Branch) if (-not $Branch -or -not (has gh)) { return $null } @@ -657,17 +699,31 @@ function Get-PromptPrState { if (-not $repoKey) { return $null } $repoKey = $repoKey | Select-Object -First 1 - $cacheKey = "$repoKey`:$Branch" + $cacheKey = "$repoKey`:$Branch`:pr-v2" $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() if ($global:__PwshRcPromptPrCacheKey -eq $cacheKey -and ($now - [int64]$global:__PwshRcPromptPrCacheTime) -lt 300) { if ($global:__PwshRcPromptPrCacheValue[0] -eq '__none') { return $null } return [pscustomobject]@{ Number = $global:__PwshRcPromptPrCacheValue[0]; Color = $global:__PwshRcPromptPrCacheValue[1] } } - $jq = 'map(select(.state == "OPEN" or .state == "MERGED")) | sort_by(.updatedAt) | reverse | .[0] | select(.number != null) | .number, .state' - $prLine = Invoke-ExternalCommand gh pr list --head $Branch --state all --limit 20 --json number,state,updatedAt --jq $jq 2>$null - $prNumber = $prLine | Select-Object -First 1 - $prState = $prLine | Select-Object -Skip 1 -First 1 + $repoOwner = Invoke-ExternalCommand gh repo view --json owner --jq '.owner.login' 2>$null | Select-Object -First 1 + if (-not $repoOwner) { return $null } + + $pr = Get-PromptPrByHead $repoOwner $Branch + + if (-not $pr) { + $branchRemote = Invoke-RawGit config --get "branch.$Branch.remote" 2>$null | Select-Object -First 1 + $branchMerge = Invoke-RawGit config --get "branch.$Branch.merge" 2>$null | Select-Object -First 1 + if ($branchRemote -and $branchMerge -and $branchMerge.StartsWith('refs/heads/')) { + $branchHead = $branchMerge.Substring('refs/heads/'.Length) + $remoteUrl = Get-GitRemoteUrl $branchRemote + $remoteOwner = Get-GithubOwnerFromRemoteUrl $remoteUrl + if ($remoteOwner) { $pr = Get-PromptPrByHead $remoteOwner $branchHead } + } + } + + $prNumber = if ($pr) { $pr.Number } else { $null } + $prState = if ($pr) { $pr.State } else { $null } $prColor = if ($prState -eq 'MERGED') { 'AF87FF' } else { '00FF00' } $global:__PwshRcPromptPrCacheKey = $cacheKey