Adding Get Repository.Contents functionality (#146)
Adds `Get-GitHubContent` and corresponding tests. Also adds formatting files for VS Code and EditorConfig.
This commit is contained in:
Родитель
e07d3fabc0
Коммит
9a45908dc6
|
@ -0,0 +1,8 @@
|
|||
# editorconfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
|
||||
trim_trailing_whitespace = true
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"[powershell]": {
|
||||
"files.trimTrailingWhitespace": true
|
||||
},
|
||||
"powershell.codeFormatting.openBraceOnSameLine": false,
|
||||
"powershell.codeFormatting.alignPropertyValuePairs": false
|
||||
}
|
|
@ -181,7 +181,7 @@ function Get-GitHubComment
|
|||
'UriFragment' = $uriFragment
|
||||
'Description' = $description
|
||||
'AccessToken' = $AccessToken
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AcceptHeader $squirrelAcceptHeader)
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson -AcceptHeader $squirrelAcceptHeader)
|
||||
'TelemetryEventName' = $MyInvocation.MyCommand.Name
|
||||
'TelemetryProperties' = $telemetryProperties
|
||||
'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -Name NoStatus -ConfigValueName DefaultNoStatus)
|
||||
|
@ -292,7 +292,7 @@ function New-GitHubComment
|
|||
'Method' = 'Post'
|
||||
'Description' = "Creating comment under issue $Issue for $RepositoryName"
|
||||
'AccessToken' = $AccessToken
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AcceptHeader $squirrelAcceptHeader)
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson -AcceptHeader $squirrelAcceptHeader)
|
||||
'TelemetryEventName' = $MyInvocation.MyCommand.Name
|
||||
'TelemetryProperties' = $telemetryProperties
|
||||
'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -Name NoStatus -ConfigValueName DefaultNoStatus)
|
||||
|
@ -403,7 +403,7 @@ function Set-GitHubComment
|
|||
'Method' = 'Patch'
|
||||
'Description' = "Update comment $CommentID for $RepositoryName"
|
||||
'AccessToken' = $AccessToken
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AcceptHeader $squirrelAcceptHeader)
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson -AcceptHeader $squirrelAcceptHeader)
|
||||
'TelemetryEventName' = $MyInvocation.MyCommand.Name
|
||||
'TelemetryProperties' = $telemetryProperties
|
||||
'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -Name NoStatus -ConfigValueName DefaultNoStatus)
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
function Get-GitHubContent
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Retrieve the contents of a file or directory in a repository on GitHub.
|
||||
|
||||
.DESCRIPTION
|
||||
Retrieve content from files on GitHub.
|
||||
The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub
|
||||
|
||||
.PARAMETER OwnerName
|
||||
Owner of the repository.
|
||||
If not supplied here, the DefaultOwnerName configuration property value will be used.
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
Name of the repository.
|
||||
If not supplied here, the DefaultRepositoryName configuration property value will be used.
|
||||
|
||||
.PARAMETER Uri
|
||||
Uri for the repository.
|
||||
The OwnerName and RepositoryName will be extracted from here instead of needing to provide
|
||||
them individually.
|
||||
|
||||
.PARAMETER Path
|
||||
The file path for which to retrieve contents
|
||||
|
||||
.PARAMETER MediaType
|
||||
The format in which the API will return the body of the issue.
|
||||
Object - Return a json object representation a file or folder. This is the default if you do not pass any specific media type.
|
||||
Raw - Return the raw contents of a file.
|
||||
Html - For markup files such as Markdown or AsciiDoc, you can retrieve the rendered HTML using the Html media type.
|
||||
|
||||
.PARAMETER ResultAsString
|
||||
If this switch is specified and the MediaType is either Raw or Html then the resulting bytes will be decoded the result will be
|
||||
returned as a string instead of bytes. If the MediaType is Object, then an additional property on the object is returned 'contentAsString'
|
||||
which will be the decoded base64 result as a string.
|
||||
|
||||
.PARAMETER AccessToken
|
||||
If provided, this will be used as the AccessToken for authentication with the
|
||||
REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated.
|
||||
|
||||
.PARAMETER NoStatus
|
||||
If this switch is specified, long-running commands will run on the main thread
|
||||
with no commandline status update. When not specified, those commands run in
|
||||
the background, enabling the command prompt to provide status information.
|
||||
If not supplied here, the DefaultNoStatus configuration property value will be used.
|
||||
|
||||
.EXAMPLE
|
||||
Get-GitHubContent -OwnerName microsoft -RepositoryName PowerShellForGitHub -Path README.md -MediaType Html
|
||||
|
||||
Get the Html output for the README.md file
|
||||
|
||||
.EXAMPLE
|
||||
Get-GitHubContent -OwnerName microsoft -RepositoryName PowerShellForGitHub -Path LICENSE
|
||||
|
||||
Get the Binary file output for the LICENSE file
|
||||
|
||||
.EXAMPLE
|
||||
Get-GitHubContent -OwnerName microsoft -RepositoryName PowerShellForGitHub -Path Tests
|
||||
|
||||
List the files within the "Tests" path of the repository
|
||||
#>
|
||||
[CmdletBinding(
|
||||
SupportsShouldProcess,
|
||||
DefaultParametersetName = 'Elements')]
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification = "Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")]
|
||||
param(
|
||||
[Parameter(Mandatory, ParameterSetName = 'Elements')]
|
||||
[string] $OwnerName,
|
||||
|
||||
[Parameter(Mandatory, ParameterSetName = 'Elements')]
|
||||
[string] $RepositoryName,
|
||||
|
||||
[Parameter(
|
||||
Mandatory,
|
||||
ParameterSetName='Uri')]
|
||||
[string] $Uri,
|
||||
|
||||
[string] $Path,
|
||||
|
||||
[ValidateSet('Raw', 'Html', 'Object')]
|
||||
[string] $MediaType = 'Object',
|
||||
|
||||
[switch] $ResultAsString,
|
||||
|
||||
[string] $AccessToken,
|
||||
|
||||
[switch] $NoStatus
|
||||
)
|
||||
|
||||
Write-InvocationLog
|
||||
|
||||
$elements = Resolve-RepositoryElements -DisableValidation
|
||||
$OwnerName = $elements.ownerName
|
||||
$RepositoryName = $elements.repositoryName
|
||||
|
||||
$telemetryProperties = @{
|
||||
'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName)
|
||||
'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName)
|
||||
}
|
||||
|
||||
$description = [String]::Empty
|
||||
|
||||
$uriFragment = "/repos/$OwnerName/$RepositoryName/contents"
|
||||
|
||||
if ($PSBoundParameters.ContainsKey('Path'))
|
||||
{
|
||||
$Path = $Path.TrimStart("\", "/")
|
||||
$uriFragment += "/$Path"
|
||||
$description = "Getting content for $Path in $RepositoryName"
|
||||
}
|
||||
else
|
||||
{
|
||||
$description = "Getting all content for in $RepositoryName"
|
||||
}
|
||||
|
||||
$params = @{
|
||||
'UriFragment' = $uriFragment
|
||||
'Description' = $description
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType)
|
||||
'AccessToken' = $AccessToken
|
||||
'TelemetryEventName' = $MyInvocation.MyCommand.Name
|
||||
'TelemetryProperties' = $telemetryProperties
|
||||
'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -Name NoStatus -ConfigValueName DefaultNoStatus)
|
||||
}
|
||||
|
||||
$result = Invoke-GHRestMethodMultipleResult @params
|
||||
|
||||
if ($ResultAsString)
|
||||
{
|
||||
if ($MediaType -eq 'Raw' -or $MediaType -eq 'Html')
|
||||
{
|
||||
# Decode bytes to string
|
||||
$result = [System.Text.Encoding]::UTF8.GetString($result)
|
||||
}
|
||||
elseif ($MediaType -eq 'Object')
|
||||
{
|
||||
# Convert from base64
|
||||
$decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($result.content))
|
||||
Add-Member -InputObject $result -NotePropertyName "contentAsString" -NotePropertyValue $decoded
|
||||
}
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
|
@ -127,7 +127,7 @@ function Invoke-GHRestMethod
|
|||
# be coming from the Location header in a previous response. Either way, we don't want there
|
||||
# to be a leading "/" or trailing '/'
|
||||
if ($UriFragment.StartsWith('/')) { $UriFragment = $UriFragment.Substring(1) }
|
||||
if ($UriFragment.EndsWIth('/')) { $UriFragment = $UriFragment.Substring(0, $UriFragment.Length - 1) }
|
||||
if ($UriFragment.EndsWith('/')) { $UriFragment = $UriFragment.Substring(0, $UriFragment.Length - 1) }
|
||||
|
||||
if ([String]::IsNullOrEmpty($Description))
|
||||
{
|
||||
|
@ -645,7 +645,8 @@ function Invoke-GHRestMethodMultipleResult
|
|||
|
||||
try
|
||||
{
|
||||
do {
|
||||
do
|
||||
{
|
||||
$params = @{
|
||||
'UriFragment' = $nextLink
|
||||
'Method' = 'Get'
|
||||
|
@ -949,6 +950,10 @@ function Get-MediaAcceptHeader
|
|||
Text - Return a text only representation of the markdown body. Response will include body_text.
|
||||
Html - Return HTML rendered from the body's markdown. Response will include body_html.
|
||||
Full - Return raw, text and HTML representations. Response will include body, body_text, and body_html.
|
||||
Object - Return a json object representation a file or folder.
|
||||
|
||||
.PARAMETER AsJson
|
||||
If this switch is specified as +json value is appended to the MediaType header.
|
||||
|
||||
.PARAMETER AcceptHeader
|
||||
The accept header that should be included with the MediaType accept header.
|
||||
|
@ -960,16 +965,24 @@ function Get-MediaAcceptHeader
|
|||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Raw', 'Text', 'Html', 'Full')]
|
||||
[ValidateSet('Raw', 'Text', 'Html', 'Full', 'Object')]
|
||||
[string] $MediaType = 'Raw',
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[switch] $AsJson,
|
||||
|
||||
[string] $AcceptHeader
|
||||
)
|
||||
|
||||
$acceptHeaders = @(
|
||||
$AcceptHeader,
|
||||
"application/vnd.github.$mediaTypeVersion.$($MediaType.ToLower())+json")
|
||||
$resultHeaders = "application/vnd.github.$mediaTypeVersion.$($MediaType.ToLower())"
|
||||
if ($AsJson)
|
||||
{
|
||||
$resultHeaders = $resultHeaders + "+json"
|
||||
}
|
||||
|
||||
return ($acceptHeaders -join ',')
|
||||
if (-not [String]::IsNullOrEmpty($AcceptHeader))
|
||||
{
|
||||
$resultHeaders = "$AcceptHeader,$resultHeaders"
|
||||
}
|
||||
|
||||
return $resultHeaders
|
||||
}
|
||||
|
|
|
@ -315,7 +315,7 @@ function Get-GitHubIssue
|
|||
$params = @{
|
||||
'UriFragment' = $uriFragment + '?' + ($getParams -join '&')
|
||||
'Description' = $description
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AcceptHeader $symmetraAcceptHeader)
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson -AcceptHeader $symmetraAcceptHeader)
|
||||
'AccessToken' = $AccessToken
|
||||
'TelemetryEventName' = $MyInvocation.MyCommand.Name
|
||||
'TelemetryProperties' = $telemetryProperties
|
||||
|
@ -543,7 +543,7 @@ function New-GitHubIssue
|
|||
'Body' = (ConvertTo-Json -InputObject $hashBody)
|
||||
'Method' = 'Post'
|
||||
'Description' = "Creating new Issue ""$Title"" on $RepositoryName"
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AcceptHeader $symmetraAcceptHeader)
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson -AcceptHeader $symmetraAcceptHeader)
|
||||
'AccessToken' = $AccessToken
|
||||
'TelemetryEventName' = $MyInvocation.MyCommand.Name
|
||||
'TelemetryProperties' = $telemetryProperties
|
||||
|
@ -694,7 +694,7 @@ function Update-GitHubIssue
|
|||
'Body' = (ConvertTo-Json -InputObject $hashBody)
|
||||
'Method' = 'Patch'
|
||||
'Description' = "Updating Issue #$Issue on $RepositoryName"
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AcceptHeader $symmetraAcceptHeader)
|
||||
'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson -AcceptHeader $symmetraAcceptHeader)
|
||||
'AccessToken' = $AccessToken
|
||||
'TelemetryEventName' = $MyInvocation.MyCommand.Name
|
||||
'TelemetryProperties' = $telemetryProperties
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
'GitHubBranches.ps1',
|
||||
'GitHubCore.ps1',
|
||||
'GitHubComments.ps1',
|
||||
'GitHubContents.ps1',
|
||||
'GitHubEvents.ps1',
|
||||
'GitHubIssues.ps1',
|
||||
'GitHubLabels.ps1',
|
||||
|
@ -55,6 +56,7 @@
|
|||
'Get-GitHubCodeOfConduct',
|
||||
'Get-GitHubComment',
|
||||
'Get-GitHubConfiguration',
|
||||
'Get-GitHubContent',
|
||||
'Get-GitHubEmoji',
|
||||
'Get-GitHubEvent',
|
||||
'Get-GitHubGitIgnore',
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Tests for GitHubContents.ps1 module
|
||||
#>
|
||||
|
||||
# This is common test code setup logic for all Pester test files
|
||||
$moduleRootPath = Split-Path -Path $PSScriptRoot -Parent
|
||||
. (Join-Path -Path $moduleRootPath -ChildPath 'Tests\Common.ps1')
|
||||
|
||||
try
|
||||
{
|
||||
# Define Script-scoped, readonly, hidden variables.
|
||||
@{
|
||||
repoGuid = [Guid]::NewGuid().Guid
|
||||
readmeFileName = "README.md"
|
||||
}.GetEnumerator() | ForEach-Object {
|
||||
Set-Variable -Force -Scope Script -Option ReadOnly -Visibility Private -Name $_.Key -Value $_.Value
|
||||
}
|
||||
|
||||
# Need two separate blocks to set constants because we need to reference a constant from the first block in this block.
|
||||
@{
|
||||
htmlOutput = "<div id=`"file`" class=`"md`" data-path=`"README.md`"><article class=`"markdown-body entry-content`" itemprop=`"text`"><h1><a id=`"user-content-$repoGuid`" class=`"anchor`" aria-hidden=`"true`" href=`"#$repoGuid`"><svg class=`"octicon octicon-link`" viewBox=`"0 0 16 16`" version=`"1.1`" width=`"16`" height=`"16`" aria-hidden=`"true`"><path fill-rule=`"evenodd`" d=`"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z`"></path></svg></a>$repoGuid</h1></article></div>"
|
||||
rawOutput = "# $repoGuid"
|
||||
}.GetEnumerator() | ForEach-Object {
|
||||
Set-Variable -Force -Scope Script -Option ReadOnly -Visibility Private -Name $_.Key -Value $_.Value
|
||||
}
|
||||
|
||||
Describe 'Getting file and folder content' {
|
||||
# AutoInit will create a readme with the GUID of the repo name
|
||||
$repo = New-GitHubRepository -RepositoryName ($repoGuid) -AutoInit
|
||||
|
||||
Context 'For getting folder contents' {
|
||||
|
||||
$folderOutput = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name
|
||||
|
||||
It "Should have the expected name" {
|
||||
$folderOutput.name | Should be ""
|
||||
}
|
||||
It "Should have the expected path" {
|
||||
$folderOutput.path | Should be ""
|
||||
}
|
||||
It "Should have the expected type" {
|
||||
$folderOutput.type | Should be "dir"
|
||||
}
|
||||
It "Should have the expected entries" {
|
||||
$folderOutput.entries.length | Should be 1
|
||||
}
|
||||
It "Should have the expected entry data" {
|
||||
$folderOutput.entries[0].name | Should be $readmeFileName
|
||||
$folderOutput.entries[0].path | Should be $readmeFileName
|
||||
}
|
||||
}
|
||||
|
||||
Context 'For getting folder contents via URL' {
|
||||
|
||||
$folderOutput = Get-GitHubContent -Uri "https://github.com/$($script:ownerName)/$($repo.name)"
|
||||
|
||||
It "Should have the expected name" {
|
||||
$folderOutput.name | Should be ""
|
||||
}
|
||||
It "Should have the expected path" {
|
||||
$folderOutput.path | Should be ""
|
||||
}
|
||||
It "Should have the expected type" {
|
||||
$folderOutput.type | Should be "dir"
|
||||
}
|
||||
It "Should have the expected entries" {
|
||||
$folderOutput.entries.length | Should be 1
|
||||
}
|
||||
It "Should have the expected entry data" {
|
||||
$folderOutput.entries[0].name | Should be $readmeFileName
|
||||
$folderOutput.entries[0].path | Should be $readmeFileName
|
||||
}
|
||||
}
|
||||
|
||||
Context 'For getting raw (byte) file contents' {
|
||||
|
||||
$readmeFileBytes = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name -Path $readmeFileName -MediaType Raw
|
||||
$readmeFileString = [System.Text.Encoding]::UTF8.GetString($readmeFileBytes)
|
||||
|
||||
It "Should have the expected content" {
|
||||
$readmeFileString | Should be $rawOutput
|
||||
}
|
||||
}
|
||||
|
||||
Context 'For getting raw (string) file contents' {
|
||||
|
||||
$readmeFileString = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name -Path $readmeFileName -MediaType Raw -ResultAsString
|
||||
|
||||
It "Should have the expected content" {
|
||||
$readmeFileString | Should be $rawOutput
|
||||
}
|
||||
}
|
||||
|
||||
Context 'For getting html (byte) file contents' {
|
||||
|
||||
$readmeFileBytes = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name -Path $readmeFileName -MediaType Html
|
||||
$readmeFileString = [System.Text.Encoding]::UTF8.GetString($readmeFileBytes)
|
||||
|
||||
It "Should have the expected content" {
|
||||
# Replace newlines with empty for comparison
|
||||
$readmeFileString.Replace("`n", "").Replace("`r", "") | Should be $htmlOutput
|
||||
}
|
||||
}
|
||||
|
||||
Context 'For getting html (string) file contents' {
|
||||
|
||||
$readmeFileString = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name -Path $readmeFileName -MediaType Html -ResultAsString
|
||||
|
||||
It "Should have the expected content" {
|
||||
# Replace newlines with empty for comparison
|
||||
$readmeFileString.Replace("`n", "").Replace("`r", "") | Should be $htmlOutput
|
||||
}
|
||||
}
|
||||
|
||||
Context 'For getting object (default) file result' {
|
||||
|
||||
$readmeFileObject = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name -Path $readmeFileName
|
||||
|
||||
It "Should have the expected name" {
|
||||
$readmeFileObject.name | Should be $readmeFileName
|
||||
}
|
||||
It "Should have the expected path" {
|
||||
$readmeFileObject.path | Should be $readmeFileName
|
||||
}
|
||||
It "Should have the expected type" {
|
||||
$readmeFileObject.type | Should be "file"
|
||||
}
|
||||
It "Should have the expected encoding" {
|
||||
$readmeFileObject.encoding | Should be "base64"
|
||||
}
|
||||
|
||||
It "Should have the expected content" {
|
||||
# Convert from base64
|
||||
$readmeFileString = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($readmeFileObject.content))
|
||||
$readmeFileString | Should be $rawOutput
|
||||
}
|
||||
}
|
||||
|
||||
Context 'For getting object file result as string' {
|
||||
|
||||
$readmeFileObject = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name -Path $readmeFileName -MediaType Object -ResultAsString
|
||||
|
||||
It "Should have the expected name" {
|
||||
$readmeFileObject.name | Should be $readmeFileName
|
||||
}
|
||||
It "Should have the expected path" {
|
||||
$readmeFileObject.path | Should be $readmeFileName
|
||||
}
|
||||
It "Should have the expected type" {
|
||||
$readmeFileObject.type | Should be "file"
|
||||
}
|
||||
It "Should have the expected encoding" {
|
||||
$readmeFileObject.encoding | Should be "base64"
|
||||
}
|
||||
|
||||
It "Should have the expected content" {
|
||||
$readmeFileObject.contentAsString | Should be $rawOutput
|
||||
}
|
||||
}
|
||||
|
||||
Remove-GitHubRepository -Uri $repo.svn_url
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Test-Path -Path $script:originalConfigFile -PathType Leaf)
|
||||
{
|
||||
# Restore the user's configuration to its pre-test state
|
||||
Restore-GitHubConfiguration -Path $script:originalConfigFile
|
||||
$script:originalConfigFile = $null
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче