Last active
September 23, 2025 07:27
-
-
Save guinetik/f8be35b0cc3eb618f86245038e9e628a to your computer and use it in GitHub Desktop.
clone stuff with PAT
This file contains hidden or 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
| <# | |
| .SYNOPSIS | |
| Clona repositórios do GitHub baseado em critérios de pesquisa usando um Personal Access Token (PAT). | |
| .DESCRIPTION | |
| Este script utiliza a API do GitHub para buscar repositórios que correspondem aos critérios especificados | |
| e os clona localmente. Por padrão, força a busca apenas por repositórios privados, mas pode ser | |
| customizado através do parâmetro Query. | |
| .PARAMETER Pat | |
| Personal Access Token do GitHub com permissões de acesso aos repositórios. | |
| Para gerar um PAT, acesse: https://github.com/settings/tokens | |
| .PARAMETER Query | |
| Critério de busca para filtrar repositórios. Suporta a sintaxe de pesquisa do GitHub. | |
| Exemplos: | |
| - "language:python" | |
| - "org:minha-org language:javascript" | |
| - "topic:machine-learning" | |
| .PARAMETER OutDir | |
| Diretório onde os repositórios serão clonados. Padrão: ".\clones" | |
| .PARAMETER Proxy | |
| URL do servidor proxy HTTP/HTTPS (opcional). | |
| Formato: "http://proxy.empresa.com:8080" | |
| .PARAMETER IncludePublic | |
| Inclui repositórios públicos na busca. Por padrão, apenas repositórios privados são considerados. | |
| .PARAMETER MaxRepos | |
| Número máximo de repositórios a serem clonados. Padrão: ilimitado. | |
| .PARAMETER Verbose | |
| Exibe informações detalhadas durante a execução. | |
| .EXAMPLE | |
| .\Clone-GitHubRepos.ps1 -Pat "ghp_xxxxxxxxxxxxxxxxxxxx" -Query "language:python" | |
| .EXAMPLE | |
| .\Clone-GitHubRepos.ps1 -Pat "ghp_xxxxxxxxxxxxxxxxxxxx" -Query "org:minha-empresa" -OutDir "C:\repos" -IncludePublic | |
| .EXAMPLE | |
| .\Clone-GitHubRepos.ps1 -Pat "ghp_xxxxxxxxxxxxxxxxxxxx" -Query "topic:api" -Proxy "http://proxy:8080" -ProxyUser "usuario" -ProxyPassword (ConvertTo-SecureString "senha" -AsPlainText -Force) -MaxRepos 10 | |
| .NOTES | |
| Versão: 2.0 | |
| Autor: Guinetik 🚀 | |
| Data: 2025-09-23 | |
| Requisitos: | |
| - PowerShell 5.1 ou superior | |
| - Git instalado e configurado | |
| - Acesso à internet (ou proxy configurado) | |
| - PAT com permissões adequadas | |
| .LINK | |
| https://docs.github.com/en/rest/search#search-repositories | |
| #> | |
| [CmdletBinding(SupportsShouldProcess)] | |
| param( | |
| [Parameter(Mandatory = $true, HelpMessage = "Personal Access Token do GitHub")] | |
| [ValidateNotNullOrEmpty()] | |
| [string] $Pat, | |
| [Parameter(Mandatory = $true, HelpMessage = "Critério de busca para repositórios")] | |
| [ValidateNotNullOrEmpty()] | |
| [string] $Query, | |
| [Parameter(HelpMessage = "Diretório de destino para clones")] | |
| [ValidateNotNullOrEmpty()] | |
| [string] $OutDir = ".\clones", | |
| [Parameter(HelpMessage = "URL do servidor proxy (opcional)")] | |
| [string] $Proxy = "", | |
| [Parameter(HelpMessage = "Usuário para autenticação do proxy")] | |
| [string] $ProxyUser = "", | |
| [Parameter(HelpMessage = "Senha para autenticação do proxy")] | |
| [Security.SecureString] $ProxyPassword, | |
| [Parameter(HelpMessage = "Incluir repositórios públicos na busca")] | |
| [switch] $IncludePublic, | |
| [Parameter(HelpMessage = "Número máximo de repositórios a clonar")] | |
| [ValidateRange(1, 1000)] | |
| [int] $MaxRepos = [int]::MaxValue | |
| ) | |
| #Requires -Version 5.1 | |
| # Configurações e constantes | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = 'Stop' | |
| $script:CONFIG = @{ | |
| ApiBaseUrl = "https://api.github.com" | |
| PerPage = 100 | |
| MaxRetries = 3 | |
| RetryDelay = 2 | |
| TimeoutSeconds = 30 | |
| UserAgent = "PowerShell-GitHub-Cloner/2.0" | |
| } | |
| # Cores para output | |
| $script:COLORS = @{ | |
| Info = 'Cyan' | |
| Success = 'Green' | |
| Warning = 'Yellow' | |
| Error = 'Red' | |
| Progress = 'Magenta' | |
| } | |
| #region Funções Auxiliares | |
| # Definir constantes para SetThreadExecutionState | |
| Add-Type -TypeDefinition @" | |
| using System; | |
| using System.Runtime.InteropServices; | |
| public class PowerManagement { | |
| [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] | |
| public static extern uint SetThreadExecutionState(uint esFlags); | |
| public const uint ES_CONTINUOUS = 0x80000000; | |
| public const uint ES_SYSTEM_REQUIRED = 0x00000001; | |
| public const uint ES_DISPLAY_REQUIRED = 0x00000002; | |
| } | |
| "@ | |
| function Set-PreventSleep { | |
| <# | |
| .SYNOPSIS | |
| Previne o sistema de entrar em sleep/hibernação. | |
| #> | |
| param([bool] $Prevent = $true) | |
| if ($Prevent) { | |
| # Manter sistema e display ativos | |
| $result = [PowerManagement]::SetThreadExecutionState( | |
| [PowerManagement]::ES_CONTINUOUS -bor | |
| [PowerManagement]::ES_SYSTEM_REQUIRED -bor | |
| [PowerManagement]::ES_DISPLAY_REQUIRED | |
| ) | |
| if ($result -ne 0) { | |
| Write-ColorOutput "💡 Prevenção de sleep ativada" -Color $script:COLORS.Info | |
| } else { | |
| Write-ColorOutput "⚠️ Não foi possível ativar prevenção de sleep" -Color $script:COLORS.Warning | |
| } | |
| } else { | |
| # Restaurar comportamento normal | |
| $result = [PowerManagement]::SetThreadExecutionState([PowerManagement]::ES_CONTINUOUS) | |
| Write-ColorOutput "😴 Prevenção de sleep desativada" -Color $script:COLORS.Info | |
| } | |
| } | |
| function Write-ColorOutput { | |
| <# | |
| .SYNOPSIS | |
| Escreve mensagem colorida no console com timestamp. | |
| #> | |
| param( | |
| [string] $Message, | |
| [string] $Color = 'White', | |
| [switch] $NoNewline | |
| ) | |
| $timestamp = Get-Date -Format "HH:mm:ss" | |
| $prefix = "[$timestamp]" | |
| if ($NoNewline) { | |
| Write-Host "$prefix $Message" -ForegroundColor $Color -NoNewline | |
| } else { | |
| Write-Host "$prefix $Message" -ForegroundColor $Color | |
| } | |
| } | |
| function Test-Prerequisites { | |
| <# | |
| .SYNOPSIS | |
| Verifica se todos os pré-requisitos estão atendidos. | |
| #> | |
| Write-ColorOutput "🔍 Verificando pré-requisitos..." -Color $script:COLORS.Info | |
| # Verificar se o Git está instalado | |
| try { | |
| $gitVersion = git --version 2>$null | |
| if (-not $gitVersion) { throw "Git não encontrado" } | |
| Write-Verbose "Git encontrado: $gitVersion" | |
| } catch { | |
| throw "❌ Git não está instalado ou não está no PATH. Instale o Git em: https://git-scm.com/" | |
| } | |
| # Verificar conectividade com GitHub | |
| try { | |
| $testUrl = "$($script:CONFIG.ApiBaseUrl)/rate_limit" | |
| $headers = @{ | |
| "Authorization" = "token $Pat" | |
| "User-Agent" = $script:CONFIG.UserAgent | |
| } | |
| $proxyParams = @{} | |
| if ($Proxy) { | |
| $proxyParams.Proxy = $Proxy | |
| if ($ProxyUser -and $ProxyPassword) { | |
| $credential = New-Object System.Management.Automation.PSCredential($ProxyUser, $ProxyPassword) | |
| $proxyParams.ProxyCredential = $credential | |
| } | |
| } | |
| Invoke-RestMethod -Uri $testUrl -Headers $headers -TimeoutSec 10 @proxyParams | Out-Null | |
| Write-ColorOutput "✅ Conectividade com GitHub API verificada" -Color $script:COLORS.Success | |
| } catch { | |
| throw "❌ Não foi possível conectar com a API do GitHub. Verifique sua conexão e PAT: $($_.Exception.Message)" | |
| } | |
| # Verificar/criar diretório de saída | |
| try { | |
| if (-not (Test-Path $OutDir)) { | |
| New-Item -ItemType Directory -Path $OutDir -Force | Out-Null | |
| Write-ColorOutput "📁 Diretório criado: $OutDir" -Color $script:COLORS.Info | |
| } else { | |
| Write-ColorOutput "📁 Usando diretório existente: $OutDir" -Color $script:COLORS.Info | |
| } | |
| } catch { | |
| throw "❌ Erro ao criar/acessar diretório: $OutDir - $($_.Exception.Message)" | |
| } | |
| } | |
| function Invoke-GitHubApiWithRetry { | |
| <# | |
| .SYNOPSIS | |
| Chama a API do GitHub com retry automático e tratamento de rate limit. | |
| #> | |
| param( | |
| [string] $Url, | |
| [hashtable] $Headers, | |
| [hashtable] $ProxyParams = @{} | |
| ) | |
| for ($attempt = 1; $attempt -le $script:CONFIG.MaxRetries; $attempt++) { | |
| try { | |
| Write-Verbose "Tentativa $attempt de $($script:CONFIG.MaxRetries) para: $Url" | |
| $response = Invoke-RestMethod -Uri $Url -Headers $Headers -TimeoutSec $script:CONFIG.TimeoutSeconds @ProxyParams | |
| # Log de rate limit (se disponível nos headers) | |
| if ($response.PSObject.Properties['rate_limit']) { | |
| Write-Verbose "Rate limit restante: $($response.rate_limit.remaining)/$($response.rate_limit.limit)" | |
| } | |
| return $response | |
| } catch { | |
| $errorDetails = $_.Exception.Message | |
| # Tratamento específico para rate limit | |
| if ($_.Exception.Response.StatusCode -eq 403) { | |
| Write-ColorOutput "⚠️ Rate limit atingido. Aguardando..." -Color $script:COLORS.Warning | |
| Start-Sleep -Seconds 60 | |
| continue | |
| } | |
| # Tratamento para erros temporários | |
| if ($attempt -lt $script:CONFIG.MaxRetries -and | |
| ($_.Exception.Response.StatusCode -ge 500 -or $_.Exception -is [System.Net.WebException])) { | |
| Write-ColorOutput "⚠️ Erro temporário (tentativa $attempt): $errorDetails" -Color $script:COLORS.Warning | |
| Start-Sleep -Seconds ($script:CONFIG.RetryDelay * $attempt) | |
| continue | |
| } | |
| # Erro final | |
| throw "Falha na chamada da API após $attempt tentativas: $errorDetails" | |
| } | |
| } | |
| } | |
| function Get-SafeDirectoryName { | |
| <# | |
| .SYNOPSIS | |
| Gera nome de diretório seguro removendo caracteres inválidos. | |
| #> | |
| param([string] $Name) | |
| $invalidChars = [IO.Path]::GetInvalidFileNameChars() -join '' | |
| $safeName = $Name -replace "[$invalidChars]", '_' | |
| return $safeName | |
| } | |
| function Start-GitClone { | |
| <# | |
| .SYNOPSIS | |
| Clona um repositório Git com tratamento robusto de erros. | |
| #> | |
| param( | |
| [string] $CloneUrl, | |
| [string] $TargetPath, | |
| [string] $RepoName, | |
| [hashtable] $ProxyParams = @{} | |
| ) | |
| try { | |
| Write-ColorOutput "📥 Clonando: $RepoName" -Color $script:COLORS.Progress | |
| # Preparar argumentos do git | |
| $gitArgs = @('clone', $CloneUrl, $TargetPath, '--depth=1') | |
| if ($Proxy) { | |
| # Configurar proxy para Git | |
| if ($ProxyUser -and $ProxyPassword) { | |
| $plainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ProxyPassword)) | |
| $proxyWithAuth = $Proxy -replace "://", "://$ProxyUser`:$plainPassword@" | |
| $gitArgs = @('-c', "http.proxy=$proxyWithAuth") + $gitArgs | |
| } else { | |
| $gitArgs = @('-c', "http.proxy=$Proxy") + $gitArgs | |
| } | |
| } | |
| # Executar git clone usando operador & | |
| Write-Verbose "Executando: git $($gitArgs -join ' ')" | |
| & git @gitArgs | |
| if ($LASTEXITCODE -eq 0) { | |
| Write-ColorOutput "✅ Sucesso: $RepoName" -Color $script:COLORS.Success | |
| return $true | |
| } else { | |
| Write-ColorOutput "❌ Falha ao clonar: $RepoName (Exit Code: $LASTEXITCODE)" -Color $script:COLORS.Error | |
| return $false | |
| } | |
| } catch { | |
| Write-ColorOutput "❌ Erro no clone de $RepoName : $($_.Exception.Message)" -Color $script:COLORS.Error | |
| return $false | |
| } | |
| } | |
| #endregion | |
| #region Função Principal | |
| function Start-RepositoryCloning { | |
| <# | |
| .SYNOPSIS | |
| Função principal que orquestra todo o processo de clonagem. | |
| #> | |
| # Banner inicial | |
| Write-Host @" | |
| ╔══════════════════════════════════════════════════════════════╗ | |
| ║ 🚀 GitHub Repository Cloner v2.0 ║ | |
| ║ por Guinetik ║ | |
| ╚══════════════════════════════════════════════════════════════╝ | |
| "@ -ForegroundColor $script:COLORS.Info | |
| # Verificar pré-requisitos | |
| Test-Prerequisites | |
| # Ativar prevenção de sleep | |
| Set-PreventSleep -Prevent $true | |
| # Configurar headers da API | |
| $headers = @{ | |
| "Authorization" = "token $Pat" | |
| "Accept" = "application/vnd.github.v3+json" | |
| "User-Agent" = $script:CONFIG.UserAgent | |
| } | |
| # Configurar proxy se especificado | |
| $proxyParams = @{} | |
| if ($Proxy) { | |
| $proxyParams.Proxy = $Proxy | |
| # Configurar credenciais do proxy se fornecidas | |
| if ($ProxyUser -and $ProxyPassword) { | |
| $credential = New-Object System.Management.Automation.PSCredential($ProxyUser, $ProxyPassword) | |
| $proxyParams.ProxyCredential = $credential | |
| } | |
| Write-ColorOutput "🌐 Usando proxy: $Proxy" -Color $script:COLORS.Info | |
| if ($ProxyUser) { | |
| Write-ColorOutput "👤 Proxy com autenticação: $ProxyUser" -Color $script:COLORS.Info | |
| } | |
| } | |
| # Construir query de busca | |
| $searchQuery = $Query | |
| if (-not $IncludePublic) { | |
| $searchQuery += " is:private" | |
| } | |
| Write-ColorOutput "🔍 Critério de busca: $searchQuery" -Color $script:COLORS.Info | |
| Write-ColorOutput "📂 Diretório de destino: $OutDir" -Color $script:COLORS.Info | |
| Write-ColorOutput "🎯 Limite máximo: $MaxRepos repositórios" -Color $script:COLORS.Info | |
| # Contadores e estatísticas | |
| $stats = @{ | |
| Total = 0 | |
| Cloned = 0 | |
| Skipped = 0 | |
| Failed = 0 | |
| StartTime = Get-Date | |
| } | |
| $page = 1 | |
| $repositories = @() | |
| # Buscar repositórios paginados | |
| Write-ColorOutput "🔄 Iniciando busca de repositórios..." -Color $script:COLORS.Info | |
| do { | |
| $encodedQuery = [System.Uri]::EscapeDataString($searchQuery) | |
| $apiUrl = "$($script:CONFIG.ApiBaseUrl)/search/repositories?q=$encodedQuery&per_page=$($script:CONFIG.PerPage)&page=$page" | |
| Write-Verbose "Buscando página $page..." | |
| try { | |
| $response = Invoke-GitHubApiWithRetry -Url $apiUrl -Headers $headers -ProxyParams $proxyParams | |
| if ($response.items -and $response.items.Count -gt 0) { | |
| $repositories += $response.items | |
| Write-ColorOutput "📄 Página $page : $($response.items.Count) repositórios encontrados" -Color $script:COLORS.Info | |
| # Verificar se atingiu o limite máximo | |
| if ($repositories.Count -ge $MaxRepos) { | |
| $repositories = $repositories[0..($MaxRepos - 1)] | |
| break | |
| } | |
| } else { | |
| break | |
| } | |
| $page++ | |
| } catch { | |
| Write-ColorOutput "❌ Erro na busca: $($_.Exception.Message)" -Color $script:COLORS.Error | |
| break | |
| } | |
| } while ($response.items.Count -eq $script:CONFIG.PerPage) | |
| $stats.Total = $repositories.Count | |
| Write-ColorOutput "📊 Total de repositórios encontrados: $($stats.Total)" -Color $script:COLORS.Info | |
| if ($stats.Total -eq 0) { | |
| Write-ColorOutput "ℹ️ Nenhum repositório encontrado com os critérios especificados." -Color $script:COLORS.Warning | |
| return | |
| } | |
| # Processar clonagem | |
| Write-ColorOutput "🚀 Iniciando processo de clonagem..." -Color $script:COLORS.Info | |
| foreach ($repo in $repositories) { | |
| $owner = $repo.owner.login | |
| $name = $repo.name | |
| $fullName = "$owner/$name" | |
| # Usar apenas o nome do repositório como diretório | |
| $safeDirName = Get-SafeDirectoryName $name | |
| $targetDir = Join-Path $OutDir $safeDirName | |
| # Verificar se já existe | |
| if (Test-Path $targetDir) { | |
| Write-ColorOutput "⏭️ Pulando (já existe): $fullName" -Color $script:COLORS.Warning | |
| $stats.Skipped++ | |
| continue | |
| } | |
| # Construir URL de clone com autenticação | |
| $cloneUrl = $repo.clone_url -replace "https://", "https://oauth:$Pat@" | |
| # Executar clone | |
| if ($PSCmdlet.ShouldProcess($fullName, "Clonar repositório")) { | |
| $cloneResult = Start-GitClone -CloneUrl $cloneUrl -TargetPath $targetDir -RepoName $fullName -ProxyParams $proxyParams | |
| if ($cloneResult) { | |
| $stats.Cloned++ | |
| } else { | |
| $stats.Failed++ | |
| } | |
| } | |
| # Exibir progresso | |
| $completed = $stats.Cloned + $stats.Skipped + $stats.Failed | |
| $percent = [math]::Round(($completed / $stats.Total) * 100, 1) | |
| Write-Progress -Activity "Clonando repositórios" -Status "$completed de $($stats.Total) processados ($percent%)" -PercentComplete $percent | |
| } | |
| # Limpar barra de progresso | |
| Write-Progress -Activity "Clonando repositórios" -Completed | |
| # Relatório final | |
| $duration = (Get-Date) - $stats.StartTime | |
| Write-Host @" | |
| ╔══════════════════════════════════════════════════════════════╗ | |
| ║ 📊 RELATÓRIO FINAL ║ | |
| ╠══════════════════════════════════════════════════════════════╣ | |
| ║ Total encontrados: $($stats.Total.ToString().PadLeft(3)) ║ | |
| ║ Clonados com sucesso: $($stats.Cloned.ToString().PadLeft(3)) ║ | |
| ║ Pulados (já existem): $($stats.Skipped.ToString().PadLeft(3)) ║ | |
| ║ Falharam: $($stats.Failed.ToString().PadLeft(3)) ║ | |
| ║ Tempo total: $($duration.ToString('hh\:mm\:ss')) ║ | |
| ║ Diretório: $($OutDir.PadRight(42)) ║ | |
| ╚══════════════════════════════════════════════════════════════╝ | |
| "@ -ForegroundColor $script:COLORS.Success | |
| if ($stats.Failed -gt 0) { | |
| Write-ColorOutput "⚠️ Alguns repositórios falharam. Verifique os logs acima para detalhes." -Color $script:COLORS.Warning | |
| } else { | |
| Write-ColorOutput "🎉 Processo concluído com sucesso!" -Color $script:COLORS.Success | |
| } | |
| } | |
| #endregion | |
| #region Execução Principal | |
| try { | |
| Start-RepositoryCloning | |
| } catch { | |
| Write-ColorOutput "💥 Erro fatal: $($_.Exception.Message)" -Color $script:COLORS.Error | |
| Write-Verbose $_.ScriptStackTrace | |
| exit 1 | |
| } finally { | |
| # Restaurar comportamento normal de energia | |
| Set-PreventSleep -Prevent $false | |
| Write-ColorOutput "🏁 Script finalizado." -Color $script:COLORS.Info | |
| } | |
| #endregion |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment