Skip to content

Commit 8a8f06a

Browse files
committed
Dump CI & lint checks & precommit hook & resolve #4, #3, #2
1 parent f9a5513 commit 8a8f06a

15 files changed

Lines changed: 865 additions & 8 deletions

.gitattributes

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Default: keep text normalized in repo
22
* text=auto
33

4+
# Repo metadata/config files should stay LF
5+
.gitattributes text eol=lf
6+
.gitignore text eol=lf
7+
.editorconfig text eol=lf
8+
49
# PowerShell scripts: use CRLF in working tree on Windows
510
*.ps1 text eol=crlf
611
*.psm1 text eol=crlf
@@ -20,3 +25,6 @@
2025
*.zip binary
2126
*.exe binary
2227
*.dll binary
28+
29+
# Git hooks should stay LF for sh compatibility
30+
tools/hooks/* text eol=lf

.github/workflows/ci.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
7+
jobs:
8+
windows:
9+
name: ${{ matrix.label }}
10+
runs-on: windows-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
include:
15+
- label: Windows PowerShell 5.1
16+
shell: powershell
17+
- label: PowerShell 7
18+
shell: pwsh
19+
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
24+
- name: Show PowerShell Version
25+
shell: ${{ matrix.shell }}
26+
run: |
27+
$PSVersionTable.PSVersion.ToString()
28+
29+
- name: Install Dependencies
30+
shell: ${{ matrix.shell }}
31+
run: |
32+
if ($PSVersionTable.PSEdition -eq 'Desktop') {
33+
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
34+
$userModules = Join-Path $HOME 'Documents\WindowsPowerShell\Modules'
35+
} else {
36+
$userModules = Join-Path $HOME 'Documents/PowerShell/Modules'
37+
}
38+
if ($env:PSModulePath -notlike "*$userModules*") {
39+
$env:PSModulePath = "$userModules;$env:PSModulePath"
40+
}
41+
Set-PSRepository PSGallery -InstallationPolicy Trusted
42+
Install-Module PSScriptAnalyzer -Scope CurrentUser -Force -AllowClobber
43+
Install-Module Pester -Scope CurrentUser -Force -SkipPublisherCheck
44+
45+
- name: Run Lint and Tests
46+
shell: ${{ matrix.shell }}
47+
run: |
48+
./tools/ci.ps1

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
private.ps1
22

3+
TestResults.xml
4+
5+
# generated patches
6+
*.patch
7+
*.mbox
8+
patches/

PSScriptAnalyzerSettings.psd1

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@{
2+
Severity = @('Error', 'Warning')
3+
ExcludeRules = @(
4+
'PSAvoidUsingWriteHost',
5+
'PSUseApprovedVerbs',
6+
'PSUseSingularNouns',
7+
'PSUseConsistentWhitespace',
8+
'PSUseConsistentIndentation'
9+
)
10+
Rules = @{
11+
PSUseConsistentWhitespace = @{
12+
Enable = $true
13+
}
14+
PSUseConsistentIndentation = @{
15+
Enable = $true
16+
IndentationSize = 4
17+
PipelineIndentation = 'IncreaseIndentationForFirstPipeline'
18+
}
19+
PSAvoidUsingCmdletAliases = @{
20+
Enable = $true
21+
Whitelist = @()
22+
}
23+
}
24+
}

README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,49 @@
11
# dotfiles
22

3-
To install put following content in $PROFILE.CurrentUserAllHosts:
3+
## Profile installation
4+
5+
Put this in `$PROFILE.CurrentUserAllHosts`:
46

57
```powershell
68
# Microsoft.PowerShell_profile.ps1 (CurrentUserAllHosts)
79
$dot = Join-Path $HOME 'dotfiles\profile.ps1'
810
if (Test-Path $dot) { . $dot }
911
```
12+
13+
## Quality checks
14+
15+
Run both lint and tests:
16+
17+
```powershell
18+
.\tools\ci.ps1
19+
```
20+
21+
Run only lint:
22+
23+
```powershell
24+
.\tools\ci.ps1 -LintOnly
25+
```
26+
27+
Run only tests:
28+
29+
```powershell
30+
.\tools\ci.ps1 -TestOnly
31+
```
32+
33+
## Pre-commit hook
34+
35+
Install local git hook:
36+
37+
```powershell
38+
.\tools\install-hooks.ps1
39+
```
40+
41+
The hook runs `tools/ci.ps1` before every commit.
42+
43+
## What CI checks
44+
45+
- `PSScriptAnalyzer` linting with `PSScriptAnalyzerSettings.psd1`
46+
- `Pester` tests in `tests\`
47+
- GitHub Actions matrix on:
48+
- Windows PowerShell 5.1
49+
- PowerShell 7

bootstrap.ps1

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,25 @@ function Ensure-Module($name, [Version]$min = $null) {
33
if (-not $ok) { Install-Module $name -Scope CurrentUser -Force -AllowClobber -SkipPublisherCheck }
44
}
55

6+
function Ensure-GitGlobalConfig([string]$Key, [string]$Value) {
7+
$current = git config --global --get $Key 2>$null
8+
if ($current -ne $Value) {
9+
git config --global $Key $Value
10+
}
11+
}
12+
613
# PSReadLine is required for Windows PowerShell 5.1; it's already built into PowerShell Core (pwsh)
714
if ($PSVersionTable.PSEdition -ne 'Core') { Ensure-Module PSReadLine ([Version]'2.2.6') }
815

916
Ensure-Module posh-git
1017
Ensure-Module git-aliases
1118

19+
# Git push defaults:
20+
# - first push of a new branch auto-creates upstream tracking
21+
# - default push target is the current branch to origin
22+
Ensure-GitGlobalConfig 'push.autoSetupRemote' 'true'
23+
Ensure-GitGlobalConfig 'push.default' 'current'
24+
Ensure-GitGlobalConfig 'remote.pushDefault' 'origin'
25+
1226
Write-Host "Bootstrap done. Restart PowerShell."
1327

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
@{
2+
RootModule = 'GitAliases.Extras.psm1'
3+
ModuleVersion = '0.1.0'
4+
GUID = 'a5c2859e-7dce-4853-9db5-8cb7927dbdda'
5+
Author = 'PhysShell'
6+
CompanyName = ''
7+
Copyright = ''
8+
Description = 'Custom git aliases and tab completion helpers for PowerShell on top of posh-git'
9+
PowerShellVersion = '5.1'
10+
CompatiblePSEditions = @('Desktop', 'Core')
11+
FunctionsToExport = @(
12+
'Test-InGitRepo',
13+
'Test-GitInProgress',
14+
'Test-WorkingTreeClean',
15+
'Get-CurrentBranch',
16+
'Test-GitRefExists',
17+
'UpMerge',
18+
'UpRebase',
19+
'gapt',
20+
'gcor',
21+
'gdct',
22+
'gdt',
23+
'gdnolock',
24+
'gdv',
25+
'gfo',
26+
'glp',
27+
'gmtl',
28+
'gmtlvim',
29+
'gtv',
30+
'gtl',
31+
'gwip',
32+
'gunwip',
33+
'grsh',
34+
'gccd',
35+
'grl',
36+
'ghash',
37+
'gfp',
38+
'gsw',
39+
'gswc',
40+
'Register-GitAliasCompletion'
41+
)
42+
AliasesToExport = @(
43+
'gum',
44+
'gur',
45+
'gh'
46+
)
47+
}

modules/GitAliases.Extras/GitAliases.Extras.psm1

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ function Test-GitRefExists {
5656
return ($LASTEXITCODE -eq 0)
5757
}
5858

59+
function Convert-ToPowerShellBranchCompletionText {
60+
param(
61+
[Parameter(Mandatory = $true)]
62+
[string]$BranchName
63+
)
64+
65+
if ($BranchName.StartsWith('#')) {
66+
$escaped = $BranchName -replace "'", "''"
67+
return "'$escaped'"
68+
}
69+
70+
return $BranchName
71+
}
72+
5973

6074
# --- Custom Git Command Functions ---
6175
function UpMerge {
@@ -156,6 +170,81 @@ function ghash {
156170
}
157171
}
158172

173+
function gfp {
174+
[CmdletBinding()]
175+
param(
176+
[Parameter(Position = 0)]
177+
[string]$TargetBranch,
178+
[Parameter(Position = 1)]
179+
[string]$OutputFile = 'series.mbox'
180+
)
181+
182+
if (-not (Test-InGitRepo)) {
183+
throw "Not a git repository."
184+
}
185+
186+
$defaultRemote = git config --get checkout.defaultRemote 2>$null
187+
if (-not $defaultRemote) { $defaultRemote = 'origin' }
188+
189+
if (-not $TargetBranch) {
190+
if (Test-GitRefExists "refs/remotes/$defaultRemote/main") {
191+
$TargetBranch = 'main'
192+
} elseif (Test-GitRefExists "refs/remotes/$defaultRemote/master") {
193+
$TargetBranch = 'master'
194+
} else {
195+
$remoteHead = git symbolic-ref --quiet "refs/remotes/$defaultRemote/HEAD" 2>$null
196+
if ($LASTEXITCODE -eq 0 -and $remoteHead) {
197+
$prefix = "refs/remotes/$defaultRemote/"
198+
if ($remoteHead.StartsWith($prefix)) {
199+
$TargetBranch = $remoteHead.Substring($prefix.Length).Trim()
200+
}
201+
}
202+
}
203+
204+
if (-not $TargetBranch) { $TargetBranch = 'main' }
205+
}
206+
207+
$range = "$defaultRemote/$TargetBranch..HEAD"
208+
$resolvedOutput = if ([IO.Path]::IsPathRooted($OutputFile)) {
209+
$OutputFile
210+
} else {
211+
Join-Path (Get-Location) $OutputFile
212+
}
213+
214+
$outputDir = Split-Path -Parent $resolvedOutput
215+
if ($outputDir -and -not (Test-Path $outputDir)) {
216+
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
217+
}
218+
219+
$stderrFile = [IO.Path]::GetTempFileName()
220+
try {
221+
$process = Start-Process -FilePath 'git' `
222+
-ArgumentList @('format-patch', '--cover-letter', '--stat', '--stdout', $range) `
223+
-NoNewWindow `
224+
-PassThru `
225+
-Wait `
226+
-RedirectStandardOutput $resolvedOutput `
227+
-RedirectStandardError $stderrFile
228+
229+
$stderrText = Get-Content -LiteralPath $stderrFile -Raw -ErrorAction SilentlyContinue
230+
if ($stderrText) {
231+
$stderrText = $stderrText.Trim()
232+
if ($stderrText) {
233+
Write-Host $stderrText
234+
}
235+
}
236+
237+
if ($process.ExitCode -ne 0) {
238+
Remove-Item -LiteralPath $resolvedOutput -Force -ErrorAction SilentlyContinue
239+
throw "git format-patch failed for '$range' (exit code: $($process.ExitCode))."
240+
}
241+
242+
return $resolvedOutput
243+
} finally {
244+
Remove-Item -LiteralPath $stderrFile -Force -ErrorAction SilentlyContinue
245+
}
246+
}
247+
159248
function gsw {
160249
[CmdletBinding()]
161250
param(
@@ -224,8 +313,12 @@ function Register-GitAliasCompletion {
224313
}
225314
}
226315

227-
# Add your custom aliases from this module
228-
Get-Command -Module GitAliases.Extras -CommandType Function | ForEach-Object {
316+
# Add custom aliases from this module without module-name lookups
317+
# to avoid self-import recursion during module initialization.
318+
$moduleName = $ExecutionContext.SessionState.Module.Name
319+
Get-Command -CommandType Function |
320+
Where-Object { $_.ModuleName -eq $moduleName } |
321+
ForEach-Object {
229322
$func = $_
230323
$definition = $func.ScriptBlock.ToString()
231324
if ($definition -match $aliasRegex) {
@@ -257,11 +350,32 @@ function Register-GitAliasCompletion {
257350
$gitLine = $line -replace "^$commandName", "git $subCommand"
258351
$offset = ("git $subCommand").Length - $commandName.Length
259352
$gitCursorPosition = $cursorPosition + $offset
353+
if ($gitCursorPosition -lt 0) { $gitCursorPosition = 0 }
354+
if ($gitCursorPosition -gt $gitLine.Length) { $gitCursorPosition = $gitLine.Length }
260355

261356
# Use posh-git's official completion function
262357
if (Get-Command GitTabExpansion -ErrorAction SilentlyContinue) {
263358
try {
264-
return GitTabExpansion $gitLine $gitCursorPosition
359+
$results = GitTabExpansion $gitLine $gitCursorPosition
360+
if ($subCommand -in @('checkout', 'switch', 'merge', 'rebase', 'branch', 'reset', 'revert')) {
361+
return $results | ForEach-Object {
362+
if ($null -eq $_) { return }
363+
$completionText = $_.CompletionText
364+
if ($completionText -is [string] -and $completionText.StartsWith('#')) {
365+
$safeText = Convert-ToPowerShellBranchCompletionText -BranchName $completionText
366+
return [System.Management.Automation.CompletionResult]::new(
367+
$safeText,
368+
$_.ListItemText,
369+
$_.ResultType,
370+
$_.ToolTip
371+
)
372+
}
373+
374+
return $_
375+
}
376+
}
377+
378+
return $results
265379
} catch {
266380
Write-Warning "posh-git's GitTabExpansion failed for alias '$commandName'."
267381
}
@@ -276,10 +390,14 @@ function Register-GitAliasCompletion {
276390
Where-Object { $_ -like "$wordToComplete*" }
277391
if ($branches) {
278392
return $branches | ForEach-Object {
279-
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
393+
$branchName = $_
394+
$safeText = Convert-ToPowerShellBranchCompletionText -BranchName $branchName
395+
[System.Management.Automation.CompletionResult]::new($safeText, $branchName, 'ParameterValue', $branchName)
280396
}
281397
}
282-
} catch {}
398+
} catch {
399+
return @()
400+
}
283401
}
284402

285403
# Return nothing if no completions are found

0 commit comments

Comments
 (0)