Created
February 2, 2025 16:16
-
-
Save sankalpmukim/d67af569fc10904963f554646a9cebf2 to your computer and use it in GitHub Desktop.
A simple script to stage sections of your code, and not have to stage your entire file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
Stages Git changes within a specified line range for a given file. | |
.DESCRIPTION | |
This script takes a file path and a line range (start and end lines) as input. | |
It identifies all Git changes within that line range and stages them for commit. | |
.PARAMETER FilePath | |
The path to the target file. | |
.PARAMETER StartLine | |
The starting line number of the range. | |
.PARAMETER EndLine | |
The ending line number of the range. | |
.EXAMPLE | |
.\Stage-GitChangesByLineRange.ps1 -FilePath "src\example.cs" -StartLine 10 -EndLine 20 -Verbose | |
#> | |
[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')] | |
param ( | |
[Parameter(Mandatory = $true, Position = 0, HelpMessage = "The path to the target file.")] | |
[ValidateNotNullOrEmpty()] | |
[string]$FilePath, | |
[Parameter(Mandatory = $true, Position = 1, HelpMessage = "The starting line number of the range.")] | |
[ValidateRange(1, [int]::MaxValue)] | |
[int]$StartLine, | |
[Parameter(Mandatory = $true, Position = 2, HelpMessage = "The ending line number of the range.")] | |
[ValidateRange(1, [int]::MaxValue)] | |
[int]$EndLine | |
) | |
# Enable detailed logging | |
$LogFilePath = Join-Path -Path (Get-Location) -ChildPath "Stage-GitChangesByLineRange.log" | |
# Function to log messages to both console and log file | |
function Write-Log { | |
param ( | |
[Parameter(Mandatory = $true)] | |
[ValidateSet("INFO", "WARN", "ERROR", "DEBUG")] | |
[string]$Level, | |
[Parameter(Mandatory = $true)] | |
[string]$Message | |
) | |
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" | |
$logMessage = "[$timestamp] [$Level] $Message" | |
switch ($Level) { | |
"INFO" { Write-Host $logMessage -ForegroundColor Green } | |
"WARN" { Write-Host $logMessage -ForegroundColor Yellow } | |
"ERROR" { Write-Host $logMessage -ForegroundColor Red } | |
"DEBUG" { Write-Verbose $logMessage } | |
} | |
# Append to log file | |
Add-Content -Path $LogFilePath -Value $logMessage | |
} | |
# Function to display error and exit | |
function Throw-ErrorAndExit { | |
param ( | |
[string]$Message | |
) | |
Write-Log -Level "ERROR" -Message $Message | |
exit 1 | |
} | |
# Start logging | |
Write-Log -Level "INFO" -Message "Script execution started." | |
# Validate that Git is installed | |
Write-Log -Level "DEBUG" -Message "Checking if Git is installed." | |
if (-not (Get-Command git -ErrorAction SilentlyContinue)) { | |
Throw-ErrorAndExit "Git is not installed or not available in the system PATH." | |
} | |
Write-Log -Level "INFO" -Message "Git is installed." | |
# Validate file existence | |
Write-Log -Level "DEBUG" -Message "Validating existence of file: $FilePath" | |
if (-not (Test-Path -Path $FilePath)) { | |
Throw-ErrorAndExit "The file '$FilePath' does not exist." | |
} | |
Write-Log -Level "INFO" -Message "File '$FilePath' exists." | |
# Validate line numbers | |
Write-Log -Level "DEBUG" -Message "Validating line numbers: StartLine=$StartLine, EndLine=$EndLine" | |
if ($StartLine -lt 1) { | |
Throw-ErrorAndExit "StartLine must be greater than or equal to 1." | |
} | |
if ($EndLine -lt $StartLine) { | |
Throw-ErrorAndExit "EndLine must be greater than or equal to StartLine." | |
} | |
# Get total number of lines in the file | |
Write-Log -Level "DEBUG" -Message "Counting total lines in the file." | |
try { | |
$totalLines = (Get-Content -Path $FilePath -ErrorAction Stop).Count | |
Write-Log -Level "DEBUG" -Message "Total lines in file: $totalLines" | |
} | |
catch { | |
Throw-ErrorAndExit "Failed to read the file '$FilePath'. Error: $_" | |
} | |
if ($EndLine -gt $totalLines) { | |
Throw-ErrorAndExit "EndLine ($EndLine) exceeds total number of lines ($totalLines) in the file." | |
} | |
Write-Log -Level "INFO" -Message "Line numbers are within the valid range." | |
# Ensure the file is tracked by Git | |
Write-Log -Level "DEBUG" -Message "Checking if the file is tracked by Git." | |
$gitLsFiles = git ls-files --error-unmatch "$FilePath" 2>$null | |
if ($LASTEXITCODE -ne 0) { | |
Throw-ErrorAndExit "The file '$FilePath' is not tracked by Git." | |
} | |
Write-Log -Level "INFO" -Message "File '$FilePath' is tracked by Git." | |
# Get the Git diff for the file with file headers | |
Write-Log -Level "DEBUG" -Message "Retrieving Git diff for the file." | |
$diff = git diff --unified=0 "$FilePath" 2>&1 | |
if ($LASTEXITCODE -ne 0) { | |
Throw-ErrorAndExit "Failed to retrieve Git diff for '$FilePath'. Git output:`n$diff" | |
} | |
if (-not $diff) { | |
Write-Log -Level "INFO" -Message "No changes detected in '$FilePath'. Nothing to stage." | |
exit 0 | |
} | |
Write-Log -Level "INFO" -Message "Changes detected in '$FilePath'. Processing diffs." | |
# Initialize variables for parsing diff | |
$filteredDiff = @() | |
$diffLines = $diff -split "`n" | |
$currentHunk = @() | |
$hunkStartOld = 0 | |
$hunkStartNew = 0 | |
$hunkOldCount = 0 | |
$hunkNewCount = 0 | |
$inHunk = $false | |
$fileHeaders = @() | |
# Capture file headers before any hunks | |
Write-Log -Level "DEBUG" -Message "Capturing file headers from the diff." | |
foreach ($line in $diffLines) { | |
if ($line.StartsWith('@@')) { | |
break | |
} | |
$fileHeaders += $line | |
} | |
# Start processing hunks | |
Write-Log -Level "DEBUG" -Message "Parsing Git diff to filter relevant hunks." | |
foreach ($line in $diffLines) { | |
if ($line -match '^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@') { | |
# If there is an existing hunk, check if it overlaps and add if it does | |
if ($inHunk) { | |
# Determine hunk line range in new file | |
$hunkEndNew = $hunkStartNew + ($hunkNewCount - 1) | |
if ($hunkEndNew -ge $StartLine -and $hunkStartNew -le $EndLine) { | |
Write-Log -Level "DEBUG" -Message "Hunk from $hunkStartNew to $hunkEndNew overlaps with specified range. Adding to filtered diff." | |
$filteredDiff += $currentHunk | |
} | |
else { | |
Write-Log -Level "DEBUG" -Message "Hunk from $hunkStartNew to $hunkEndNew does not overlap with specified range. Skipping." | |
} | |
# Reset current hunk | |
$currentHunk = @() | |
} | |
# Parse hunk header | |
$hunkStartOld = [int]$matches[1] | |
$hunkOldCount = if ($matches[2]) { [int]$matches[2] } else { 1 } | |
$hunkStartNew = [int]$matches[3] | |
$hunkNewCount = if ($matches[4]) { [int]$matches[4] } else { 1 } | |
# Start new hunk | |
$currentHunk = @($line) | |
$inHunk = $true | |
} | |
elseif ($inHunk) { | |
# Add lines to current hunk | |
$currentHunk += $line | |
# Track line numbers based on diff | |
if ($line.StartsWith('+') -and -not $line.StartsWith('+++')) { | |
$hunkNewCount += 1 | |
} | |
elseif ($line.StartsWith('-') -and -not $line.StartsWith('---')) { | |
$hunkOldCount += 1 | |
} | |
else { | |
# Context lines | |
$hunkStartNew += 1 | |
$hunkNewCount += 1 | |
} | |
} | |
} | |
# After loop, check the last hunk | |
if ($inHunk) { | |
$hunkEndNew = $hunkStartNew + ($hunkNewCount - 1) | |
if ($hunkEndNew -ge $StartLine -and $hunkStartNew -le $EndLine) { | |
Write-Log -Level "DEBUG" -Message "Last hunk from $hunkStartNew to $hunkEndNew overlaps with specified range. Adding to filtered diff." | |
$filteredDiff += $currentHunk | |
} | |
else { | |
Write-Log -Level "DEBUG" -Message "Last hunk from $hunkStartNew to $hunkEndNew does not overlap with specified range. Skipping." | |
} | |
} | |
# If no hunks are in the specified range | |
if ($filteredDiff.Count -eq 0) { | |
Write-Log -Level "INFO" -Message "No changes detected within lines $StartLine to $EndLine in '$FilePath'. Nothing to stage." | |
exit 0 | |
} | |
Write-Log -Level "INFO" -Message "Relevant hunks identified. Preparing to stage changes." | |
# Create a temporary patch file | |
$tempPatch = [System.IO.Path]::GetTempFileName() | |
Write-Log -Level "DEBUG" -Message "Creating temporary patch file at '$tempPatch'." | |
try { | |
# Write file headers first | |
if ($fileHeaders.Count -gt 0) { | |
Write-Log -Level "DEBUG" -Message "Adding file headers to filtered diff." | |
$filteredDiff = $fileHeaders + $filteredDiff | |
} | |
else { | |
Write-Log -Level "WARN" -Message "No file headers found in the original diff. Patch may fail." | |
} | |
$filteredDiff | Set-Content -Path $tempPatch -Encoding utf8 | |
Write-Log -Level "DEBUG" -Message "Filtered diff written to temporary patch file." | |
# Apply the patch to the index | |
Write-Log -Level "DEBUG" -Message "Applying patch to Git index." | |
$applyResult = git apply --cached --unidiff-zero --ignore-space-change --ignore-whitespace $tempPatch 2>&1 | |
if ($LASTEXITCODE -ne 0) { | |
Throw-ErrorAndExit "Failed to apply patch to index. Git output:`n$applyResult" | |
} | |
else { | |
Write-Log -Level "INFO" -Message "Successfully staged changes in '$FilePath' between lines $StartLine and $EndLine." | |
} | |
} | |
catch { | |
Throw-ErrorAndExit "An error occurred during patch application. Error: $_" | |
} | |
finally { | |
# Clean up temporary patch file | |
if (Test-Path -Path $tempPatch) { | |
Remove-Item -Path $tempPatch -Force | |
Write-Log -Level "DEBUG" -Message "Temporary patch file '$tempPatch' removed." | |
} | |
} | |
Write-Log -Level "INFO" -Message "Script execution completed successfully." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment