Last active
March 13, 2021 05:58
-
-
Save shadowmoose/7b35dea71f996f70d3ed51f794bd8757 to your computer and use it in GitHub Desktop.
Gource git history into MP4 format - Windows Powershell
This file contains 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
# When run (in PowerShell), this script prompts for a directory containing a git project. | |
# It then jumps into that directory and uses Gource + FFmpeg to generate an mp4 animation of the history of the current branch. | |
# Additionally, it has support for embedding background music files. Click "Cancel" on the prompt to not add audio. | |
# If the video is shorter than the selected audio, the audio will fade out. If longer, the video will be sped up to fit the audio. | |
# | |
# Note: If the complementary script "handle_avatars.ps1" is present, this script will use it to download user avatars from GitHub. | |
# This may hit rate limiting, so ClientID and ClientSecret oAuth params are accepted, which will bypass GitHub's limits. | |
# You can cut down on the required queries by properly using a ".mailmap" file to combine user's emails. | |
# Pass "SkipGithubAvatars" to ignore this entirely, if your project is not hosted on GitHub. | |
# | |
# Gource (http://gource.io/) and FFmpeg MUST be installed for this to work! | |
# Last updated by ShadowMoose on 07/10/2018 | |
param( | |
[string] $ClientID, | |
[string] $ClientSecret, | |
[switch] $SkipGithubAvatars | |
) | |
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null | |
[System.Windows.Forms.Application]::EnableVisualStyles() | |
$ErrorActionPreference = "Stop" | |
$initial_path = (Get-Item -Path ".\").FullName | |
function Get-BrowseLocation{ | |
$browse = New-Object System.Windows.Forms.FolderBrowserDialog | |
$browse.RootFolder = [System.Environment+SpecialFolder]'MyComputer' | |
$browse.ShowNewFolderButton = $false | |
$browse.Description = "Choose a directory containing a Git project" | |
$loop = $true | |
while($loop) | |
{ | |
if ($browse.ShowDialog() -eq "OK") | |
{ | |
$loop = $false | |
} else | |
{ | |
$res = [System.Windows.Forms.MessageBox]::Show("You clicked Cancel. Try again or exit script?", "Choose a directory", [System.Windows.Forms.MessageBoxButtons]::RetryCancel) | |
if($res -eq "Cancel") | |
{ | |
#End script | |
return | |
} | |
} | |
} | |
$browse.SelectedPath | |
$browse.Dispose() | |
} | |
Function Get-FileName($initialDirectory){ | |
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null | |
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog | |
$OpenFileDialog.Title = "Choose an audio file to include. Cancel for no music:" | |
$OpenFileDialog.initialDirectory = $initialDirectory | |
$OpenFileDialog.ShowDialog() | Out-Null | |
$OpenFileDialog.filename | |
} | |
$targ = Get-BrowseLocation | |
if(-not $targ){ | |
exit | |
} | |
Write-Host "Select an audio file to include as music, or cancel for none." | |
$music_file = Get-FileName "C:\temp" | |
if($music_file){ | |
Write-Host "Using $($music_file) music file." | |
}else{ | |
Write-Host "Not including music file." | |
} | |
Write-Host "Path: $targ" | |
Set-Location -Path "$targ" | |
[Environment]::CurrentDirectory = $PWD | |
$avatarDir = "./" | |
if (-not $SkipGithubAvatars -and (Test-Path "$($initial_path)\handle_avatars.ps1")){ | |
Write-Host "Downloading user avatars from GitHub, if applicable..." -ForegroundColor green | |
$args = '' | |
if($ClientID){ | |
$args="-ClientID $($ClientID) -ClientSecret $($ClientSecret)" | |
} | |
Invoke-Expression "$($initial_path)\handle_avatars.ps1 $($args)" | |
$avatarDir = "./.git/avatar/" | |
} | |
Clear-Host | |
Write-Host "Generating the initial video file using Gource..." -ForegroundColor green | |
Write-Host "(Try to not interact with the Gource window while it runs!)" | |
Write-Host "" | |
$logfile ='./_gource_log.txt' | |
git log --no-walk --tags --reverse --encoding=UTF-8 --pretty="%at|Release %D" > $logfile | |
$text = [IO.File]::ReadAllText($logfile) -replace "`r`n", "`n" -replace "tag: ", "" | |
[IO.File]::WriteAllText($logfile, $text) | |
& cmd /c "gource -1920x1080 --hide mouse,progress --highlight-all-users --highlight-dirs --seconds-per-day 2.5 --output-framerate 60 --dir-name-depth 10 --auto-skip-seconds 1.5 --user-image-dir $avatarDir --key --caption-file $logfile --caption-size 24 --caption-colour 00BFFF -caption-duration 2.5 --output-ppm-stream - | ffmpeg -y -r 60 -f image2pipe -vcodec ppm -i - -vcodec mp4 -qscale 0 -movflags faststart -vcodec libx264 -crf 16 _gource_out.mp4" | |
Remove-Item -Path $logfile -Confirm:$false -Force | |
Clear-Host | |
$outputFile = Split-Path $targ -leaf | |
$outputFile = "$($outputFile)_Gource.mp4" | |
if(-not $music_file){ | |
if (Test-Path "$($outputFile)"){ | |
Remove-Item -Path "$($outputFile)" -Confirm:$false -Force | |
} | |
Rename-Item -Path "_gource_out.mp4" -NewName "$($outputFile)" | |
Clear-Host | |
}else{ | |
Write-Host "PREPARING BACKGROUND MUSIC..." -ForegroundColor green | |
$vid_len = ((ffprobe -v error -select_streams v:0 -show_entries stream=duration -of default=noprint_wrappers=1:nokey=1 "_gource_out.mp4") -as [double]) | |
$aud_len = ((ffprobe -v error -select_streams a:0 -show_entries stream=duration -of default=noprint_wrappers=1:nokey=1 $music_file)-as [double]) | |
$minLen = ([math]::min( $vid_len, $aud_len )) | |
if($minLen -lt $aud_len){ | |
ffmpeg -y -i $music_file -t $minLen -af "afade=t=out:st=$($minLen - 3):d=3" "./_tmp_gource_music.mp3" | |
$aud_len = $minLen | |
Clear-Host | |
Write-Host "ADDING TRIMMED BACKGROUND MUSIC..." -ForegroundColor green | |
ffmpeg -y -i "./_gource_out.mp4" -i "./_tmp_gource_music.mp3" -movflags faststart -codec copy -shortest $outputFile | |
}else{ | |
ffmpeg -y -i $music_file "./_tmp_gource_music.mp3" | |
Clear-Host | |
Write-Host "COMPRESSING + ADDING BACKGROUND MUSIC..." -ForegroundColor green | |
ffmpeg -y -i "./_gource_out.mp4" -i "./_tmp_gource_music.mp3" -filter:v "setpts=$($aud_len)/$($vid_len)*PTS" -movflags faststart $outputFile | |
} | |
Clear-Host | |
Remove-Item -Path "./_tmp_gource_music.mp3" -Confirm:$false -Force | |
Remove-Item -Path "./_gource_out.mp4" -Confirm:$false -Force | |
} | |
if (Test-Path ".delete_tmp_mailmap"){ | |
Remove-Item -Path ".delete_tmp_mailmap" -Confirm:$false -Force | |
Remove-Item -Path ".mailmap" -Confirm:$false -Force | |
} | |
Clear-Host | |
Set-Location -Path "$initial_path" | |
Write-Host "Done!" |
This file contains 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
# Run this PowerShell script within a GitHub project dirctory to download each Contributor's Avatar. | |
# The script will create a copy of the avatar for the latest known alias they've contributed under. | |
# If one does not exist, it will also generate a ".mailmap" list, to allow all Git queries to properly map users. | |
# The main (only?) use for this script, is generating Avatars for Gource rendering. | |
# This script will make one request to the GitHub API per project Contributor Email, so you may get rate-limited without Client authentication. | |
# If you have a '.mailmap' file set up to remap user emails, it will respect those mappings and only lookup the mapped email addresses. | |
# | |
# PARAMS: | |
# +Supports client ID & Secret for GitHub API authentication. | |
# +Also acceps a custom download directory. | |
param( | |
[string] $ClientID, | |
[string] $ClientSecret, | |
[string] $OutputDir | |
) | |
$ErrorActionPreference = "Stop" | |
[Net.ServicePointManager]::SecurityProtocol = "Tls12, Tls11, Tls, Ssl3" | |
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") | |
[Environment]::CurrentDirectory = $PWD | |
if(-not $OutputDir){ | |
$OutputDir = './.git/avatar/' | |
}else{ | |
$OutputDir += '/' | |
} | |
$github_auth = "client_id=$($ClientID)&client_secret=$($ClientSecret)" | |
if(-not $ClientID){ | |
$github_auth="" | |
} | |
$g_url=git config --get remote.origin.url | |
Write-Host $g_url | |
if(-not $g_url -like "*//github.com/*"){ | |
Write-Host "This is not a GitHub project!" -ForegroundColor red | |
exit 1 | |
} | |
$g_split = $g_url.split('/') | |
$gh_user = $g_split[3] | |
$gh_repo = $g_split[4].replace('.git', '') | |
$github_url="https://api.github.com/repos/$($gh_user)/$($gh_repo)" | |
Write-Host "GITHUB URL: $($github_url)" | |
# $user_info_str=git log --no-walk --pretty=format:"%an,%ae" | |
# Write-Host "$github_url/contributors?$($github_auth)" | |
$build_info=(git --no-pager log --pretty="%aN,%H,%aE").split([Environment]::NewLine) | |
$usernames = @{ } | |
$github = @( ) | |
Foreach ($u IN $build_info){ | |
#Write-Host $u | |
$username,$sha,$email=$u.split(',') | |
$found = 0 | |
Foreach($u IN $github){ | |
if($u.emails.Contains($email)){ | |
$found = 1 | |
if(-not $u.aliases.Contains($username)){ | |
$u.aliases += $username | |
Write-Host "`tUsername '$username' is an alias of User $($u.id)" | |
} | |
} | |
} | |
if($found){ | |
continue | |
} | |
Try{ | |
Write-Host "Looking up username: $($username) [$email]" | |
$lookup=Invoke-RestMethod -Uri "$($github_url)/commits/$($sha)?$($github_auth)" | |
}Catch{ | |
continue | |
} | |
# $alias = $lookup.commit.author.name | |
$image = $lookup.author.avatar_url | |
$aid = $lookup.author.id | |
$exists = 0 | |
Foreach($g in $github){ | |
if($g.id -eq $aid){ | |
$g.aliases += $username | |
$g.emails += $email | |
$exists = 1 | |
Write-Host "`tUsername '$username' is an alias of User $($g.id)." | |
} | |
} | |
if(-not $exists){ | |
$github+= @{"id"=$aid;"image"=$image;"aliases"=@($username);"latest"=$username;"emails"=@($email)} | |
} | |
} | |
Write-Host | |
Write-Host "GitHub Users: $(ConvertTo-Json $github)" -ForegroundColor green | |
Write-Host | |
Write-Host "DOWNLOADING AVATAR IMAGES:" -ForegroundColor green | |
New-Item -ItemType Directory -Force -Path "$($OutputDir)" | out-null | |
# Build .mailmap file if it doesn't already exist. | |
$mailmap = (Test-Path ".mailmap") | |
if(-not $mailmap){ | |
$used_emails = @() | |
Foreach($u IN $github){ | |
Foreach($e in $u.emails){ | |
if(-not $used_emails.Contains($e)){ | |
$used_emails+=$e | |
Add-Content ".mailmap" "$($u.latest) <$($u.emails[0])> <$($e)>" | |
} | |
} | |
$u.aliases=@($u.latest) | |
} | |
# Signal (via file) that this mailmap should be temporary. | |
Add-Content ".delete_tmp_mailmap" "true" | |
} | |
Foreach($u IN $github){ | |
Write-Host "`tDownloading Avatar for '$($u.aliases[0])'..." -ForegroundColor yellow | |
$tmp_img = ("$($OutputDir)_tmp_img.png") | |
$ttmp = $tmp_img+'.tmp' | |
Invoke-WebRequest -Uri $u.image -OutFile $ttmp | |
$img=[Drawing.Image]::FromFile($ttmp) | |
$img.Save($tmp_img, 'png') | |
$img.Dispose() | |
Remove-Item -Path $ttmp | |
Foreach($al in $u.aliases){ | |
Write-Host "`t`t+Saving for alias '$($al)'." -ForegroundColor yellow | |
Copy-Item $tmp_img -Destination "$($OutputDir)$($al).png" | |
} | |
Remove-Item -Path $tmp_img | |
Write-Host "`tSaved." -ForegroundColor green | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment