mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:34:27 +00:00 
			
		
		
		
	Support migration from AWS CodeCommit (#31981)
This PR adds support for migrating repos from [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/welcome.html). The access key ID and secret access key are required to get repository information and pull requests. And [HTTPS Git credentials](https://docs.aws.amazon.com/codecommit/latest/userguide/setting-up-gc.html) are required to clone the repository. <img src="https://github.com/user-attachments/assets/82ecb2d0-8d43-42b0-b5af-f5347a13b9d0" width="680" /> The AWS CodeCommit icon is from [AWS Architecture Icons](https://aws.amazon.com/architecture/icons/). <img src="https://github.com/user-attachments/assets/3c44d21f-d753-40f5-9eae-5d3589e0d50d" width="320" />
This commit is contained in:
		
							
								
								
									
										50
									
								
								assets/go-licenses.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										50
									
								
								assets/go-licenses.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										7
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								go.mod
									
									
									
									
									
								
							@@ -23,6 +23,9 @@ require (
 | 
			
		||||
	github.com/PuerkitoBio/goquery v1.9.2
 | 
			
		||||
	github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.2
 | 
			
		||||
	github.com/alecthomas/chroma/v2 v2.14.0
 | 
			
		||||
	github.com/aws/aws-sdk-go v1.43.21
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/credentials v1.17.30
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/service/codecommit v1.25.1
 | 
			
		||||
	github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
 | 
			
		||||
	github.com/blevesearch/bleve/v2 v2.4.2
 | 
			
		||||
	github.com/buildkite/terminal-to-html/v3 v3.12.1
 | 
			
		||||
@@ -146,6 +149,10 @@ require (
 | 
			
		||||
	github.com/andybalholm/cascadia v1.3.2 // indirect
 | 
			
		||||
	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
 | 
			
		||||
	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2 v1.30.4 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
 | 
			
		||||
	github.com/aws/smithy-go v1.20.4 // indirect
 | 
			
		||||
	github.com/aymerick/douceur v0.2.0 // indirect
 | 
			
		||||
	github.com/beorn7/perks v1.0.1 // indirect
 | 
			
		||||
	github.com/bits-and-blooms/bitset v1.13.0 // indirect
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								go.sum
									
									
									
									
									
								
							@@ -109,6 +109,20 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
 | 
			
		||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 | 
			
		||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
 | 
			
		||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
 | 
			
		||||
github.com/aws/aws-sdk-go v1.43.21 h1:E4S2eX3d2gKJyI/ISrcIrSwXwqjIvCK85gtBMt4sAPE=
 | 
			
		||||
github.com/aws/aws-sdk-go v1.43.21/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.30 h1:aau/oYFtibVovr2rDt8FHlU17BTicFEMAi29V1U+L5Q=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.30/go.mod h1:BPJ/yXV92ZVq6G8uYvbU0gSl8q94UB63nMT5ctNO38g=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.25.1 h1:mOOALIM4JzhYkq3voCBbmZqmyEVEhHsfasMTbVxLkNs=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.25.1/go.mod h1:6zf5j3mIUXKM0s2iz5ttR2Qwq+o47D0jotpAyaKgZRA=
 | 
			
		||||
github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
 | 
			
		||||
github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 | 
			
		||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 | 
			
		||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
 | 
			
		||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 | 
			
		||||
@@ -504,6 +518,9 @@ github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LF
 | 
			
		||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
 | 
			
		||||
github.com/jhillyerd/enmime v1.2.0 h1:dIu1IPEymQgoT2dzuB//ttA/xcV40NMPpQtmd4wslHk=
 | 
			
		||||
github.com/jhillyerd/enmime v1.2.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
 | 
			
		||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 | 
			
		||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 | 
			
		||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 | 
			
		||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 | 
			
		||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 | 
			
		||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 | 
			
		||||
@@ -894,6 +911,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
 | 
			
		||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 | 
			
		||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 | 
			
		||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 | 
			
		||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 | 
			
		||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 | 
			
		||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 | 
			
		||||
 
 | 
			
		||||
@@ -38,4 +38,7 @@ type MigrateOptions struct {
 | 
			
		||||
	ReleaseAssets   bool
 | 
			
		||||
	MigrateToRepoID int64
 | 
			
		||||
	MirrorInterval  string `json:"mirror_interval"`
 | 
			
		||||
 | 
			
		||||
	AWSAccessKeyID     string
 | 
			
		||||
	AWSSecretAccessKey string
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -300,6 +300,7 @@ const (
 | 
			
		||||
	OneDevService                           // 6 onedev service
 | 
			
		||||
	GitBucketService                        // 7 gitbucket service
 | 
			
		||||
	CodebaseService                         // 8 codebase service
 | 
			
		||||
	CodeCommitService                       // 9 codecommit service
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Name represents the service type's name
 | 
			
		||||
@@ -325,6 +326,8 @@ func (gt GitServiceType) Title() string {
 | 
			
		||||
		return "GitBucket"
 | 
			
		||||
	case CodebaseService:
 | 
			
		||||
		return "Codebase"
 | 
			
		||||
	case CodeCommitService:
 | 
			
		||||
		return "CodeCommit"
 | 
			
		||||
	case PlainGitService:
 | 
			
		||||
		return "Git"
 | 
			
		||||
	}
 | 
			
		||||
@@ -361,6 +364,9 @@ type MigrateRepoOptions struct {
 | 
			
		||||
	PullRequests   bool   `json:"pull_requests"`
 | 
			
		||||
	Releases       bool   `json:"releases"`
 | 
			
		||||
	MirrorInterval string `json:"mirror_interval"`
 | 
			
		||||
 | 
			
		||||
	AWSAccessKeyID     string `json:"aws_access_key_id"`
 | 
			
		||||
	AWSSecretAccessKey string `json:"aws_secret_access_key"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TokenAuth represents whether a service type supports token-based auth
 | 
			
		||||
@@ -382,6 +388,7 @@ var SupportedFullGitService = []GitServiceType{
 | 
			
		||||
	OneDevService,
 | 
			
		||||
	GitBucketService,
 | 
			
		||||
	CodebaseService,
 | 
			
		||||
	CodeCommitService,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RepoTransfer represents a pending repo transfer
 | 
			
		||||
 
 | 
			
		||||
@@ -1176,6 +1176,11 @@ migrate.gogs.description = Migrate data from notabug.org or other Gogs instances
 | 
			
		||||
migrate.onedev.description = Migrate data from code.onedev.io or other OneDev instances.
 | 
			
		||||
migrate.codebase.description = Migrate data from codebasehq.com.
 | 
			
		||||
migrate.gitbucket.description = Migrate data from GitBucket instances.
 | 
			
		||||
migrate.codecommit.description = Migrate data from AWS CodeCommit.
 | 
			
		||||
migrate.codecommit.aws_access_key_id = AWS Access Key ID
 | 
			
		||||
migrate.codecommit.aws_secret_access_key = AWS Secret Access Key
 | 
			
		||||
migrate.codecommit.https_git_credentials_username = HTTPS Git Credentials Username
 | 
			
		||||
migrate.codecommit.https_git_credentials_password = HTTPS Git Credentials Password
 | 
			
		||||
migrate.migrating_git = Migrating Git Data
 | 
			
		||||
migrate.migrating_topics = Migrating Topics
 | 
			
		||||
migrate.migrating_milestones = Migrating Milestones
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								public/assets/img/svg/gitea-codecommit.svg
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/assets/img/svg/gitea-codecommit.svg
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -10 100 100" class="svg gitea-codecommit" width="16" height="16" aria-hidden="true"><g fill="none" fill-rule="evenodd"><path fill="#C925D1" d="M0 0h80v80H0z"/><path fill="#FFF" d="M26.628 28.105h-2.017v-6.982c0-.558.36-.99.926-.99l7.144-.007v1.994H27.95l4.862 4.819-1.445 1.434-4.806-4.728zm28.07 10.867 1.869.827-6.541 14.446-1.868-.827zm1.311 10.493 4.003-2.89-3.526-3.535 1.458-1.422 4.36 4.373a1.002 1.002 0 0 1-.126 1.527l-4.963 3.58zm-9.043-8.802 1.205 1.633-4.061 2.932 3.538 3.536-1.454 1.424-4.374-4.373a1 1 0 0 1 .124-1.528zM69 24.13v42.858c0 .56-.458 1.012-1.024 1.012h-31.26c-.272 0-.53-.107-.723-.297a.96.96 0 0 1-.285-.7V55.034h2.018v10.971h29.256V25.113H37.726v-1.995h30.25c.566 0 1.024.453 1.024 1.012M33.182 34.588c0-1.927 1.585-3.495 3.535-3.495s3.535 1.568 3.535 3.495-1.585 3.495-3.535 3.495-3.535-1.568-3.535-3.495M17.549 66.009c-1.95 0-3.535-1.568-3.535-3.495s1.585-3.494 3.535-3.494 3.535 1.567 3.535 3.494-1.585 3.495-3.535 3.495m-3.535-23.442c0-1.927 1.585-3.495 3.535-3.495 1.982 0 3.535 1.535 3.535 3.495 0 1.927-1.585 3.495-3.535 3.495s-3.535-1.568-3.535-3.495m.004-25.081c0-1.925 1.584-3.491 3.53-3.491 1.948 0 3.532 1.566 3.532 3.49s-1.584 3.491-3.531 3.491-3.531-1.566-3.531-3.49m23.708 29.762v-7.276c2.57-.477 4.535-2.708 4.535-5.384 0-3.022-2.487-5.482-5.544-5.482s-5.545 2.46-5.545 5.482c0 2.676 1.966 4.907 4.536 5.384v7.276c0 1.163-.786 2.218-1.98 2.686l-10.451 4.1c-1.673.657-2.903 1.948-3.434 3.496-.433-.195-.801-.336-1.285-.416v-9.146c2.623-.433 4.535-2.687 4.535-5.401 0-2.764-1.878-4.972-4.535-5.393V22.889c2.626-.431 4.54-2.688 4.54-5.403 0-3.025-2.49-5.486-5.55-5.486S12 14.46 12 17.486c0 2.64 2.022 4.85 4.54 5.369v14.347c-2.515.518-4.536 2.727-4.536 5.365s2.02 4.846 4.536 5.365v9.217c-2.515.52-4.536 2.727-4.536 5.365 0 3.022 2.488 5.482 5.545 5.482s5.544-2.46 5.544-5.482a5.43 5.43 0 0 0-1.458-3.693c.167-1.27 1.066-2.384 2.397-2.905l10.45-4.1c1.98-.777 3.244-2.57 3.244-4.568"/></g></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.9 KiB  | 
@@ -169,6 +169,10 @@ func Migrate(ctx *context.APIContext) {
 | 
			
		||||
		opts.PullRequests = false
 | 
			
		||||
		opts.Releases = false
 | 
			
		||||
	}
 | 
			
		||||
	if gitServiceType == api.CodeCommitService {
 | 
			
		||||
		opts.AWSAccessKeyID = form.AWSAccessKeyID
 | 
			
		||||
		opts.AWSSecretAccessKey = form.AWSSecretAccessKey
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repo, err := repo_service.CreateRepositoryDirectly(ctx, ctx.Doer, repoOwner, repo_service.CreateRepoOptions{
 | 
			
		||||
		Name:           opts.RepoName,
 | 
			
		||||
 
 | 
			
		||||
@@ -231,6 +231,10 @@ func MigratePost(ctx *context.Context) {
 | 
			
		||||
		opts.PullRequests = false
 | 
			
		||||
		opts.Releases = false
 | 
			
		||||
	}
 | 
			
		||||
	if form.Service == structs.CodeCommitService {
 | 
			
		||||
		opts.AWSAccessKeyID = form.AWSAccessKeyID
 | 
			
		||||
		opts.AWSSecretAccessKey = form.AWSSecretAccessKey
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = repo_model.CheckCreateRepository(ctx, ctx.Doer, ctxUser, opts.RepoName, false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,8 @@ func ToGitServiceType(value string) structs.GitServiceType {
 | 
			
		||||
		return structs.OneDevService
 | 
			
		||||
	case "gitbucket":
 | 
			
		||||
		return structs.GitBucketService
 | 
			
		||||
	case "codecommit":
 | 
			
		||||
		return structs.CodeCommitService
 | 
			
		||||
	default:
 | 
			
		||||
		return structs.PlainGitService
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -79,6 +79,9 @@ type MigrateRepoForm struct {
 | 
			
		||||
	PullRequests   bool   `json:"pull_requests"`
 | 
			
		||||
	Releases       bool   `json:"releases"`
 | 
			
		||||
	MirrorInterval string `json:"mirror_interval"`
 | 
			
		||||
 | 
			
		||||
	AWSAccessKeyID     string `json:"aws_access_key_id"`
 | 
			
		||||
	AWSSecretAccessKey string `json:"aws_secret_access_key"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Validate validates the fields
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										269
									
								
								services/migrations/codecommit.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								services/migrations/codecommit.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,269 @@
 | 
			
		||||
// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package migrations
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	git_module "code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	base "code.gitea.io/gitea/modules/migration"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
 | 
			
		||||
	"github.com/aws/aws-sdk-go-v2/credentials"
 | 
			
		||||
	"github.com/aws/aws-sdk-go-v2/service/codecommit"
 | 
			
		||||
	"github.com/aws/aws-sdk-go-v2/service/codecommit/types"
 | 
			
		||||
	"github.com/aws/aws-sdk-go/aws"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	_ base.Downloader        = &CodeCommitDownloader{}
 | 
			
		||||
	_ base.DownloaderFactory = &CodeCommitDownloaderFactory{}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	RegisterDownloaderFactory(&CodeCommitDownloaderFactory{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CodeCommitDownloaderFactory defines a codecommit downloader factory
 | 
			
		||||
type CodeCommitDownloaderFactory struct{}
 | 
			
		||||
 | 
			
		||||
// New returns a Downloader related to this factory according MigrateOptions
 | 
			
		||||
func (c *CodeCommitDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
 | 
			
		||||
	u, err := url.Parse(opts.CloneAddr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hostElems := strings.Split(u.Host, ".")
 | 
			
		||||
	if len(hostElems) != 4 {
 | 
			
		||||
		return nil, fmt.Errorf("cannot get the region from clone URL")
 | 
			
		||||
	}
 | 
			
		||||
	region := hostElems[1]
 | 
			
		||||
 | 
			
		||||
	pathElems := strings.Split(u.Path, "/")
 | 
			
		||||
	if len(pathElems) == 0 {
 | 
			
		||||
		return nil, fmt.Errorf("cannot get the repo name from clone URL")
 | 
			
		||||
	}
 | 
			
		||||
	repoName := pathElems[len(pathElems)-1]
 | 
			
		||||
 | 
			
		||||
	baseURL := u.Scheme + "://" + u.Host
 | 
			
		||||
 | 
			
		||||
	return NewCodeCommitDownloader(ctx, repoName, baseURL, opts.AWSAccessKeyID, opts.AWSSecretAccessKey, region), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GitServiceType returns the type of git service
 | 
			
		||||
func (c *CodeCommitDownloaderFactory) GitServiceType() structs.GitServiceType {
 | 
			
		||||
	return structs.CodeCommitService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewCodeCommitDownloader(ctx context.Context, repoName, baseURL, accessKeyID, secretAccessKey, region string) *CodeCommitDownloader {
 | 
			
		||||
	downloader := CodeCommitDownloader{
 | 
			
		||||
		ctx:      ctx,
 | 
			
		||||
		repoName: repoName,
 | 
			
		||||
		baseURL:  baseURL,
 | 
			
		||||
		codeCommitClient: codecommit.New(codecommit.Options{
 | 
			
		||||
			Credentials: credentials.NewStaticCredentialsProvider(accessKeyID, secretAccessKey, ""),
 | 
			
		||||
			Region:      region,
 | 
			
		||||
		}),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &downloader
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CodeCommitDownloader implements a downloader for AWS CodeCommit
 | 
			
		||||
type CodeCommitDownloader struct {
 | 
			
		||||
	base.NullDownloader
 | 
			
		||||
	ctx               context.Context
 | 
			
		||||
	codeCommitClient  *codecommit.Client
 | 
			
		||||
	repoName          string
 | 
			
		||||
	baseURL           string
 | 
			
		||||
	allPullRequestIDs []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetContext set context
 | 
			
		||||
func (c *CodeCommitDownloader) SetContext(ctx context.Context) {
 | 
			
		||||
	c.ctx = ctx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRepoInfo returns a repository information
 | 
			
		||||
func (c *CodeCommitDownloader) GetRepoInfo() (*base.Repository, error) {
 | 
			
		||||
	output, err := c.codeCommitClient.GetRepository(c.ctx, &codecommit.GetRepositoryInput{
 | 
			
		||||
		RepositoryName: aws.String(c.repoName),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	repoMeta := output.RepositoryMetadata
 | 
			
		||||
 | 
			
		||||
	repo := &base.Repository{
 | 
			
		||||
		Name:      *repoMeta.RepositoryName,
 | 
			
		||||
		Owner:     *repoMeta.AccountId,
 | 
			
		||||
		IsPrivate: true, // CodeCommit repos are always private
 | 
			
		||||
		CloneURL:  *repoMeta.CloneUrlHttp,
 | 
			
		||||
	}
 | 
			
		||||
	if repoMeta.DefaultBranch != nil {
 | 
			
		||||
		repo.DefaultBranch = *repoMeta.DefaultBranch
 | 
			
		||||
	}
 | 
			
		||||
	if repoMeta.RepositoryDescription != nil {
 | 
			
		||||
		repo.DefaultBranch = *repoMeta.RepositoryDescription
 | 
			
		||||
	}
 | 
			
		||||
	return repo, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetComments returns comments of an issue or PR
 | 
			
		||||
func (c *CodeCommitDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
 | 
			
		||||
	var (
 | 
			
		||||
		nextToken *string
 | 
			
		||||
		comments  []*base.Comment
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		resp, err := c.codeCommitClient.GetCommentsForPullRequest(c.ctx, &codecommit.GetCommentsForPullRequestInput{
 | 
			
		||||
			NextToken:     nextToken,
 | 
			
		||||
			PullRequestId: aws.String(strconv.FormatInt(commentable.GetForeignIndex(), 10)),
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, false, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, prComment := range resp.CommentsForPullRequestData {
 | 
			
		||||
			for _, ccComment := range prComment.Comments {
 | 
			
		||||
				comment := &base.Comment{
 | 
			
		||||
					IssueIndex: commentable.GetForeignIndex(),
 | 
			
		||||
					PosterName: c.getUsernameFromARN(*ccComment.AuthorArn),
 | 
			
		||||
					Content:    *ccComment.Content,
 | 
			
		||||
					Created:    *ccComment.CreationDate,
 | 
			
		||||
					Updated:    *ccComment.LastModifiedDate,
 | 
			
		||||
				}
 | 
			
		||||
				comments = append(comments, comment)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		nextToken = resp.NextToken
 | 
			
		||||
		if nextToken == nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return comments, true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPullRequests returns pull requests according page and perPage
 | 
			
		||||
func (c *CodeCommitDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
 | 
			
		||||
	allPullRequestIDs, err := c.getAllPullRequestIDs()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	startIndex := (page - 1) * perPage
 | 
			
		||||
	endIndex := page * perPage
 | 
			
		||||
	if endIndex > len(allPullRequestIDs) {
 | 
			
		||||
		endIndex = len(allPullRequestIDs)
 | 
			
		||||
	}
 | 
			
		||||
	batch := allPullRequestIDs[startIndex:endIndex]
 | 
			
		||||
 | 
			
		||||
	prs := make([]*base.PullRequest, 0, len(batch))
 | 
			
		||||
	for _, id := range batch {
 | 
			
		||||
		output, err := c.codeCommitClient.GetPullRequest(c.ctx, &codecommit.GetPullRequestInput{
 | 
			
		||||
			PullRequestId: aws.String(id),
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, false, err
 | 
			
		||||
		}
 | 
			
		||||
		orig := output.PullRequest
 | 
			
		||||
		number, err := strconv.ParseInt(*orig.PullRequestId, 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("CodeCommit pull request id is not a number: %s", *orig.PullRequestId)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if len(orig.PullRequestTargets) == 0 {
 | 
			
		||||
			log.Error("CodeCommit pull request does not contain targets", *orig.PullRequestId)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		target := orig.PullRequestTargets[0]
 | 
			
		||||
		pr := &base.PullRequest{
 | 
			
		||||
			Number:     number,
 | 
			
		||||
			Title:      *orig.Title,
 | 
			
		||||
			PosterName: c.getUsernameFromARN(*orig.AuthorArn),
 | 
			
		||||
			Content:    *orig.Description,
 | 
			
		||||
			State:      "open",
 | 
			
		||||
			Created:    *orig.CreationDate,
 | 
			
		||||
			Updated:    *orig.LastActivityDate,
 | 
			
		||||
			Merged:     target.MergeMetadata.IsMerged,
 | 
			
		||||
			Head: base.PullRequestBranch{
 | 
			
		||||
				Ref:      strings.TrimPrefix(*target.SourceReference, git_module.BranchPrefix),
 | 
			
		||||
				SHA:      *target.SourceCommit,
 | 
			
		||||
				RepoName: c.repoName,
 | 
			
		||||
			},
 | 
			
		||||
			Base: base.PullRequestBranch{
 | 
			
		||||
				Ref:      strings.TrimPrefix(*target.DestinationReference, git_module.BranchPrefix),
 | 
			
		||||
				SHA:      *target.DestinationCommit,
 | 
			
		||||
				RepoName: c.repoName,
 | 
			
		||||
			},
 | 
			
		||||
			ForeignIndex: number,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if orig.PullRequestStatus == types.PullRequestStatusEnumClosed {
 | 
			
		||||
			pr.State = "closed"
 | 
			
		||||
			pr.Closed = orig.LastActivityDate
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_ = CheckAndEnsureSafePR(pr, c.baseURL, c)
 | 
			
		||||
		prs = append(prs, pr)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return prs, len(prs) < perPage, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormatCloneURL add authentication into remote URLs
 | 
			
		||||
func (c *CodeCommitDownloader) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) {
 | 
			
		||||
	u, err := url.Parse(remoteAddr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	u.User = url.UserPassword(opts.AuthUsername, opts.AuthPassword)
 | 
			
		||||
	return u.String(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *CodeCommitDownloader) getAllPullRequestIDs() ([]string, error) {
 | 
			
		||||
	if len(c.allPullRequestIDs) > 0 {
 | 
			
		||||
		return c.allPullRequestIDs, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		nextToken *string
 | 
			
		||||
		prIDs     []string
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		output, err := c.codeCommitClient.ListPullRequests(c.ctx, &codecommit.ListPullRequestsInput{
 | 
			
		||||
			RepositoryName: aws.String(c.repoName),
 | 
			
		||||
			NextToken:      nextToken,
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		prIDs = append(prIDs, output.PullRequestIds...)
 | 
			
		||||
		nextToken = output.NextToken
 | 
			
		||||
		if nextToken == nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.allPullRequestIDs = prIDs
 | 
			
		||||
	return c.allPullRequestIDs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *CodeCommitDownloader) getUsernameFromARN(arn string) string {
 | 
			
		||||
	parts := strings.Split(arn, "/")
 | 
			
		||||
	if len(parts) > 0 {
 | 
			
		||||
		return parts[len(parts)-1]
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										117
									
								
								templates/repo/migrate/codecommit.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								templates/repo/migrate/codecommit.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
			
		||||
{{template "base/head" .}}
 | 
			
		||||
<div role="main" aria-label="{{.Title}}" class="page-content repository new migrate">
 | 
			
		||||
	<div class="ui middle very relaxed page grid">
 | 
			
		||||
		<div class="column">
 | 
			
		||||
			<form class="ui form" action="{{.Link}}" method="post">
 | 
			
		||||
				{{template "base/disable_form_autofill"}}
 | 
			
		||||
				{{.CsrfTokenHtml}}
 | 
			
		||||
				<h3 class="ui top attached header">
 | 
			
		||||
					{{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}}
 | 
			
		||||
					<input id="service_type" type="hidden" name="service" value="{{.service}}">
 | 
			
		||||
				</h3>
 | 
			
		||||
				<div class="ui attached segment">
 | 
			
		||||
					{{template "base/alert" .}}
 | 
			
		||||
					<div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
 | 
			
		||||
						<label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
 | 
			
		||||
						<input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required>
 | 
			
		||||
						<span class="help">
 | 
			
		||||
						{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}}
 | 
			
		||||
						</span>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class="inline required field {{if .Err_Auth}}error{{end}}">
 | 
			
		||||
						<label for="aws_access_key_id">{{ctx.Locale.Tr "repo.migrate.codecommit.aws_access_key_id"}}</label>
 | 
			
		||||
						<input id="aws_access_key_id" name="aws_access_key_id" value="{{.aws_access_key_id}}" required>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="inline required field {{if .Err_Auth}}error{{end}}">
 | 
			
		||||
						<label for="aws_secret_access_key">{{ctx.Locale.Tr "repo.migrate.codecommit.aws_secret_access_key"}}</label>
 | 
			
		||||
						<input id="aws_secret_access_key" name="aws_secret_access_key" type="password" value="{{.aws_secret_access_key}}" required>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="inline required field {{if .Err_Auth}}error{{end}}">
 | 
			
		||||
						<label for="auth_username">{{ctx.Locale.Tr "repo.migrate.codecommit.https_git_credentials_username"}}</label>
 | 
			
		||||
						<input id="auth_username" name="auth_username" value="{{.auth_username}}" required>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="inline required field {{if .Err_Auth}}error{{end}}">
 | 
			
		||||
						<label for="auth_password">{{ctx.Locale.Tr "repo.migrate.codecommit.https_git_credentials_password"}}</label>
 | 
			
		||||
						<input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}" required>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					{{if not .DisableNewPullMirrors}}
 | 
			
		||||
					<div class="inline field">
 | 
			
		||||
						<label>{{ctx.Locale.Tr "repo.migrate_options"}}</label>
 | 
			
		||||
						<div class="ui checkbox">
 | 
			
		||||
							<input id="mirror" name="mirror" type="checkbox" {{if .mirror}} checked{{end}}>
 | 
			
		||||
							<label>{{ctx.Locale.Tr "repo.migrate_options_mirror_helper"}}</label>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
					{{end}}
 | 
			
		||||
 | 
			
		||||
					<div id="migrate_items">
 | 
			
		||||
						<div class="inline field">
 | 
			
		||||
							<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 | 
			
		||||
							<div class="ui checkbox">
 | 
			
		||||
								<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
 | 
			
		||||
								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class="divider"></div>
 | 
			
		||||
 | 
			
		||||
					<div class="inline required field {{if .Err_Owner}}error{{end}}">
 | 
			
		||||
						<label>{{ctx.Locale.Tr "repo.owner"}}</label>
 | 
			
		||||
						<div class="ui selection owner dropdown">
 | 
			
		||||
							<input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required>
 | 
			
		||||
							<span class="text truncated-item-container" title="{{.ContextUser.Name}}">
 | 
			
		||||
								{{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}}
 | 
			
		||||
								<span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span>
 | 
			
		||||
							</span>
 | 
			
		||||
							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 | 
			
		||||
							<div class="menu" title="{{.SignedUser.Name}}">
 | 
			
		||||
								<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}">
 | 
			
		||||
									{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
 | 
			
		||||
									<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
 | 
			
		||||
								</div>
 | 
			
		||||
								{{range .Orgs}}
 | 
			
		||||
									<div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}">
 | 
			
		||||
										{{ctx.AvatarUtils.Avatar . 28 "mini"}}
 | 
			
		||||
										<span class="truncated-item-name">{{.ShortName 40}}</span>
 | 
			
		||||
									</div>
 | 
			
		||||
								{{end}}
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class="inline required field {{if .Err_RepoName}}error{{end}}">
 | 
			
		||||
						<label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label>
 | 
			
		||||
						<input id="repo_name" name="repo_name" value="{{.repo_name}}" required maxlength="100">
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="inline field">
 | 
			
		||||
						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 | 
			
		||||
						<div class="ui checkbox">
 | 
			
		||||
							{{if .IsForcedPrivate}}
 | 
			
		||||
								<input name="private" type="checkbox" checked disabled>
 | 
			
		||||
								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 | 
			
		||||
							{{else}}
 | 
			
		||||
								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
 | 
			
		||||
								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 | 
			
		||||
							{{end}}
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="inline field {{if .Err_Description}}error{{end}}">
 | 
			
		||||
						<label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label>
 | 
			
		||||
						<textarea id="description" name="description" maxlength="2048">{{.description}}</textarea>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class="inline field">
 | 
			
		||||
						<label></label>
 | 
			
		||||
						<button class="ui primary button">
 | 
			
		||||
							{{ctx.Locale.Tr "repo.migrate_repo"}}
 | 
			
		||||
						</button>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</form>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
{{template "base/footer" .}}
 | 
			
		||||
							
								
								
									
										8
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							@@ -22615,6 +22615,14 @@
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "x-go-name": "AuthUsername"
 | 
			
		||||
        },
 | 
			
		||||
        "aws_access_key_id": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "x-go-name": "AWSAccessKeyID"
 | 
			
		||||
        },
 | 
			
		||||
        "aws_secret_access_key": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "x-go-name": "AWSSecretAccessKey"
 | 
			
		||||
        },
 | 
			
		||||
        "clone_addr": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "x-go-name": "CloneAddr"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								web_src/svg/gitea-codecommit.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								web_src/svg/gitea-codecommit.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
<svg width="80" height="80" viewBox="-10 -10 100 100" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
    <g fill="none" fill-rule="evenodd">
 | 
			
		||||
        <path d="M0 0h80v80H0z" fill="#C925D1"/>
 | 
			
		||||
        <path d="M26.628 28.105h-2.017v-6.982c0-.558.36-.99.926-.99l7.144-.007v1.994H27.95l4.862 4.819-1.445 1.434-4.806-4.728.067 4.46zm28.07 10.867l1.869.827-6.541 14.446-1.868-.827 6.54-14.446zm1.311 10.493l4.003-2.89-3.526-3.535 1.458-1.422 4.36 4.373a1.002 1.002 0 0 1-.126 1.527l-4.963 3.58-1.206-1.633zm-9.043-8.802l1.205 1.633-4.061 2.932 3.538 3.536-1.454 1.424-4.374-4.373a1 1 0 0 1 .124-1.528l5.022-3.624zM69 24.13v42.858c0 .56-.458 1.012-1.024 1.012h-31.26c-.272 0-.53-.107-.723-.297a.958.958 0 0 1-.285-.7V55.034h2.018v10.971h29.256V25.113H37.726v-1.995h30.25c.566 0 1.024.453 1.024 1.012zM33.182 34.588c0-1.927 1.585-3.495 3.535-3.495 1.95 0 3.535 1.568 3.535 3.495s-1.585 3.495-3.535 3.495c-1.95 0-3.535-1.568-3.535-3.495zM17.549 66.009c-1.95 0-3.535-1.568-3.535-3.495s1.585-3.494 3.535-3.494c1.95 0 3.535 1.567 3.535 3.494 0 1.927-1.585 3.495-3.535 3.495zm-3.535-23.442c0-1.927 1.585-3.495 3.535-3.495 1.982 0 3.535 1.535 3.535 3.495 0 1.927-1.585 3.495-3.535 3.495-1.95 0-3.535-1.568-3.535-3.495zm.004-25.081c0-1.925 1.584-3.491 3.53-3.491 1.948 0 3.532 1.566 3.532 3.49 0 1.925-1.584 3.491-3.531 3.491s-3.531-1.566-3.531-3.49zm23.708 29.762v-7.276c2.57-.477 4.535-2.708 4.535-5.384 0-3.022-2.487-5.482-5.544-5.482-3.057 0-5.545 2.46-5.545 5.482 0 2.676 1.966 4.907 4.536 5.384v7.276c0 1.163-.786 2.218-1.98 2.686l-10.451 4.1c-1.673.657-2.903 1.948-3.434 3.496-.433-.195-.801-.336-1.285-.416v-9.146c2.623-.433 4.535-2.687 4.535-5.401 0-2.764-1.878-4.972-4.535-5.393V22.889c2.626-.431 4.54-2.688 4.54-5.403 0-3.025-2.49-5.486-5.55-5.486C14.489 12 12 14.46 12 17.486c0 2.64 2.022 4.85 4.54 5.369v14.347c-2.515.518-4.536 2.727-4.536 5.365 0 2.638 2.02 4.846 4.536 5.365v9.217c-2.515.52-4.536 2.727-4.536 5.365 0 3.022 2.488 5.482 5.545 5.482 3.056 0 5.544-2.46 5.544-5.482a5.425 5.425 0 0 0-1.458-3.693c.167-1.27 1.066-2.384 2.397-2.905l10.45-4.1c1.98-.777 3.244-2.57 3.244-4.568z" fill="#FFF"/>
 | 
			
		||||
    </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 2.1 KiB  | 
		Reference in New Issue
	
	Block a user