NoGit.psm1

#Region '.\Private\Get-NoGitHubRepoRecursiveContents.ps1' -1

function Get-NoGitHubRepoRecursiveContents {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]
        $Url,

        [string]
        $RelativePath = '',

        [Parameter(Mandatory)]
        [hashtable]
        $Headers,

        [Parameter(Mandatory)]
        [string]
        $TargetDir,

        [Parameter(Mandatory)]
        [string]
        $Branch
    )

    try {
        $items = Invoke-RestMethod -Uri $Url -Headers $Headers -ErrorAction Stop -Verbose:$false
        $count = @($items).Count
        Write-Verbose "Fetching: $Url ($count item(s))"
    }
    catch {
        Write-Error "Error fetching contents from: $Url - $($_.Exception.Message)"
        $script:FailCount++
        return
    }

    foreach ($item in $items) {
        $ItemPath = if ($RelativePath) { "$RelativePath\$($item.name)" } else { $item.name }

        if ($item.type -eq 'file') {
            $DownloadUrl = $item.download_url
            $OutputPath = Join-Path -Path $TargetDir -ChildPath $ItemPath
            $OutputDir = Split-Path -Path $OutputPath -Parent

            try {
                if (-not (Test-Path -Path $OutputDir)) {
                    New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
                }

                Invoke-WebRequest -Uri $DownloadUrl -Headers $Headers -OutFile $OutputPath -ErrorAction Stop -Verbose:$false
                Write-Verbose "Downloaded: $ItemPath"
                $script:SuccessCount++
            }
            catch {
                Write-Error "Failed: $ItemPath - $($_.Exception.Message)"
                $script:FailCount++
            }
        }
        elseif ($item.type -eq 'dir') {
            $UrlForItem = if ($item.url -like '*?ref=*') { $item.url } else { "$($item.url)?ref=$Branch" }

            $childParams = @{
                Url          = $UrlForItem
                RelativePath = $ItemPath
                Headers      = $Headers
                TargetDir    = $TargetDir
                Branch       = $Branch
            }

            Get-NoGitHubRepoRecursiveContents @childParams
        }
    }
}
#EndRegion '.\Private\Get-NoGitHubRepoRecursiveContents.ps1' 72
#Region '.\Public\Get-NoGitHubRepoContents.ps1' -1

function Get-NoGitHubRepoContents {
    <#
    .SYNOPSIS
    Recursively downloads the contents of a GitHub repository to a local directory using the GitHub REST API.
 
    .DESCRIPTION
    This function connects to GitHub using a personal access token (PAT) to authenticate and recursively downloads all
    files and folders from a specified repository and branch. It supports downloading the entire content tree of the
    repository and saves it to a designated local directory.
 
    It uses GitHub's REST API (`/repos/:owner/:repo/contents`) to fetch files and handles directories by recursion.
 
    .PARAMETER Token
    A GitHub Personal Access Token (PAT) with appropriate repository access permissions.
    This token is used for authentication with the GitHub API.
 
    You can create a token at: https://github.com/settings/tokens
 
    .PARAMETER Owner
    The username or organization name that owns the GitHub repository.
    This is part of the GitHub repository URL. For example:
 
        GitHub URL: https://github.com/microsoft/PowerToys
                                       ^^^^^^^^^
                                       This is the Owner
 
    .PARAMETER Repo
    The name of the repository to download contents from.
    This is also part of the GitHub repository URL. For example:
 
        GitHub URL: https://github.com/microsoft/PowerToys
                                                 ^^^^^^^^
                                                 This is the Repo
 
    .PARAMETER Branch
    (Optional) The name of the branch to download from.
    Defaults to 'main' if not specified.
 
    .PARAMETER TargetDir
    The local directory path where the contents of the GitHub repository will be downloaded.
    If the directory does not exist, it will be created.
 
    .EXAMPLE
    Get-NoGitHubRepoContents -Token 'ghp_xxx' -Owner 'microsoft' -Repo 'PowerToys' -TargetDir 'C:\Repos\PowerToys'
 
    Downloads the contents of the `PowerToys` repository from the `microsoft` organization on the `main` branch and saves them into `C:\Repos\PowerToys`.
 
    .EXAMPLE
    Get-NoGitHubRepoContents -Token 'ghp_abc' -Owner 'kevinblumenfeld' -Repo 'PS7' -Branch 'dev' -TargetDir 'D:\Temp\PS7Dev'
 
    Downloads the contents of the `PS7` repository from the `kevinblumenfeld` account on the `dev` branch into `D:\Temp\PS7Dev`.
 
    .NOTES
    Author: Your Name
    Required: PowerShell 7.0+
    GitHub API Rate Limits apply (even for authenticated requests). For large repositories, use cautiously.
 
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]
        $Token,

        [Parameter(Mandatory)]
        [string]
        $Owner,

        [Parameter(Mandatory)]
        [string]
        $Repo,

        [string]
        $Branch = 'main',

        [Parameter(Mandatory)]
        [string]
        $TargetDir
    )

    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

    $headers = @{
        Authorization = "token $Token"
        'User-Agent'  = $Owner
    }

    if (-not (Test-Path -Path $TargetDir)) {
        try {
            New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null
            Write-Verbose "Created directory: $TargetDir"
        }
        catch {
            Write-Error ("Failed to create directory: $TargetDir - {0}" -f $_.Exception.Message)
            return
        }
    }

    $script:SuccessCount = 0
    $script:FailCount = 0

    $ApiUrl = "https://api.github.com/repos/$Owner/$Repo/contents"
    $initialParams = @{
        Url       = "${ApiUrl}?ref=${Branch}"
        Headers   = $headers
        TargetDir = $TargetDir
        Branch    = $Branch
    }

    Get-NoGitHubRepoRecursiveContents @initialParams

    $stopwatch.Stop()

    $elapsed = $stopwatch.Elapsed
    $formattedTime = '{0:D2}:{1:D2}:{2:D2}' -f $elapsed.Hours, $elapsed.Minutes, $elapsed.Seconds

    Write-Verbose "--- Summary for $Owner/$Repo ---"
    Write-Verbose ("Success : {0}" -f $script:SuccessCount)
    Write-Verbose ("Fail : {0}" -f $script:FailCount)
    Write-Verbose ("OutputDir : {0}" -f $TargetDir)
    Write-Verbose ("Elapsed : {0}" -f $formattedTime)
    
}
#EndRegion '.\Public\Get-NoGitHubRepoContents.ps1' 124
#Region '.\Public\Get-NoGitHubRepoTreeContents.ps1' -1

function Get-NoGitHubRepoTreeContents {
    <#
    .SYNOPSIS
        Downloads files from a GitHub repository using the Git Trees API.
 
    .DESCRIPTION
        Uses the Git Trees API to recursively traverse a repository tree
        and download all blob (file) entries to a local directory.
 
        Handles SHA resolution, recursive trees, blob retrieval, and writes
        file contents to disk. Skips directories and submodules.
 
    .PARAMETER Token
        The GitHub Personal Access Token (PAT).
 
    .PARAMETER Owner
        The repository owner (user or organization).
 
    .PARAMETER Repo
        The name of the repository.
 
    .PARAMETER Branch
        The branch to download (default: main).
 
    .PARAMETER TargetDir
        Directory to save the files to.
 
    .EXAMPLE
        Get-NoGitHubRepoTreeContents -Token 'abc' -Owner 'octocat' -Repo 'Hello-World' -TargetDir './repo'
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]
        $Token,

        [Parameter(Mandatory)]
        [string]
        $Owner,

        [Parameter(Mandatory)]
        [string]
        $Repo,

        [Parameter()]
        [string] 
        $Branch = 'main',

        [Parameter(Mandatory)]
        [string]
        $TargetDir
    )

    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

    $headers = @{
        Authorization = "token $Token"
        'User-Agent'  = 'NoGit'
        Accept        = 'application/vnd.github+json'
    }

    if (-not (Test-Path -Path $TargetDir)) {
        New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null
    }

    # Step 1: Get commit SHA from branch reference
    $refUrl = "https://api.github.com/repos/$Owner/$Repo/git/refs/heads/$Branch"
    try {
        $refResponse = Invoke-RestMethod -Uri $refUrl -Headers $headers
        $commitUrl = $refResponse.object.url
    }
    catch {
        Write-Error "Failed to resolve branch '$Branch'. Verify that it exists."
        return
    }

    # Step 2: Get commit object to find tree SHA
    $commitResponse = Invoke-RestMethod -Uri $commitUrl -Headers $headers
    $treeSha = $commitResponse.tree.sha

    # Step 3: Get tree recursively
    $treeUrl = "https://api.github.com/repos/$Owner/$Repo/git/trees/${treeSha}?recursive=1"
    $treeResponse = Invoke-RestMethod -Uri $treeUrl -Headers $headers

    if ($treeResponse.truncated -eq $true) {
        Write-Warning "Tree listing was truncated. Not all files may be downloaded."
    }

    $script:SuccessCount = 0
    $script:FailCount = 0

    foreach ($entry in $treeResponse.tree) {
        if ($entry.type -ne 'blob') { continue }

        $outputPath = Join-Path -Path $TargetDir -ChildPath $entry.path
        $outputDir = Split-Path -Path $outputPath -Parent

        if (-not (Test-Path -Path $outputDir)) {
            New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
        }

        try {
            $blobUrl = "https://api.github.com/repos/$Owner/$Repo/git/blobs/$($entry.sha)"
            $blobHeaders = $headers.Clone()
            $blobHeaders['Accept'] = 'application/vnd.github.v3.raw'

            Invoke-WebRequest -Uri $blobUrl -Headers $blobHeaders -OutFile $outputPath -Verbose:$false
            Write-Verbose "Downloaded: $($entry.path)"
            $script:SuccessCount++
        }
        catch {
            Write-Error "Failed to download: $($entry.path) - $_"
            $script:FailCount++
        }
    }

    $stopwatch.Stop()

    $elapsed = $stopwatch.Elapsed
    $formattedTime = '{0:D2}:{1:D2}:{2:D2}' -f $elapsed.Hours, $elapsed.Minutes, $elapsed.Seconds

    Write-Verbose "--- Summary for $Owner/$Repo ---"
    Write-Verbose ("Success : {0}" -f $script:SuccessCount)
    Write-Verbose ("Fail : {0}" -f $script:FailCount)
    Write-Verbose ("OutputDir : {0}" -f $TargetDir)
    Write-Verbose ("Elapsed : {0}" -f $formattedTime)
    
}
#EndRegion '.\Public\Get-NoGitHubRepoTreeContents.ps1' 129