Skip to content

Instantly share code, notes, and snippets.

@sankalpmukim
Created February 2, 2025 16:16
Show Gist options
  • Save sankalpmukim/d67af569fc10904963f554646a9cebf2 to your computer and use it in GitHub Desktop.
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.
<#
.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