Skip to content

Instantly share code, notes, and snippets.

@rfennell
Last active December 2, 2025 11:08
Show Gist options
  • Select an option

  • Save rfennell/76069e57e6d9134c46b9d5ec9e09f2b5 to your computer and use it in GitHub Desktop.

Select an option

Save rfennell/76069e57e6d9134c46b9d5ec9e09f2b5 to your computer and use it in GitHub Desktop.
A set of scripts to help export Visual Sourcesafe content to GitHub

Assumptions

Installed Tools

  • VSS2Git
  • GitHub CLI
  • Visual Souresafe CLI Client - assmed you must have this as you are using Sourcesafe via Visual Studio or similar

Step 1 - Build the list of VSS projects to export

  1. Run the VSSS CLI to export a list VSS projects (folders off the root of teh VSS server)
    # set the environment variable to find the VSS folder
    $env:SSDIR = "C:\VSS
    & "C:\Program Files (x86)\Microsoft Visual SourceSafe\ss.exe" dir
    Username: myuserid
    Password: ****
    $/:
    $Project 1
    $Project 2
    ...
  2. Copy the output text after the $/: and save it to a file (I uess this could also be scripted)
  3. Use the script create-projectfile.ps1 to convert this raw project data to a CSV with VSS source, target valid GitHub repo name and a control boolean

Step 2 - Migrate the content

  1. Authenticate with GitHub using the GH command using PAT token with the minimum scope repo:all admin:org
    & gh.exe" auth login
    ? Where do you use GitHub? GitHub.com
    ? What is your preferred protocol for Git operations on this host? HTTPS
    ? Authenticate Git with your GitHub credentials? Yes
    ? How would you like to authenticate GitHub CLI? Paste an authentication token
    Tip: you can generate a Personal Access Token here https://github.com/settings/tokens
    The minimum required scopes are 'repo', 'read:org', 'workflow'.
    ? Paste your authentication token: ****************************************
    - gh config set -h github.com git_protocol https
     Configured git protocol
     Logged in as mygithub
  2. Run export-vss2github.ps1 with suitable parameters. This will attempt to import each VSS project in turn via a local Gite repo that is deleted after themigration to save space.

Step 3 - Validate the migrations

  1. Set the VSS directory, user and password environment variables
    $env:SSDIR = "C:\VSS
    $env:SSUSER = myname
    $env:SSPWD = mypassword
    
  2. Use the check-vss2Gitexport.ps1 script to parse the VSS2Git log, state of the target GitHub repo and if there have been any updates to source VSS Db
param (
$projectfile = "projects.csv", # list of projects to export
$folderPath = "D:\Git1",
$searchText = "Git export complete in",
$analysisFile = "analysis.csv",
$org = "myorg",
$exportDate ="2025/11/13",
$failedProjectFile = "failedprojects.csv",
$outdatedProjectFile = "outdatedprojects.csv"
)
try {
write-host "Assumes gh auth login has been run" -ForegroundColor Green
write-host "Assumes (VSS) SS environment variables SSDIR, SSUSER & SSPWD have been set" -ForegroundColor Green
# Validate folder path
if (-not (Test-Path -Path $folderPath -PathType Container)) {
throw "Folder path '$folderPath' does not exist."
}
write-host "Load the list of projects to export from $projectfile"
$projectData = Import-Csv -Path $projectfile
# create the analysis file
Set-Content -Value "VSSProject,VSS2GITSuccess,Time,GitHubproject,GitHubSuccess,LastVSSCheckin,UpToDate" -path $folderPath\$analysisFile
$processed = 0
$processSuccessful = 0
$outofdate = 0
# create the failed and outdated project file
set-Content -Value "vss,gitrepo,migrate" -path $folderPath\$failedProjectFile
set-Content -Value "vss,gitrepo,migrate" -path $folderPath\$outdatedProjectFile
foreach ($project in $projectData) {
if (([string]::IsnullorEmpty($project.gitrepo) -eq $false) -and ($project.migrate -eq $true)) {
$source = $($project.vss)
$target = $($project.gitrepo)
# create the reprocess line in case it is needed
$reproccesrow = $source + "," + $target + ", true"
write-host "Checking VSS project $source'" -ForegroundColor Cyan
$processed ++
$file = "$folderPath\$target.log"
try {
write-host " Loading file $file"
$vss2gitsuccess = $false
$githubsuccess = $false
$exportupdatodate = $false
$time = '00:00:00'
# Read file content safely
$content = Get-Content -Path $file -ErrorAction Stop
# Search for the text
$line = $content | Where-Object { $_ -match [regex]::Escape($searchText) }
if ($line) {
# Extract the time using regex (format HH:MM:SS)
if ($line -match "\b\d{2}:\d{2}:\d{2}\b") {
$vsstime = $matches[0]
Write-Host " VSS2Git check for export OK from: $file in $vsstime"
$vss2gitsuccess = $true
} else {
Write-Output " VSS2Git Export OK, but no time found in the line." -ForegroundColor Red
}
} else {
Write-Host " VSS2Git no success message in: $file" -ForegroundColor Red
}
}
catch {
Write-Host " Error reading file: $file - $_" -ForegroundColor Red
}
# Check if the repository is empty (no default branch or no commits)
try {
# Get the default branch name
write-host " Checking GitHub for $org/$target"
$defaultBranch = & "D:\gh-cli\gh" repo view "$org/$target" --json defaultBranchRef -q ".defaultBranchRef.name" 2>$null
if (-not $defaultBranch) {
Write-Host " GitHub Repository $org/$target exists but is empty (no default branch)." -ForegroundColor Yellow
} elseif ( $defaultBranch -ne "main") {
Write-Host " GitHub Repository $org/$target exists but has wrong branch name ($defaultBranch)." -ForegroundColor Yellow
} else {
Write-Host " Gihub Repository $org/$target exists and has content."
$githubsuccess = $true
}
}
catch {
Write-Host " Error checking repository content: $_" -ForegroundColor Red
}
# Check if more recent VSS updates
try {
# Get the default branch name
write-host " Checking for last VSS update for $source"
$ssresult = & 'C:\Program Files (x86)\Microsoft Visual SourceSafe\ss.exe' history "$source" -#1 -r
$rows = $ssresult.Split("`n")
$daterow = ""
foreach ($r in $rows) {
if ($r.contains("Date:")) {
$daterow = $r
break
}
}
if ($daterow -match 'Date:\s*(\d{1,2}/\d{1,2}/\d{2})\s*Time:\s*(\d{1,2}:\d{2})') {
$date = $matches[1] # first capture group -> 15/08/17
$time = $matches[2] # second capture group -> 9:22
# Combine date and time into a single variable
$datetimeObject = [datetime]::ParseExact("$date $time", "d/MM/yy H:mm", $null)
# Check for updates
write-host (" Last VSS update $datetimeObject")
if ([datetime]$exportDate -gt $datetimeObject) {
Write-Host " Export up to date, no changes changes in VSS since $($datetimeObject.ToString("d/MM/yy H:mm")) " -ForegroundColor Green
$exportupdatodate = $true
} else {
Write-Host " Export out of date, newest changes in VSS was at $($datetimeObject.ToString("d/MM/yy H:mm"))" -ForegroundColor Yellow
$outofdate ++
}
} else {
Write-Host " Date and time not found for last VSS export " -ForegroundColor Red
}
}
catch {
Write-Host " Error checking repository content: $_" -ForegroundColor Red
}
if ($vss2gitsuccess -and $githubsuccess) {
$processSuccessful ++
} else {
add-Content -Value $reproccesrow -path $folderPath\$failedProjectFile
}
if ($exportupdatodate -eq $false) {
add-Content -Value $reproccesrow -path $folderPath\$outdatedProjectFile
}
Add-Content -Value "$source,$vss2gitsuccess,$vsstime,$target,$githubsuccess,$($datetimeObject.ToString("d/MM/yy H:mm")),$exportupdatodate" -path $folderPath\$analysisFile -Force
}
}
Write-host "Processed: $processed"
write-host "Processing Unsuccessful: $($processed - $processSuccessful)"
write-host "Processing Successful: $processSuccessful"
write-Host "VSS Export Out of Date: $outofdate"
}
catch {
Write-Host "Script error: $_" -ForegroundColor Red
}
param(
$projectfile = "projects1.csv", # list of projects to export
$vssdirfile = "rawvssdir.txt" # dir of the root vss folder
)
write-host "Load the list of projects to exported from $vssdirfile"
$projectData = get-content -Path $vssdirfile
set-Content $projectfile -Value "vss,gitrepo,migrate"
foreach ($project in $projectData) {
$vsspath = $project.replace("$" , "$/")
$gitpath = $project.tolower().replace("&" , "").replace(" " , "-").replace("_" , "-")
$row = $vsspath + "," + $gitpath + ", true"
$row
add-Content $projectfile -Value $row
}
write-host "Written the list of projects to exportwd from $projectfile"
param(
$projectfile = "projects.csv", # list of projects to export
$vssbasedirectory = "D:\VSS", # where is the VSS DB
$paramfile = "params.txt", # CLI export settings, generated for each project
$org = "myorg", # GHE org
$gitbasedirectory = "D:\Git1", # tmp folder for local git repos
$replaceghrepo = $true, # replaces the ll content in
$deletelocalrepo = $false # tidy the local storage
)
function export-folder {
param(
$vssbasedirectory,
$vsssourecfolder ,
$gitbasedirectory ,
$gitrepo,
$paramsfile
)
# create param file
$multiLineText = @"
VSS_DIRECTORY=$vssbasedirectory
VSS_PROJECT=$vsssourecfolder
VSS_EXCLUDE_PATHS=
ENCODING=1252
GIT_DIRECTORY=$gitbasedirectory\$gitrepo
DEFAULT_EMAIL_DOMAIN=forterro.com
DEFAULT_COMMENT=
LOG_FILE=$gitbasedirectory\$gitrepo.log
TRANSCODE_COMMENTS=True
FORCE_ANNOTATED_TAGS=True
IGNORE_GIT_ERRORS=True
ANY_COMMENT_SECONDS=15
SAME_COMMENT_SECONDS=600
"@
$multiLineText | Out-File -FilePath $paramsfile
}
write-host "Assumes gh auth login has been run" -ForegroundColor Green
write-host "Load the list of projects to export from $projectfile"
$projectData = Import-Csv -Path $projectfile
foreach ($project in $projectData) {
if (([string]::IsnullorEmpty($project.gitrepo) -eq $false) -and ($project.migrate -eq $true)) {
$source = $($project.vss)
$target = $($project.gitrepo)
write-host "Exporting $source' to '$target'"
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
if (Test-Path $gitbasedirectory\$target) {
Write-Error "The export folder $gitbasedirectory\$target already exists"
} else {
write-host "Create VSS2Git $paramfile file"
export-folder -vsssourecfolder $source -gitrepo $target -paramsfile $paramfile -gitbasedirectory $gitbasedirectory -vssbasedirectory $vssbasedirectory
write-host "Export VSS to Git"
& .\Vss2Git.exe $paramfile
write-host "Add .gitignore to $gitbasedirectory\$target"
copy-item ".gitignore" $gitbasedirectory\$target
cd $gitbasedirectory\$target
$folder = $source.replace("$/","")
$extraCommitMessage = ""
if (Test-Path $folder) {
write-host "There is folder with same name as the repo, move it up one level from the $folder subfolder to correct structure"
Move-Item -path $folder\* -Destination .\
remove-item $folder -Recurse -Force
$extraCommitMessage = "and corrected folder structure"
}
write-host "Commit the changes"
git add -A
git commit -m "Added .gitignore $extraCommitMessage "
write-host "Set branch name"
git branch -m main
write-host "Set config values to avoid memory issues"
git config pack.threads 1
git config pack.windowsMemory 50m
git config http.postBuffer 524288000
if ($replaceghrepo) {
write-host "Remove the existing GitHub repository"
& "D:\gh-cli\gh.exe" repo delete $org/$target --yes
}
write-host "Create remote repo $org/$target and push the current repo"
& "D:\gh-cli\gh.exe" repo create $org/$target --internal --source=. --remote=origin --push
cd D:\vss2git
if ($deletelocalrepo) {
write-host "Removing local temporary Git repo"
Remove-Item -Path $gitbasedirectory\$target -Recurse -Force
} else {
write-host "Skipping delete of local temporary Git repo"
}
}
$stopwatch.Stop()
Write-Host "Export Time: $($stopwatch.Elapsed.ToString())" -ForegroundColor Green
} else {
write-host "Skipping '$($project.vss)' as either no git repo name, or migrate flag not set"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment