Public/New-Repository.ps1
|
function New-Repository { <# .SYNOPSIS Clone a git repository into the standard worktree-ready directory layout. .DESCRIPTION Clones a repository from a GitHub or Azure DevOps URL into <root>\<org>\<repo>\<branch>, ready for worktree use, where <root> comes from -Root, falling back to $env:SOURCE_REPOS. The org and repo name are extracted automatically from the URL. Submodules are recursively initialized by default. .PARAMETER Url The clone URL (HTTPS or SSH) for the repository. .PARAMETER Root The repos root to clone into. Defaults to $env:SOURCE_REPOS. Cloning fails if neither is set. .PARAMETER Org Override the auto-detected organization/owner name. .PARAMETER Name Override the auto-detected repository name. .PARAMETER Branch Branch to clone. Defaults to the remote's default branch. .PARAMETER NoSetLocation Do not change to the cloned directory after cloning. .PARAMETER NoRecurseSubmodules Do not recursively initialize submodules. .PARAMETER Shallow Perform a partial (blobless) clone with --filter=blob:none. .EXAMPLE New-Repository https://github.com/microsoft/terminal Clones into D:\source\repos\microsoft\terminal\main and navigates there. .EXAMPLE New-Repository https://dev.azure.com/contoso/Platform/_git/runtime -Branch release/v2 Clones the release/v2 branch into D:\source\repos\contoso\runtime\release\v2. .EXAMPLE New-Repository https://github.com/dotnet/runtime -Org dotnet -Shallow Performs a blobless clone into D:\source\repos\dotnet\runtime\<default-branch>. .EXAMPLE New-Repository git@github.com:spectreconsole/spectre.console.git -NoSetLocation Clones without changing the current directory. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [string]$Url, [string]$Root, [string]$Org, [string]$Name, [string]$Branch, [switch]$NoSetLocation, [switch]$NoRecurseSubmodules, [switch]$Shallow ) # Parse org and repo name from URL if not provided # Supports: https://github.com/<org>/<repo>[.git] # https://dev.azure.com/<org>/<project>/_git/<repo> # https://<org>.visualstudio.com/[DefaultCollection/]<project>/_git/<repo> # git@github.com:<org>/<repo>.git # <org>@vs-ssh.visualstudio.com:v3/<org>/<project>/<repo> if (-not $Name -or -not $Org) { $parsed = $false # Azure DevOps HTTPS: https://dev.azure.com/<org>/<project>/_git/<repo> if ($Url -match 'dev\.azure\.com/([^/]+)/[^/]+/_git/([^/?]+)') { if (-not $Org) { $Org = $Matches[1] } if (-not $Name) { $Name = $Matches[2] -replace '\.git$', '' } $parsed = $true } # Azure DevOps legacy HTTPS: https://<org>.visualstudio.com/[DefaultCollection/]<project>/_git/<repo> elseif ($Url -match '([^/]+)\.visualstudio\.com(?:/DefaultCollection)?/[^/]+/_git/([^/?]+)') { if (-not $Org) { $Org = $Matches[1] } if (-not $Name) { $Name = $Matches[2] -replace '\.git$', '' } $parsed = $true } # Azure DevOps SSH: <org>@vs-ssh.visualstudio.com:v3/<org>/<project>/<repo> elseif ($Url -match 'vs-ssh\.visualstudio\.com:v3/([^/]+)/[^/]+/([^/?]+)') { if (-not $Org) { $Org = $Matches[1] } if (-not $Name) { $Name = $Matches[2] -replace '\.git$', '' } $parsed = $true } # GitHub/generic HTTPS: https://<host>/<org>/<repo>[.git] elseif ($Url -match 'https?://[^/]+/([^/]+)/([^/?]+)') { if (-not $Org) { $Org = $Matches[1] } if (-not $Name) { $Name = $Matches[2] -replace '\.git$', '' } $parsed = $true } # GitHub/generic SSH: git@<host>:<org>/<repo>.git elseif ($Url -match 'git@[^:]+:([^/]+)/([^/?]+)') { if (-not $Org) { $Org = $Matches[1] } if (-not $Name) { $Name = $Matches[2] -replace '\.git$', '' } $parsed = $true } if (-not $parsed -or -not $Org -or -not $Name) { Write-Error "Unable to parse org and repo name from URL: $Url. Use -Org and -Name to specify manually." return } } $reposRoot = if ($Root) { $Root } else { $env:SOURCE_REPOS } if (-not $reposRoot) { Write-Error 'No repository root specified. Pass -Root or set $env:SOURCE_REPOS.' return } # Clone to a temp directory first, then determine the branch name for the final path $cloneArgs = @('clone', $Url) if ($Branch) { $cloneArgs += '--branch', $Branch } if (-not $NoRecurseSubmodules) { $cloneArgs += '--recurse-submodules' } if ($Shallow) { $cloneArgs += '--filter=blob:none' } # Determine the target branch name for the directory if ($Branch) { $branchDir = $Branch } else { # Query the remote for the default branch $remoteBranch = git ls-remote --symref $Url HEAD 2>&1 | Select-String 'ref: refs/heads/(\S+)\s+HEAD' | ForEach-Object { $_.Matches[0].Groups[1].Value } if (-not $remoteBranch) { $branchDir = 'main' Write-Warning "Could not detect default branch, using 'main'." } else { $branchDir = $remoteBranch } } $targetPath = Join-Path $reposRoot $Org $Name $branchDir if (Test-Path $targetPath) { Write-Error "Target path already exists: $targetPath" return } if ($PSCmdlet.ShouldProcess("$Org/$Name", "Clone repository to '$targetPath'")) { # Ensure parent directory exists $parentPath = Split-Path $targetPath -Parent if (-not (Test-Path $parentPath)) { New-Item -ItemType Directory -Path $parentPath -Force | Out-Null } $cloneArgs += $targetPath Write-Verbose "Cloning $Org/$Name into $targetPath" & git @cloneArgs if ($LASTEXITCODE -ne 0) { Write-Error "git clone failed with exit code $LASTEXITCODE" return } if (-not $NoSetLocation) { Set-Location -Path $targetPath } } } |