#!powershell #AnsibleRequires -CSharpUtil Ansible.Basic #AnsibleRequires -PowerShell Ansible.ModuleUtils.CommandUtil $module = [Ansible.Basic.AnsibleModule]::Create($args, @{ options = @{ dest = @{ type = 'path' } repo = @{ required = $true; aliases = @( 'name' ) } version = @{ default = 'HEAD' } remote = @{ default = 'origin' } executable = @{ default = $null; type = 'path' } } supports_check_mode = $false }) $dest = $module.Params.dest $repo = $module.Params.repo $version = $module.Params.version $remote = $module.Params.remote $git = $module.Params.executable if ( !$git ) { $git = Get-ExecutablePath 'git' } # ================================= Utilities ================================== function Test-SshAcceptNewHostKey { try { $ssh = Get-ExecutablePath 'ssh' } catch { throw 'Remote host is missing ssh command, so you cannot use acceptnewhostkey option.' } $result = Run-Command "$ssh -o StrictHostKeyChecking=accept-new -V" if ( $result.rc -ne 0 ) { return $false } return $true } function Get-AbsolutePath { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $path ) try { $result = Resolve-Path $path } catch { return $_[0].TargetObject } return $result } function Get-GitDir { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $path ) $git_dir = Join-Path $path '.git' # Check if this .git is a file. if ( [System.IO.File]::Exists($git_dir) ) { # Extract the gitdir: path from the .git file. $groups = Get-Content "$git_dir" | ` Select-String '(gitdir:) (.*)' | ` ForEach { $_.Matches[0].Groups[1..2] } $ref_prefix = $groups[0] $gitdir = $groups[1] if ( $ref_prefix -ne 'gitdir:' ) { $module.FailJson('The .git file has invalid gitdir reference format.') } # Check if the repo path is absolute. if ( [System.IO.Path]::IsPathRooted($gitdir) ) { $git_dir = $gitdir } else { # Join with the input path to construct an absolute path. $git_dir = Join-Path $path $gitdir } if ( ![System.IO.Directory]::Exists($git_dir) ) { throw "$git_dir is not a directory." } } return $git_dir } function Get-GitSshExecutablePath { if ( $env:GIT_SSH ) { return $env:GIT_SSH } if ( $env:GIT_SSH_COMMAND ) { return $env:GIT_SSH_COMMAND } return Get-ExecutablePath 'ssh' } function Test-GitRemoteBranch { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $dest, [Parameter(Mandatory = $true)] [String] $repository, [Parameter(Mandatory = $true)] [String] $branch ) $command = "`"$git`" ls-remote $repository -h refs/heads/$branch" $result = Run-Command -command $command if ( $result.stdout.Contains($version) ) { return $true } return $false } function Test-GitRemoteTag { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $dest, [Parameter(Mandatory = $true)] [String] $repository, [Parameter(Mandatory = $true)] [String] $tag ) $command = "`"$git`" ls-remote $repository -t refs/tags/$tag" $result = Run-Command -command $command if ( $result.stdout.Contains($version) ) { return $true } return $false } function Test-GitLocalChanges { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $dest ) $command = "`"$git`" status --porcelain" $result = Run-Command -command $command -working_directory $dest $changes = $result.stdout.Split([System.Environment]::NewLine, ` [System.StringSplitOptions]::RemoveEmptyEntries) | ` Where-Object { -not $_.StartsWith('??') } | Measure-Object -Line return $changes.Lines -gt 0 } function Get-GitRemoteHead { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $repo, [Parameter(Mandatory = $true)] [String] $dest, [Parameter(Mandatory = $true)] [String] $version, [Parameter(Mandatory = $true)] [String] $remote ) $cloning = $false $cwd = $null $tag = $false if ( $remote -eq $repo ) { $cloning = $true } else { $cwd = $dest } if ( $version -eq 'HEAD' ) { if ( $cloning ) { # Cloning the repo, just get the remote's HEAD version. $command = "`"$git`" ls-remote $remote -h HEAD" } else { $head_branch = Get-GitRemoteHeadBranch $module $dest $remote $command = "`"$git`" ls-remote $remote -h refs/heads/$head_branch" } } elseif ( Test-GitRemoteBranch $dest $remote $version ) { $command = "`"$git`" ls-remote $remote -h refs/head/$version" } elseif ( Test-GitRemoteTag $dest $remote $version ) { $tag = $true $command = "`"$git`" ls-remote $remote -t refs/tags/$version*" } else { # Appears to be a sha1, return as-is since it apparently not possible # to check for a specific sha1 on remote. return $version } $result = Run-Command -command $command -working_directory $cwd if ( $result.rc -ne 0 -or $result.stdout.Length -lt 1 ) { throw "Could not determine remote ref for $vesion" } $ref = $result.stdout if ( $tag ) { # Find the dereferenced tag if this is an annotated tag. ForEach ( $tag in $ref.Split([System.Environment]::NewLine) ) { if ( $tag.EndsWith("$version ^{}") ) { $ref = $tag } elseif ( $tag.EndsWith($version) ) { $ref = $tag } } } return $ref.Split()[0] } function Test-GitDetachedHead { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] $dest ) } function Get-GitRemoteHeadBranch { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] $dest, [Parameter(Mandatory = $true)] [String] $version, [Parameter(Mandatory = $true)] [String] $remote ) $git_dir = Join-Path $dest '.git' # TODO: Check if the .git is a file. If it is a file, it means that we are # in a submodule structure. $head_file = Join-Path $git_dir 'HEAD' if ( Test-GitDetachedHead $dest ) { $head_file = Join-Path $git_dir 'refs' 'remotes' $remote 'HEAD' } } function Get-GitCurrentSha { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] $dest ) $command = "`"$git`" rev-parse HEAD" $result = Run-Command -command $command -working_directory $dest return $result.stdout.Trim() } function Get-GitRemoteUrl { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] $dest, [Parameter(Mandatory = $true)] [String] $remote ) $command = "`"$git`" ls-remote --get-url $remote" $result = Run-Command -command $command -working_directory $dest if ( $result.rc -ne 0 ) { # There was an issue getting the remote URL, most likely command is not # available in this version of Git. return $null } return $result.stdout.Trim() } function Set-GitRemoteUrl { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] $dest, [Parameter(Mandatory = $true)] [String] $remote, [Parameter(Mandatory = $true)] [String] $url ) # Return if remote URL isn't changing. $remote_url = Get-GitRemoteUrl $dest $remote if ( $remote_url -eq $repo ) { return $false } $command = "`"$git`" remote set-url $remote $url" $result = Run-Command -command $command -working_directory $dest if ( $result.rc -ne 0 ) { $module.FailJson("Failed to set a new url $url for $remote`: $result.stderr") } # Return false if remote_url is null to maintain previous behavior for Git # versions prior to 1.7.5 that lack required functionality. return $remote_url -ne $null } function Invoke-GitClone { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] $repo, [Parameter(Mandatory = $true)] [String] $remote, [Parameter(Mandatory = $true)] [String] $dest ) $dest_parent = Split-Path -Path $dest -Parent if ( !(Test-Path $dest_parent) ) { New-Item -Path $dest_parent -ItemType Directory } $command = "`"$git`" clone --recursive --origin $remote $repo $dest" if ( $version -ne "HEAD" ) { $command += " --branch $version" } $result = Run-Command -command $command -working_directory $cwd if ( $result.rc -ne 0 ) { $module.FailJson($result.stderr) } # Ensure the newly cloned repository has the correct owner $userName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $idRef = [System.Security.Principal.NTAccount]::new($userName) Get-Item $dest | foreach { ` $_ ; $_ | Get-ChildItem -Force -Recurse ` } | foreach { ` $acl = $_ | Get-Acl; $acl.SetOwner($idRef); $_ | Set-Acl -AclObject $acl ` } } function Invoke-GitFetch { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] $repo, [Parameter(Mandatory = $true)] [String] $dest, [Parameter(Mandatory = $true)] [String] $version, [Parameter(Mandatory = $true)] [String] $remote ) $command = "`"$git`" fetch --tags $remote" $result = Run-Command -command $command -working_directory $dest if ( $result.rc -ne 0 ) { $module.FailJson("Failed to download remote objects and refs:" + ` "$result.stdout $result.stderr") } } function Invoke-GitCheckout { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] $dest, [Parameter(Mandatory = $true)] [String] $version ) $result = Run-Command -command "`"$git`" checkout $version" -working_directory $dest if ( $result.rc -ne 0 ) { $module.FailJson("Failed to checkout version '$version': " + ` "$result.stdout $result.stderr") } } # ================================ Start logic ================================= if ( !$dest ) { $module.FailJson('The destination directory must be specified.') } $dest = Get-AbsolutePath $dest $git_dir = Get-GitDir $dest $gitconfig = Join-Path $git_dir 'config' $module.Result.before = $null $local_changes = $false if ( ($dest -and ![System.IO.File]::Exists($gitconfig)) -or (!$dest -and !$clone) ) { Invoke-GitClone $repo $remote $dest $version $module.Result.changed = $true } else { $local_changes = Test-GitLocalChanges $dest $module.Result.before = Get-GitCurrentSha $dest if ( $local_changes ) { $module.FailJson('Local modifications exist in repository.') } # Checkout branch, if $version is HEAD get HEAD branch # Pull } $module.ExitJson()