Claude Opus 5.5
Sync from GitHub b12dbfb: sync.ps1: use the script's own repo so it works from any folder
b215254 Download sync.ps1 from mnoorchenar/loss-function-playground: direct link, hf CLI and curl.
- Browser
- Download file 13.4 kB
-
https://huggingface.co/spaces/mnoorchenar/loss-function-playground/resolve/main/sync.ps1
- Command line
-
hf download hf://spaces/mnoorchenar/loss-function-playground/sync.ps1
-
curl -L -o sync.ps1 https://huggingface.co/spaces/mnoorchenar/loss-function-playground/resolve/main/sync.ps1
13.4 kB
| param( | |
| [string]$Message = "", | |
| [switch]$PullOnly, | |
| [switch]$NoHF | |
| ) | |
| # sync.ps1 - commit local changes, sync with GitHub (never force-pushes), then | |
| # mirror the committed files to this repo's Hugging Face copy (dataset or Space). | |
| # | |
| # The Hugging Face target is set once per repo in git config, for example: | |
| # git config hf.repo datasets/mnoorchenar/youtube | |
| # git config hf.repo spaces/mnoorchenar/docmind | |
| # Repos without hf.repo sync to GitHub only. The Hugging Face step needs HF_TOKEN. | |
| # | |
| # .\sync.ps1 "message" commit, sync GitHub, update Hugging Face | |
| # .\sync.ps1 -PullOnly only bring in new commits from GitHub | |
| # .\sync.ps1 -NoHF skip the Hugging Face step | |
| [Console]::OutputEncoding = [Text.Encoding]::UTF8 | |
| $Utf8NoBom = New-Object Text.UTF8Encoding $false | |
| function Fail([string]$Text) { Write-Host "[ERROR] $Text"; exit 1 } | |
| # ---- index.json (folder/file listing used by index.html viewer pages) -------- | |
| $SkipFolders = '.git', '__pycache__', 'node_modules', '.venv', 'venv', 'debug', '.github' | |
| $SkipFiles = 'index.html', 'index.json', 'sync.ps1', 'readme.md', '404.html' | |
| function Get-CleanName([string]$Name) { ($Name -replace '^\d+\.\s*', '').Trim() } | |
| # Names sorted by leading number first (like "01. Intro"), then by name. | |
| function Get-SortedNames([string]$Path, [bool]$Dirs) { | |
| $names = [string[]]@(Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue | | |
| Where-Object { $_.PSIsContainer -eq $Dirs -and -not ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) } | | |
| ForEach-Object { $_.Name }) | |
| if ($names.Count -lt 2) { return ,$names } | |
| $keys = [string[]]@($names | ForEach-Object { | |
| if ($_ -match '^(\d+)') { $Matches[1].TrimStart('0').PadLeft(30, '0') + '|' + $_ } | |
| else { '999999'.PadLeft(30, '0') + '|' + $_ } | |
| }) | |
| [Array]::Sort($keys, $names, [StringComparer]::Ordinal) | |
| return ,$names | |
| } | |
| function Get-HtmlFiles([string]$FolderPath, [string]$RelPrefix) { | |
| $files = New-Object System.Collections.ArrayList | |
| foreach ($fn in (Get-SortedNames $FolderPath $false)) { | |
| if ($SkipFiles -contains $fn -or -not $fn.EndsWith('.html')) { continue } | |
| [void]$files.Add([ordered]@{ name = (Get-CleanName ([IO.Path]::GetFileNameWithoutExtension($fn))); html = "$RelPrefix/$fn" }) | |
| } | |
| return ,$files | |
| } | |
| function Format-JsonString([string]$Text) { | |
| $s = $Text.Replace('\', '\\').Replace('"', '\"').Replace("`n", '\n').Replace("`r", '\r').Replace("`t", '\t') | |
| $s = [regex]::Replace($s, '[\x00-\x1f]', { param($m) '\u{0:x4}' -f [int][char]$m.Value }) | |
| return '"' + $s + '"' | |
| } | |
| # Same layout as Python's json.dumps(indent=2, ensure_ascii=False). | |
| function ConvertTo-PyJson($Value, [int]$Level = 0) { | |
| $pad = ' ' * ($Level + 1) | |
| $end = ' ' * $Level | |
| if ($Value -is [string]) { return (Format-JsonString $Value) } | |
| if ($Value -is [System.Collections.IDictionary]) { | |
| if ($Value.Count -eq 0) { return '{}' } | |
| $parts = foreach ($k in $Value.Keys) { $pad + (Format-JsonString $k) + ': ' + (ConvertTo-PyJson $Value[$k] ($Level + 1)) } | |
| return "{`n" + ($parts -join ",`n") + "`n$end}" | |
| } | |
| $items = @($Value) | |
| if ($items.Count -eq 0) { return '[]' } | |
| $parts = foreach ($i in $items) { $pad + (ConvertTo-PyJson $i ($Level + 1)) } | |
| return "[`n" + ($parts -join ",`n") + "`n$end]" | |
| } | |
| function Update-IndexJson([string]$Root) { | |
| # Only rebuild listings in this layout (or an empty list). Apps with their own | |
| # index.json format, like Markdown2PDF's library, are left alone. | |
| try { $existing = @([IO.File]::ReadAllText((Join-Path $Root 'index.json')) | ConvertFrom-Json) } catch { return $false } | |
| foreach ($e in $existing) { | |
| if ($null -eq $e -or (@($e.PSObject.Properties.Name) -join ',') -ne 'name,folder,files,subfolders') { return $false } | |
| } | |
| $structure = New-Object System.Collections.ArrayList | |
| foreach ($folder in (Get-SortedNames $Root $true)) { | |
| if ($SkipFolders -ccontains $folder) { continue } | |
| $folderPath = Join-Path $Root $folder | |
| $direct = Get-HtmlFiles $folderPath $folder | |
| $subs = New-Object System.Collections.ArrayList | |
| foreach ($sub in (Get-SortedNames $folderPath $true)) { | |
| if ($SkipFolders -ccontains $sub) { continue } | |
| $subFiles = Get-HtmlFiles (Join-Path $folderPath $sub) "$folder/$sub" | |
| if ($subFiles.Count -gt 0) { [void]$subs.Add([ordered]@{ name = (Get-CleanName $sub); folder = $sub; files = $subFiles }) } | |
| } | |
| if ($direct.Count -gt 0 -or $subs.Count -gt 0) { | |
| [void]$structure.Add([ordered]@{ name = (Get-CleanName $folder); folder = $folder; files = $direct; subfolders = $subs }) | |
| } | |
| } | |
| $json = ConvertTo-PyJson $structure | |
| $path = Join-Path $Root 'index.json' | |
| if ([IO.File]::ReadAllText($path).Replace("`r`n", "`n") -ceq $json) { return $false } | |
| [IO.File]::WriteAllText($path, $json, $Utf8NoBom) | |
| return $true | |
| } | |
| # ---- Start ------------------------------------------------------------------ | |
| # Use the repo this script lives in, so it works from any current folder. | |
| $repoRoot = git -C $PSScriptRoot rev-parse --show-toplevel 2>$null | |
| if ($LASTEXITCODE -ne 0) { Fail "Not a git repository" } | |
| Set-Location $repoRoot | |
| $branch = git rev-parse --abbrev-ref HEAD | |
| if ($Message -eq "") { $Message = "Update " + (Get-Date -Format "yyyy-MM-dd HH:mm:ss") } | |
| Write-Host "`n=== Sync: $(Split-Path $repoRoot -Leaf) ===`n" | |
| # ---- 1. Commit local changes ------------------------------------------------ | |
| if (-not $PullOnly) { | |
| if ((Test-Path -LiteralPath (Join-Path $repoRoot 'index.json')) -and (Update-IndexJson $repoRoot)) { | |
| Write-Host "[OK] index.json updated" | |
| } | |
| git -c core.safecrlf=false add -A | |
| git diff --cached --quiet | |
| if ($LASTEXITCODE -ne 0) { | |
| git commit -q -m $Message | |
| if ($LASTEXITCODE -ne 0) { Fail "Commit failed" } | |
| Write-Host "[OK] Committed: $Message" | |
| } else { | |
| Write-Host "[OK] Nothing new to commit" | |
| } | |
| } | |
| # ---- 2. GitHub: merge new commits, then push (no force) --------------------- | |
| Write-Host "Checking GitHub..." | |
| $out = git fetch -q origin 2>&1 | |
| if ($LASTEXITCODE -ne 0) { Fail "Could not reach GitHub:`n$($out | Out-String)" } | |
| git rev-parse -q --verify "origin/$branch" 2>$null | Out-Null | |
| $hasRemoteBranch = ($LASTEXITCODE -eq 0) | |
| if ($hasRemoteBranch) { | |
| $behind = [int](git rev-list --count "HEAD..origin/$branch") | |
| if ($behind -gt 0) { | |
| git merge -q --no-edit "origin/$branch" 2>&1 | Out-Null | |
| if ($LASTEXITCODE -ne 0) { | |
| git merge --abort 2>&1 | Out-Null | |
| Fail "GitHub has changes that conflict with yours. Nothing was pushed. Resolve with: git merge origin/$branch" | |
| } | |
| Write-Host "[OK] Merged $behind new commit(s) from GitHub" | |
| } else { | |
| Write-Host "[OK] Up to date with GitHub" | |
| } | |
| } | |
| if ($PullOnly) { Write-Host "`n[OK] Pull complete`n"; exit 0 } | |
| $ahead = 1 | |
| if ($hasRemoteBranch) { $ahead = [int](git rev-list --count "origin/$branch..HEAD") } | |
| if ($ahead -gt 0) { | |
| $out = git push -q origin "HEAD:$branch" 2>&1 | |
| if ($LASTEXITCODE -ne 0) { Fail "Push to GitHub failed:`n$($out | Out-String)" } | |
| Write-Host "[OK] GitHub updated" | |
| } else { | |
| Write-Host "[OK] GitHub already has everything" | |
| } | |
| # ---- 3. Hugging Face copy --------------------------------------------------- | |
| $hfRepo = git config --get hf.repo | |
| if ($NoHF -or -not $hfRepo) { | |
| if (-not $NoHF) { Write-Host "[--] No Hugging Face copy set (git config hf.repo datasets/<user>/<name>)" } | |
| Write-Host "`n[OK] Sync complete`n" | |
| exit 0 | |
| } | |
| if ($hfRepo -notmatch '^(datasets|spaces)/[^/]+/[^/]+$') { Fail "hf.repo must look like datasets/<user>/<name> or spaces/<user>/<name> (got '$hfRepo')" } | |
| $token = $env:HF_TOKEN | |
| if (-not $token) { $token = [Environment]::GetEnvironmentVariable('HF_TOKEN', 'User') } | |
| if (-not $token) { Fail "HF_TOKEN is not set" } | |
| function Hide([object]$Text) { ($Text | Out-String).Replace($token, '***') } | |
| if (git lfs ls-files 2>$null | Where-Object { $_ -match '^\S+ - ' }) { | |
| Fail "Some Git LFS files are not downloaded here. Run 'git lfs pull' first." | |
| } | |
| Write-Host "Updating Hugging Face ($hfRepo)..." | |
| $hfUser = $hfRepo.Split('/')[1] | |
| $hfUrl = "https://${hfUser}:$token@huggingface.co/$hfRepo" | |
| $cache = Join-Path $env:LOCALAPPDATA ('hf-sync\' + $hfRepo.Replace('/', '__')) | |
| # A clone of the Hugging Face repo, kept between runs. Its large files are not | |
| # downloaded, since every file is rewritten from this repo before committing. | |
| $env:GIT_LFS_SKIP_SMUDGE = '1' | |
| try { | |
| if (-not (Test-Path -LiteralPath (Join-Path $cache '.git'))) { | |
| $out = git -c credential.helper= -c core.longpaths=true clone -q $hfUrl $cache 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Remove-Item -LiteralPath $cache -Recurse -Force -ErrorAction SilentlyContinue | |
| Fail "Could not clone huggingface.co/$hfRepo`n$(Hide $out)" | |
| } | |
| git -C $cache remote set-url origin "https://huggingface.co/$hfRepo" | |
| } | |
| git -C $cache config core.longpaths true | |
| $out = git -C $cache -c credential.helper= fetch -q $hfUrl main 2>&1 | |
| if ($LASTEXITCODE -ne 0) { Fail "Could not fetch huggingface.co/$hfRepo`n$(Hide $out)" } | |
| git -C $cache reset -q --hard FETCH_HEAD | |
| git -C $cache clean -q -fdx | |
| } finally { | |
| Remove-Item Env:GIT_LFS_SKIP_SMUDGE -ErrorAction SilentlyContinue | |
| } | |
| # Delete files that no longer exist in this repo. | |
| $srcFiles = [string[]]@((git -c core.quotepath=off ls-files -z) -split "`0" | Where-Object { $_ -and $_ -ne '.gitattributes' }) | |
| $hfFiles = [string[]]@((git -C $cache -c core.quotepath=off ls-files -z) -split "`0" | Where-Object { $_ -and $_ -ne '.gitattributes' }) | |
| $srcSet = New-Object 'System.Collections.Generic.HashSet[string]' -ArgumentList (,$srcFiles) | |
| $gone = @($hfFiles | Where-Object { -not $srcSet.Contains($_) }) | |
| $listFile = Join-Path $cache '.git\sync-paths.txt' | |
| if ($gone.Count -gt 0) { | |
| [IO.File]::WriteAllText($listFile, ($gone -join "`n"), $Utf8NoBom) | |
| git -C $cache --literal-pathspecs rm -q --pathspec-from-file="$listFile" | |
| } | |
| # Write this repo's committed files into the clone. A temporary index keeps this | |
| # repo's own index untouched; Git LFS files come out with their real content. | |
| # The Hugging Face repo keeps its own .gitattributes. | |
| $attrPath = Join-Path $cache '.gitattributes' | |
| $hfAttr = $null | |
| if (Test-Path -LiteralPath $attrPath) { $hfAttr = [IO.File]::ReadAllBytes($attrPath) } | |
| $tmpIndex = Join-Path $cache '.git\sync-source-index' | |
| $env:GIT_INDEX_FILE = $tmpIndex | |
| try { | |
| git read-tree HEAD | |
| $out = git -c core.longpaths=true --work-tree="$cache" checkout-index -a -f 2>&1 | |
| $copied = ($LASTEXITCODE -eq 0) | |
| } finally { | |
| Remove-Item Env:GIT_INDEX_FILE -ErrorAction SilentlyContinue | |
| Remove-Item -LiteralPath $tmpIndex -ErrorAction SilentlyContinue | |
| } | |
| if (-not $copied) { Fail "Could not copy files for Hugging Face:`n$($out | Out-String)" } | |
| if ($hfAttr) { [IO.File]::WriteAllBytes($attrPath, $hfAttr) } | |
| elseif (Test-Path -LiteralPath $attrPath) { Remove-Item -LiteralPath $attrPath } | |
| # Copy Git LFS files from this repo's folder, where they are real files (checked | |
| # above). Git can write a placeholder instead, e.g. when .gitattributes starts with | |
| # a byte-order mark; everything is committed at this point, so the folder matches HEAD. | |
| foreach ($p in @(git -c core.quotepath=off lfs ls-files -n | Where-Object { $_ })) { | |
| try { [IO.File]::Copy((Join-Path $repoRoot $p), (Join-Path $cache $p), $true) } | |
| catch { Fail "Could not copy $p for Hugging Face: $($_.Exception.Message)" } | |
| } | |
| git -C $cache -c core.safecrlf=false -c core.longpaths=true add -A --force | |
| # Hugging Face rejects binary files and files over 10 MB unless they are in its | |
| # large-file storage, so track any such file that its .gitattributes doesn't cover. | |
| $needLfs = New-Object System.Collections.ArrayList | |
| foreach ($rec in ((git -C $cache -c core.quotepath=off ls-files -z --format='%(objectsize)|%(eolinfo:index)|%(path)') -split "`0")) { | |
| if (-not $rec) { continue } | |
| $f = $rec.Split('|', 3) | |
| if ($f[2] -eq '.gitattributes') { continue } | |
| if ($f[1] -eq '-text' -or [int64]$f[0] -gt 10MB) { [void]$needLfs.Add($f[2]) } | |
| } | |
| if ($needLfs.Count -gt 0) { | |
| Push-Location $cache | |
| foreach ($p in $needLfs) { git lfs track --filename "$p" | Out-Null } | |
| Pop-Location | |
| [IO.File]::WriteAllText($listFile, ($needLfs -join "`n"), $Utf8NoBom) | |
| git -C $cache --literal-pathspecs rm -q -f --cached --pathspec-from-file="$listFile" | |
| git -C $cache -c core.safecrlf=false -c core.longpaths=true add -A --force | |
| Write-Host "[OK] $($needLfs.Count) binary/large file(s) put in Hugging Face large-file storage" | |
| } | |
| Remove-Item -LiteralPath $listFile -ErrorAction SilentlyContinue | |
| # Safety check: every file of this repo must be in the Hugging Face copy. | |
| $hfNow = [string[]]@((git -C $cache -c core.quotepath=off ls-files -z) -split "`0" | Where-Object { $_ }) | |
| $hfSet = New-Object 'System.Collections.Generic.HashSet[string]' -ArgumentList (,$hfNow) | |
| $missing = @($srcFiles | Where-Object { -not $hfSet.Contains($_) }) | |
| if ($missing.Count -gt 0) { Fail "$($missing.Count) file(s) would be missing on Hugging Face (first: $($missing[0])). Nothing was pushed." } | |
| git -C $cache diff --cached --quiet | |
| if ($LASTEXITCODE -eq 0) { | |
| Write-Host "[OK] Hugging Face already up to date" | |
| } else { | |
| $sha = git rev-parse --short HEAD | |
| git -C $cache commit -q -m "Sync from GitHub ${sha}: $Message" | |
| $out = git -C $cache -c credential.helper= push -q $hfUrl HEAD:main 2>&1 | |
| if ($LASTEXITCODE -ne 0) { Fail "Push to Hugging Face failed:`n$(Hide $out)" } | |
| Write-Host "[OK] Hugging Face updated" | |
| } | |
| Write-Host "`n[OK] Sync complete`n" | |