├── .gitignore ├── Readme.md ├── git-lfs-fix-attributes.bat ├── git-lfs-fix-attributes.ps1 ├── git-lfs-push-unlock.bat ├── git-lfs-push-unlock.ps1 ├── git-lfs-unlock-unchanged.bat ├── git-lfs-unlock-unchanged.ps1 └── inc ├── locking.ps1 └── status.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Steve's Git (LFS+Locking) Helper Scripts 2 | 3 | This is a collection of Powershell scripts I use to help me manage a few tricky 4 | Git repo tasks, mostly those related to using [Git LFS](https://git-lfs.github.com/) 5 | and its [file locking feature](https://github.com/git-lfs/git-lfs/wiki/File-Locking). 6 | 7 | All these scripts work with the regular Powershell 5.1 shipped with Windows 10, 8 | but should also work fine with later versions. 9 | 10 | Wrapper .bat files are provided in case you want to call these from somewhere 11 | other than a Powershell prompt. 12 | 13 | ## Push And Unlock Files 14 | 15 | Push a branch to a remote, and unlock any files which you pushed (that aren't modified) 16 | 17 | ``` 18 | Usage: 19 | git-lfs-push-and-unlock.ps1 [options] [...] 20 | 21 | Arguments: 22 | : The remote to push to (required) 23 | ... : One or more refs to push (optional, current branch assumed 24 | 25 | Options: 26 | -dryrun : Don't perform actions, just report what would happen 27 | -verbose : Print more 28 | -help : Print this help 29 | ``` 30 | 31 | I added a feature to the [UE4 Git LFS Plugin](https://github.com/SRombauts/UE4GitPlugin) 32 | which automatically unlocked files on push, this script is a version of that you 33 | can run from anywhere. I find it useful when running a mixture of in-UE and 34 | out-of-UE workflow. 35 | 36 | It figures out which lockable LFS files you're pushing, and unlocks them 37 | if the push was successful, so long as you don't have further uncommitted 38 | modifications. 39 | 40 | ## Unlock unchanged files 41 | 42 | If you suspect that you have some file locks you don't need, run this command 43 | and it will unlock anything you have locked, which you don't have outstanding 44 | changes for. 45 | 46 | ``` 47 | Usage: 48 | git-lfs-unlock-unchanged.ps1 [options] 49 | 50 | Options: 51 | 52 | -dryrun : Don't perform actions, just report what would happen 53 | -verbose : Print more 54 | -help : Print this help 55 | ``` 56 | 57 | "Unchanged" means that there are no uncommitted changes, *and* no commits on 58 | this branch that contain those files that haven't been pushed to your default 59 | remote yet. 60 | 61 | ## Fix file attributes 62 | 63 | Git LFS makes lockable files read-only on checkout or unlock to prevent 64 | accidental changes. Sometimes the attributes can get out of sync though, 65 | so this script checks them all and fixes where necessary. 66 | 67 | ``` 68 | Usage: 69 | git-lfs-fix-attributes.ps1 [options] 70 | 71 | Options: 72 | 73 | -dryrun : Don't perform actions, just report what would happen 74 | -verbose : Print more 75 | -help : Print this help 76 | ``` -------------------------------------------------------------------------------- /git-lfs-fix-attributes.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | powershell git-lfs-fix-attributes.ps1 %* -------------------------------------------------------------------------------- /git-lfs-fix-attributes.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] # Fail on unknown args 2 | param ( 3 | [switch]$dryrun = $false, 4 | [switch]$help = $false 5 | ) 6 | 7 | function Print-Usage { 8 | Write-Output "Git LFS Fix Attributes" 9 | Write-Output " Fix the read-only attributes on LFS files which are lockable" 10 | Write-Output " but which are not currently locked. Unlocked files are" 11 | Write-Output " made read-only on checkout but it's possible to accidentally" 12 | Write-Output " have files left read/write when they aren't locked, which" 13 | Write-Output " will only get fixed the next time this file is checked out." 14 | Write-Output "Usage:" 15 | Write-Output " git-lfs-fix-attributes.ps1 [options]" 16 | Write-Output "Options:" 17 | Write-Output " " 18 | Write-Output " -dryrun : Don't perform actions, just report what would happen" 19 | Write-Output " -verbose : Print more" 20 | Write-Output " -help : Print this help" 21 | 22 | } 23 | 24 | $ErrorActionPreference = "Stop" 25 | 26 | if ($help) { 27 | Print-Usage 28 | Exit 0 29 | } 30 | 31 | . $PSScriptRoot\inc\locking.ps1 32 | 33 | Write-Output "Checking file attributes..." 34 | $lockableLfsFiles = Get-All-Lockable-Files 35 | Write-Verbose ("Checking attributes on lockable files:`n " + ($lockableLfsFiles -join "`n ")) 36 | 37 | # Now get active locks 38 | $lockedFiles = Get-Locked-Files 39 | Write-Verbose ("Currently locked files:`n " + ($lockedFiles -join "`n ")) 40 | 41 | $numFixed = 0 42 | foreach ($filename in $lockableLfsFiles) { 43 | # Skip missing files; may have locked something then deleted it 44 | if (-not (Test-Path $filename -PathType Leaf)) { 45 | Write-Verbose "${filename}: skipping, missing locally" 46 | continue 47 | } 48 | $shouldBeReadOnly = -not ($lockedFiles -contains $filename) 49 | $isReadOnly = Get-ItemProperty -Path $filename | Select-Object -Expand IsReadOnly 50 | if ($isReadOnly -ne $shouldBeReadOnly) { 51 | if ($dryrun) { 52 | Write-Verbose "${filename}: read-only should be $shouldBeReadOnly" 53 | } else { 54 | Write-Output "${filename}: setting read-only=$shouldBeReadOnly" 55 | Set-ItemProperty -Path $filename -Name IsReadOnly -Value $shouldBeReadOnly 56 | } 57 | ++$numFixed 58 | } 59 | } 60 | 61 | if ($numFixed -gt 0) { 62 | if ($dryrun) { 63 | Write-Output "Would have fixed $numFixed file attributes." 64 | } else { 65 | Write-Output "Fixed $numFixed file attributes." 66 | } 67 | } else { 68 | Write-Output "All file attributes are OK" 69 | } 70 | -------------------------------------------------------------------------------- /git-lfs-push-unlock.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | powershell git-lfs-push-unlock.ps1 %* -------------------------------------------------------------------------------- /git-lfs-push-unlock.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] # Fail on unknown args 2 | param ( 3 | [switch]$dryrun = $false, 4 | [switch]$help = $false, 5 | [Parameter(Position=0)] 6 | $remote, 7 | [Parameter(ValueFromRemainingArguments)] 8 | $refs 9 | ) 10 | 11 | function Print-Usage { 12 | Write-Output "Git LFS push-unlock" 13 | Write-Output " Push a branch to a remote, and unlock any files which you pushed (that aren't modified)" 14 | Write-Output "Usage:" 15 | Write-Output " git-lfs-push-and-unlock.ps1 [options] [...]" 16 | Write-Output " " 17 | Write-Output "Arguments:" 18 | Write-Output " : The remote to push to (required)" 19 | Write-Output " ... : One or more refs to push (optional, current branch assumed)" 20 | Write-Output " " 21 | Write-Output "Options:" 22 | Write-Output " -dryrun : Don't perform actions, just report what would happen" 23 | Write-Output " -verbose : Print more" 24 | Write-Output " -help : Print this help" 25 | 26 | } 27 | 28 | $ErrorActionPreference = "Stop" 29 | 30 | if ($help) { 31 | Print-Usage 32 | Exit 0 33 | } 34 | 35 | . $PSScriptRoot\inc\locking.ps1 36 | . $PSScriptRoot\inc\status.ps1 37 | 38 | if (-not $remote) { 39 | Write-Output " " 40 | Write-Output " ERROR: Missing parameter: remote" 41 | Write-Output " " 42 | Print-Usage 43 | Exit 3 44 | } 45 | 46 | if (-not $refs) { 47 | # git lfs needs at least one ref, get current branch 48 | $refs = git branch --show-current 49 | } 50 | 51 | 52 | Write-Output "Checking for what we'd push..." 53 | 54 | $gitallopt = "" 55 | if ($all) { 56 | $gitallopt = "--all" 57 | } 58 | 59 | # git lfs push in dry run mode will tell us the list of objects 60 | #$lfsPushOutput = git lfs push $gitallopt --dry-run origin master 61 | $lfspushargs = "lfs", "push", $gitallopt, "--dry-run", $remote, $refs 62 | # Invoke-Expression doesn't return errors 63 | $lfsPushOutput = git $lfspushargs 64 | if (!$?) { 65 | Write-Output "ERROR: failed to call 'git $lfspushargs'" 66 | Exit 5 67 | } 68 | 69 | # Result format is of the form 70 | # push f4ee401c063058a78842bb3ed98088e983c32aa447f346db54fa76f844a7e85e => Path/To/File 71 | # With some potential informationals we can ignore 72 | 73 | $filesbeingpushed = [System.Collections.ArrayList]@() 74 | foreach ($line in $lfsPushOutput) { 75 | if ($line -match "^push ([a-f0-9]+)\s+=>\s+(.+)$") { 76 | $oid = $matches[1] 77 | $filename = $matches[2] 78 | $filesbeingpushed.Add($filename.Trim()) > $null 79 | } 80 | } 81 | 82 | # Wrap in @() to avoid collapsing to a single string when only 1 file 83 | $filesbeingpushed = @($filesbeingpushed | Select-Object -Unique) 84 | Write-Verbose ("Files being pushed: `n " + ($filesbeingpushed -join "`n ")) 85 | 86 | # Get the list of locked files so we don't try to unlock things we don't own 87 | # That's an error for git-lfs 88 | $lockedfiles = Get-Locked-Files 89 | Write-Verbose ("Files currently locked: `n " + ($lockedfiles -join "`n ")) 90 | 91 | # get modified files, we don't unlock those 92 | $modifiedfiles = Get-Modified-Files 93 | Write-Verbose ("Files modified: `n " + ($modifiedfiles -join "`n ")) 94 | 95 | # Take the intersection of locked and pushed 96 | # then difference of modified 97 | # Wrap in @() to avoid collapsing to a single string when only 1 file 98 | $filesToUnlock = @($lockedfiles | Where-Object {$filesbeingpushed -contains $_ -and -not ($modifiedfiles -contains $_)}) 99 | Write-Verbose ("Files to unlock: `n " + ($filesToUnlock -join "`n ")) 100 | 101 | # Push first 102 | $gitpushargs = "push", $gitallopt, $remote, $refs 103 | if ($verbose -or $dryrun) { 104 | Write-Output ("Run 'git $gitpushargs'") 105 | } 106 | if (-not $dryrun) { 107 | git $gitpushargs 108 | if (!$?) { 109 | # git output is enough 110 | Exit 5 111 | } 112 | } 113 | 114 | # Unlock these files 115 | if ($filesToUnlock.Count -gt 0) { 116 | if ($dryrun) { 117 | Write-Output ("Would have unlocked:`n " + ($filesToUnlock -join "`n ")) 118 | } else { 119 | foreach ($filename in $filesToUnlock) { 120 | git lfs unlock $filename 121 | } 122 | } 123 | } 124 | 125 | Write-Output "DONE: Push and unlock completed successfully" -------------------------------------------------------------------------------- /git-lfs-unlock-unchanged.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | powershell git-lfs-unlock-unchanged.ps1 %* -------------------------------------------------------------------------------- /git-lfs-unlock-unchanged.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] # Fail on unknown args 2 | param ( 3 | [switch]$dryrun = $false, 4 | [switch]$help = $false 5 | ) 6 | 7 | function Print-Usage { 8 | Write-Output "Git LFS unlock-unchanged" 9 | Write-Output " Unlock all files which are unchanged locally." 10 | Write-Output " To be unlocked a file must not be modified in the working" 11 | Write-Output " copy, nor be part of any commits which haven't been pushed." 12 | Write-Output "Usage:" 13 | Write-Output " git-lfs-unlock-unchanged.ps1 [options]" 14 | Write-Output "Options:" 15 | Write-Output " " 16 | Write-Output " -dryrun : Don't perform actions, just report what would happen" 17 | Write-Output " -verbose : Print more" 18 | Write-Output " -help : Print this help" 19 | 20 | } 21 | 22 | $ErrorActionPreference = "Stop" 23 | 24 | if ($help) { 25 | Print-Usage 26 | Exit 0 27 | } 28 | 29 | . $PSScriptRoot\inc\locking.ps1 30 | . $PSScriptRoot\inc\status.ps1 31 | 32 | Write-Output "Checking for locked but unchanged files..." 33 | 34 | $modifiedFiles = Get-Modified-Files 35 | Write-Verbose ("Modified files:`n " + ($modifiedFiles -join "`n ")) 36 | 37 | $lfsPushOutput = git lfs push --dry-run origin HEAD 38 | if (!$?) { 39 | Write-Output "ERROR: failed to call 'git lfs push --dry-run'" 40 | Exit 5 41 | } 42 | 43 | # Result format is of the form 44 | # push f4ee401c063058a78842bb3ed98088e983c32aa447f346db54fa76f844a7e85e => Path/To/File 45 | # With some potential informationals we can ignore 46 | 47 | $filesToBePushed = [System.Collections.ArrayList]@() 48 | foreach ($line in $lfsPushOutput) { 49 | if ($line -match "^push ([a-f0-9]+)\s+=>\s+(.+)$") { 50 | $oid = $matches[1] 51 | $filename = $matches[2] 52 | $filesToBePushed.Add($filename.Trim()) > $null 53 | } 54 | } 55 | 56 | # Wrap in @() to avoid collapsing to a single string when only 1 file 57 | $filesToBePushed = @($filesToBePushed | Select-Object -Unique) 58 | Write-Verbose ("Files awaiting push: `n " + ($filesToBePushed -join "`n ")) 59 | 60 | $lockedFiles = Get-Locked-Files 61 | Write-Verbose ("Locked files: `n " + ($lockedFiles -join "`n ")) 62 | 63 | $filesToUnlock = [System.Collections.ArrayList]@() 64 | foreach ($filename in $lockedFiles) { 65 | if (-not ($modifiedFiles -contains $filename) -and -not ($filesToBePushed -contains $filename)) { 66 | $filesToUnlock.Add($filename) > $null 67 | Write-Verbose " $filename isn't modified or awaiting push, will unlock" 68 | } 69 | } 70 | 71 | if ($filesToUnlock.Count -gt 0) { 72 | if ($dryrun) { 73 | Write-Output ("Would have unlocked:`n " + ($filesToUnlock -join "`n ")) 74 | } else { 75 | foreach ($filename in $filesToUnlock) { 76 | git lfs unlock $filename 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /inc/locking.ps1: -------------------------------------------------------------------------------- 1 | # Get an array of all the lockable files in the repository 2 | function Check-IsLockable { 3 | param( 4 | [string]$filename 5 | ) 6 | $out = git check-attr lockable $filename 7 | return $out -match "^([^:]+):\slockable:\sset$" 8 | } 9 | 10 | function Lock-If-Required { 11 | param( 12 | [string]$filename 13 | ) 14 | 15 | if (Get-ItemProperty -Path $filename | Select-Object -Expand IsReadOnly) { 16 | if (Check-IsLockable $filename) { 17 | git lfs lock $filename 18 | if (!$?) { 19 | throw "Failed to lock $filename" 20 | } 21 | 22 | } else { 23 | throw "$filename is read-only but is not lockable" 24 | } 25 | } 26 | } 27 | 28 | function Get-All-Lockable-Files { 29 | 30 | # First get the list of LFS files in the repo (yes, all of them) 31 | $allLfsFiles = git lfs ls-files -n 32 | if (!$?) { 33 | Write-Output "ERROR: failed to call 'git lfs ls-files'" 34 | Exit 5 35 | } 36 | 37 | # Filter these files to those which are lockable 38 | $lockableLfsFiles = [System.Collections.ArrayList]@() 39 | # send files from stdin so we don't have to worry about command line length 40 | $lockableAttrOut = ($allLfsFiles -join "`n") | git check-attr lockable --stdin 41 | foreach ($line in $lockableAttrOut) { 42 | if ($line -match "^([^:]+):\slockable:\sset$") { 43 | $filename = $matches[1] 44 | $lockableLfsFiles.Add($filename.Trim()) > $null 45 | } 46 | } 47 | 48 | return $lockableLfsFiles 49 | } 50 | 51 | # Get an array of the files currently locked by the current user 52 | function Get-Locked-Files { 53 | $lfsLocksOutput = git lfs locks --verify 54 | if (!$?) { 55 | Write-Output "ERROR: failed to call 'git lfs locks'" 56 | Exit 5 57 | } 58 | $lockedFiles = [System.Collections.ArrayList]@() 59 | # Output is of the form (for owned) 60 | # O Path/To/File\tsteve\tID:268 61 | foreach ($line in $lfsLocksOutput) { 62 | if ($line -match "^O ([^\t]+)\t+(.+)\s+ID:(\w+).*$") { 63 | $filename = $matches[1] 64 | $owner = $matches[2] 65 | $id = $matches[3] 66 | $lockedFiles.Add($filename.Trim()) > $null 67 | } 68 | } 69 | 70 | return $lockedFiles 71 | 72 | } -------------------------------------------------------------------------------- /inc/status.ps1: -------------------------------------------------------------------------------- 1 | # Get all files which have been modified in the working copy or index 2 | function Get-Modified-Files { 3 | $statusOutput = git status --porcelain --untracked-files=no 4 | if (!$?) { 5 | Write-Output "ERROR: failed to call 'git status'" 6 | Exit 5 7 | } 8 | 9 | $modifiedFiles = [System.Collections.ArrayList]@() 10 | foreach ($line in $statusOutput) { 11 | # Match modified (any non-blank) in working copy or index or both 12 | if ($line -match "^(?: [^\s]|[^\s] |[^\s][^\s])\s+(.+)$") { 13 | $filename = $matches[1] 14 | $modifiedFiles.Add($filename.Trim()) > $null 15 | } 16 | } 17 | 18 | return $modifiedFiles 19 | 20 | } 21 | --------------------------------------------------------------------------------