Skip to content

Instantly share code, notes, and snippets.

@Kenya-West
Created May 16, 2026 16:39
Show Gist options
  • Select an option

  • Save Kenya-West/bcb996d436b858c8c996dba9022fe89d to your computer and use it in GitHub Desktop.

Select an option

Save Kenya-West/bcb996d436b858c8c996dba9022fe89d to your computer and use it in GitHub Desktop.
Copy-GitDirtyFiles - a PowerShell utility that copies only Git-untracked and Git-modified files from a repository into another folder. Useful for creating lightweight backups, transferring work-in-progress changes, or preparing partial archives without copying the entire repository.

Copy-GitDirtyFiles.ps1

A PowerShell utility that copies only Git-untracked and Git-modified files from a repository into another folder. Useful for creating lightweight backups, transferring work-in-progress changes, or preparing partial archives without copying the entire repository.

Features

  • Copies:

    • Modified tracked files (staged and unstaged)
    • Untracked files
  • Optional support for copying ignored (.gitignore) files

  • Automatically skips common cache/build/package directories:

    • node_modules
    • bin
    • obj
    • vendor
    • __pycache__
    • .next
    • dist
    • and many others
  • Preserves directory structure

  • Works from any path inside the Git repository

  • Throws an error if the source folder is not a Git repository

  • Interactive prompts if parameters are omitted


Use Cases

  • Backup only current work without .git
  • Copy local modifications between machines
  • Save dirty working tree before risky operations
  • Prepare compact archives of unfinished work
  • Exclude huge dependency folders automatically

Requirements

  • PowerShell 5.1+ or PowerShell 7+
  • Git installed and available in PATH

Installation

Download the script:

Copy-GitDirtyFiles.ps1

Or clone/save it from the GitHub Gist.


Usage

Basic

.\Copy-GitDirtyFiles.ps1 `
  -From "C:\src\myrepo" `
  -To "D:\backup\myrepo-dirty"

Include .gitignore Files

.\Copy-GitDirtyFiles.ps1 `
  -From "C:\src\myrepo" `
  -To "D:\backup\myrepo-dirty" `
  -IncludeIgnored

Interactive Mode

If -From or -To are omitted, the script will ask interactively:

.\Copy-GitDirtyFiles.ps1

Parameters

Parameter Description
-From Source Git repository path
-To Destination folder
-IncludeIgnored Include ignored (.gitignore) files

What Gets Copied

The script gathers:

Modified Tracked Files

git diff --name-only
git diff --cached --name-only

Untracked Files

git ls-files -o --exclude-standard

Optional Ignored Files

git ls-files -o -i --exclude-standard

Automatically Skipped Directories

The script intentionally ignores common dependency/cache/build folders even if they contain changes.

Examples:

node_modules
dist
build
vendor
bin
obj
__pycache__
.next
.nuxt
target
.gradle
.git

This prevents accidental copying of huge generated content.


Example Output

Git repo top-level: C:\projects\app
Copy destination:    D:\backup\app-dirty
Include ignored:     False

Copied:  14
Skipped (ignored dirs list): 5
Missing (deleted/non-file): 1
Done.

Notes

  • Deleted files are not copied
  • Only regular files are copied
  • File timestamps are preserved by Copy-Item
  • Existing files in destination are overwritten

Why This Exists

Git repositories often contain huge dependency and build directories that are unnecessary for quick backups or migration of current work.

This script focuses only on:

  • the files you actually changed
  • the files Git does not yet track

while intentionally excluding generated artifacts.


License

MIT License


Author

ChatGPT lol.

Created for developers who want lightweight Git working-tree backups without archiving the entire repository.

<#
.SYNOPSIS
Copy only Git-untracked or Git-modified files from one folder to another, skipping common cache/package folders.
.DESCRIPTION
- Copies files that are modified (staged or unstaged) and untracked.
- Optionally includes ignored files (gitignored) via -IncludeIgnored.
- Skips common cache/build/package directories (node_modules, bin/obj, __pycache__, etc.) even if they contain changes.
- Throws if the source folder is not a Git repo.
- Supports -From and -To parameters; prompts if missing.
.EXAMPLE
.\Copy-GitDirtyFiles.ps1 -From "C:\src\myrepo" -To "D:\backup\myrepo-dirty"
.EXAMPLE
.\Copy-GitDirtyFiles.ps1 -From "C:\src\myrepo" -To "D:\backup\myrepo-dirty" -IncludeIgnored
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string] $From,
[Parameter(Mandatory = $false)]
[string] $To,
[Parameter(Mandatory = $false)]
[switch] $IncludeIgnored
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Resolve-AbsolutePath {
param([Parameter(Mandatory)] [string] $Path)
$expanded = [Environment]::ExpandEnvironmentVariables($Path)
$resolved = Resolve-Path -LiteralPath $expanded -ErrorAction Stop
return $resolved.Path
}
function Ensure-Directory {
param([Parameter(Mandatory)] [string] $Dir)
if (-not (Test-Path -LiteralPath $Dir -PathType Container)) {
New-Item -ItemType Directory -Path $Dir -Force | Out-Null
}
}
function Assert-GitRepo {
param([Parameter(Mandatory)] [string] $RepoRoot)
$git = Get-Command git -ErrorAction SilentlyContinue
if (-not $git) {
throw "git executable not found in PATH."
}
$isRepo = & git -C $RepoRoot rev-parse --is-inside-work-tree 2>$null
if ($LASTEXITCODE -ne 0 -or $isRepo -ne "true") {
throw "No Git repository found at '$RepoRoot' (or inside it). Initialize Git first or choose another source folder."
}
}
function Get-GitTopLevel {
param([Parameter(Mandatory)] [string] $RepoAnyPath)
$top = & git -C $RepoAnyPath rev-parse --show-toplevel 2>$null
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($top)) {
throw "Failed to resolve Git top-level directory for '$RepoAnyPath'."
}
return $top.Trim()
}
# Common folders that should never be copied (cache/build/package artifacts).
# Match is path-segment based, so it catches nested occurrences.
$IgnoredDirNames = @(
# Node / JS
"node_modules", "bower_components", ".yarn", ".pnp", ".pnpm-store", ".npm", ".turbo", ".next", ".nuxt", ".output",
"dist", "build", ".cache", ".parcel-cache", ".vite", ".svelte-kit",
# Python
"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".tox", ".nox",
".venv", "venv", "env", "site-packages", ".eggs", "egg-info",
# .NET
"bin", "obj", ".vs", "TestResults", "packages", ".nuget",
# Java / JVM
"target", ".gradle", "build", ".idea", ".classpath", ".project", ".settings",
# Go
"vendor",
# Rust
"target",
# PHP / Composer
"vendor",
# Ruby
"vendor", ".bundle",
# iOS / macOS
"Pods", "DerivedData",
# Misc VCS / tooling caches
".git", ".svn", ".hg"
) | Sort-Object -Unique
function Should-IgnorePath {
param([Parameter(Mandatory)] [string] $RelativePath)
# Normalize separators to forward slashes for consistent checks.
$p = ($RelativePath -replace '\\', '/').TrimStart('./')
$segments = $p.Split('/', [System.StringSplitOptions]::RemoveEmptyEntries)
foreach ($seg in $segments) {
if ($IgnoredDirNames -contains $seg) {
return $true
}
}
return $false
}
function Get-DirtyPaths {
param(
[Parameter(Mandatory)] [string] $RepoRoot,
[Parameter(Mandatory)] [bool] $IncludeIgnoredFiles
)
$set = New-Object System.Collections.Generic.HashSet[string]
# 1) Modified tracked files (unstaged)
$modifiedUnstaged = & git -C $RepoRoot diff --name-only 2>$null
if ($LASTEXITCODE -ne 0) { throw "Failed: git diff --name-only" }
foreach ($p in $modifiedUnstaged) {
if (-not [string]::IsNullOrWhiteSpace($p)) { [void]$set.Add($p.Trim()) }
}
# 2) Modified tracked files (staged)
$modifiedStaged = & git -C $RepoRoot diff --cached --name-only 2>$null
if ($LASTEXITCODE -ne 0) { throw "Failed: git diff --cached --name-only" }
foreach ($p in $modifiedStaged) {
if (-not [string]::IsNullOrWhiteSpace($p)) { [void]$set.Add($p.Trim()) }
}
# 3) Untracked (NOT ignored)
$untracked = & git -C $RepoRoot ls-files -o --exclude-standard 2>$null
if ($LASTEXITCODE -ne 0) { throw "Failed: git ls-files -o --exclude-standard" }
foreach ($p in $untracked) {
if (-not [string]::IsNullOrWhiteSpace($p)) { [void]$set.Add($p.Trim()) }
}
# 4) Ignored (gitignored) files, if requested
if ($IncludeIgnoredFiles) {
$ignored = & git -C $RepoRoot ls-files -o -i --exclude-standard 2>$null
if ($LASTEXITCODE -ne 0) { throw "Failed: git ls-files -o -i --exclude-standard" }
foreach ($p in $ignored) {
if (-not [string]::IsNullOrWhiteSpace($p)) { [void]$set.Add($p.Trim()) }
}
}
return @($set)
}
function Copy-DirtyFiles {
param(
[Parameter(Mandatory)] [string] $RepoRoot,
[Parameter(Mandatory)] [string] $DestinationRoot,
[Parameter(Mandatory)] [bool] $IncludeIgnoredFiles
)
$dirty = @(Get-DirtyPaths -RepoRoot $RepoRoot -IncludeIgnoredFiles $IncludeIgnoredFiles)
if ($dirty.Count -eq 0) {
if ($IncludeIgnoredFiles) {
Write-Host "No modified, untracked, or ignored files found. Nothing to copy."
} else {
Write-Host "No modified or untracked files found. (Ignored files are hidden unless -IncludeIgnored is set.)"
}
return
}
$copied = 0
$skipped = 0
$missing = 0
foreach ($rel in $dirty) {
if (Should-IgnorePath -RelativePath $rel) {
$skipped++
continue
}
$src = Join-Path -Path $RepoRoot -ChildPath $rel
if (-not (Test-Path -LiteralPath $src -PathType Leaf)) {
$missing++
continue
}
$dst = Join-Path -Path $DestinationRoot -ChildPath $rel
$dstDir = Split-Path -Path $dst -Parent
Ensure-Directory -Dir $dstDir
Copy-Item -LiteralPath $src -Destination $dst -Force
$copied++
}
Write-Host "Copied: $copied"
Write-Host "Skipped (ignored dirs list): $skipped"
Write-Host "Missing (deleted/non-file): $missing"
Write-Host "Done."
}
# --- Main flow ---
if ([string]::IsNullOrWhiteSpace($From)) {
$From = Read-Host "Source folder (Git repo path)"
}
if ([string]::IsNullOrWhiteSpace($To)) {
$To = Read-Host "Destination folder"
}
$FromAbs = Resolve-AbsolutePath -Path $From
$ToExpanded = [Environment]::ExpandEnvironmentVariables($To)
if (-not (Test-Path -LiteralPath $ToExpanded)) {
Ensure-Directory -Dir $ToExpanded
}
$ToAbs = Resolve-AbsolutePath -Path $ToExpanded
Assert-GitRepo -RepoRoot $FromAbs
$repoTop = Get-GitTopLevel -RepoAnyPath $FromAbs
Write-Host "Git repo top-level: $repoTop"
Write-Host "Copy destination: $ToAbs"
Write-Host ("Include ignored: " + $IncludeIgnored.IsPresent)
Copy-DirtyFiles -RepoRoot $repoTop -DestinationRoot $ToAbs -IncludeIgnoredFiles $IncludeIgnored.IsPresent
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment