Skip to content

Instantly share code, notes, and snippets.

@shadowmoose
Last active March 13, 2021 05:58
Show Gist options
  • Save shadowmoose/7b35dea71f996f70d3ed51f794bd8757 to your computer and use it in GitHub Desktop.
Save shadowmoose/7b35dea71f996f70d3ed51f794bd8757 to your computer and use it in GitHub Desktop.
Gource git history into MP4 format - Windows Powershell
# 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!"
# 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