Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/gitee provider #1403

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Set up a development environment on any infrastructure, with a single command.
* __Configuration File Support__: Initially support for [dev container](https://containers.dev/), ability to expand to DevFile, Nix & Flox (Contributions welcome here!).
* __Prebuilds System__: Drastically improve environment setup times (Contributions welcome here!).
* __IDE Support__ : Seamlessly supports [VS Code](https://github.com/microsoft/vscode) & [JetBrains](https://www.jetbrains.com/remote-development/gateway/) locally, ready to use without configuration. Includes a built-in Web IDE for added convenience.
* __Git Provider Integration__: GitHub, GitLab, Bitbucket, Bitbucket Server, Gitea, Gitness, Azure DevOps, AWS CodeCommit & Gogs can be connected, allowing easy repo branch or PR pull and commit back from the workspaces.
* __Git Provider Integration__: GitHub, GitLab, Bitbucket, Bitbucket Server, Gitea, Gitee, Gitness, Azure DevOps, AWS CodeCommit & Gogs can be connected, allowing easy repo branch or PR pull and commit back from the workspaces.
* __Multiple Project Workspace__: Support for multiple project repositories in the same workspace, making it easy to develop using a micro-service architecture.
* __Reverse Proxy Integration__: Enable collaboration and streamline feedback loops by leveraging reverse proxy functionality. Access preview ports and the Web IDE seamlessly, even behind firewalls.
* __Extensibility__: Enable extensibility with plugin or provider development. Moreover, in any dynamic language, not just Go(Contributions welcome here!).
Expand Down Expand Up @@ -165,7 +165,7 @@ This initiates the Daytona Server in daemon mode. Use the command:
daytona server
```
__2. Add Your Git Provider of Choice:__
Daytona supports GitHub, GitLab, Bitbucket, Bitbucket Server, Gitea, Gitness, AWS CodeCommit, Azure DevOps and Gogs. To add them to your profile, use the command:
Daytona supports GitHub, GitLab, Bitbucket, Bitbucket Server, Gitea, Gitee, Gitness, AWS CodeCommit, Azure DevOps and Gogs. To add them to your profile, use the command:
```bash
daytona git-providers add

Expand Down
7 changes: 7 additions & 0 deletions cmd/daytona/config/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func GetSupportedGitProviders() []GitProvider {
{"azure-devops", "Azure DevOps"},
{"aws-codecommit", "AWS CodeCommit"},
{"gogs", "Gogs"},
{"gitee", "Gitee"},
}
}

Expand All @@ -80,6 +81,8 @@ func GetDocsLinkFromGitProvider(providerId string) string {
return "https://docs.codeberg.org/advanced/access-token/"
case "gitea":
return "https://docs.gitea.com/1.21/development/api-usage#generating-and-listing-api-tokens"
case "gitee":
return "https://gitee.com/profile/personal_access_tokens"
case "gitness":
return "https://docs.gitness.com/administration/user-management#generate-user-token"
case "azure-devops":
Expand All @@ -105,6 +108,8 @@ func GetDocsLinkForCommitSigning(providerId string) string {
return "https://learn.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops"
case "aws-codecommit":
return "https://docs.aws.amazon.com/codecommit/latest/userguide/setting-up-ssh-unixes.html"
case "gitee":
return "https://gitee.com/help/articles/4181 and for GPG signing see https://gitee.com/help/articles/4248"
default:
return ""
}
Expand All @@ -128,6 +133,8 @@ func GetRequiredScopesFromGitProviderId(providerId string) string {
fallthrough
case "gitea":
return "read:organization,write:repository,read:user"
case "gitee":
return "projects, pull_requests, issues, notes"
case "gitness":
return "/"
case "azure-devops":
Expand Down
296 changes: 296 additions & 0 deletions pkg/gitprovider/gitee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
// Copyright 2024 Daytona Platforms Inc.
// SPDX-License-Identifier: Apache-2.0

package gitprovider

import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
)

const giteeApiUrl = "https://gitee.com/api/v5"

type GiteeGitProvider struct {
*AbstractGitProvider

token string
baseApiUrl string
}

func NewGiteeGitProvider(token string, baseApiUrl string) *GiteeGitProvider {
if baseApiUrl == "" {
baseApiUrl = giteeApiUrl
}

provider := &GiteeGitProvider{
token: token,
baseApiUrl: baseApiUrl,
AbstractGitProvider: &AbstractGitProvider{},
}
provider.AbstractGitProvider.GitProvider = provider

return provider
}

func (g *GiteeGitProvider) CanHandle(repoUrl string) (bool, error) {
// Handle SSH URLs first
if strings.HasPrefix(repoUrl, "git@") {
parts := strings.Split(strings.TrimPrefix(repoUrl, "git@"), ":")
return len(parts) > 0 && parts[0] == "gitee.com", nil
}

// Handle HTTPS URLs
u, err := url.Parse(repoUrl)
if err != nil {
return false, nil // Return false without error for invalid URLs
}

return strings.Contains(u.Host, "gitee.com"), nil
}

func (g *GiteeGitProvider) GetUser() (*GitUser, error) {
url := fmt.Sprintf("%s/user", g.baseApiUrl)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}

req.Header.Set("Authorization", fmt.Sprintf("token %s", g.token))

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get user info: %s", resp.Status)
}

var user struct {
ID int `json:"id"`
Login string `json:"login"`
Name string `json:"name"`
Email string `json:"email"`
}

if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, err
}

return &GitUser{
Id: strconv.Itoa(user.ID),
Username: user.Login,
Name: user.Name,
Email: user.Email,
}, nil
}

func (g *GiteeGitProvider) GetNamespaces(options ListOptions) ([]*GitNamespace, error) {
url := fmt.Sprintf("%s/user/orgs", g.baseApiUrl)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}

req.Header.Set("Authorization", fmt.Sprintf("token %s", g.token))

// Add pagination parameters
q := req.URL.Query()
q.Add("page", strconv.Itoa(options.Page))
q.Add("per_page", strconv.Itoa(options.PerPage))
req.URL.RawQuery = q.Encode()

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get namespaces: %s", resp.Status)
}

var orgs []struct {
ID int `json:"id"`
Path string `json:"path"`
Name string `json:"name"`
}

if err := json.NewDecoder(resp.Body).Decode(&orgs); err != nil {
return nil, err
}

namespaces := make([]*GitNamespace, 0, len(orgs))
for _, org := range orgs {
namespaces = append(namespaces, &GitNamespace{
Id: strconv.Itoa(org.ID),
Name: org.Name,
})
}

// Add personal namespace on first page
if options.Page == 1 {
user, err := g.GetUser()
if err != nil {
return nil, err
}
namespaces = append([]*GitNamespace{{
Id: personalNamespaceId,
Name: user.Username,
}}, namespaces...)
}

return namespaces, nil
}

func (g *GiteeGitProvider) GetRepositories(namespace string, options ListOptions) ([]*GitRepository, error) {
var url string
if namespace == personalNamespaceId {
url = fmt.Sprintf("%s/user/repos", g.baseApiUrl)
} else {
url = fmt.Sprintf("%s/orgs/%s/repos", g.baseApiUrl, namespace)
}

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}

req.Header.Set("Authorization", fmt.Sprintf("token %s", g.token))

// Add pagination parameters
q := req.URL.Query()
q.Add("page", strconv.Itoa(options.Page))
q.Add("per_page", strconv.Itoa(options.PerPage))
req.URL.RawQuery = q.Encode()

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get repositories: %s", resp.Status)
}

var repos []struct {
ID int `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
DefaultBranch string `json:"default_branch"`
HTMLURL string `json:"html_url"`
SSHURL string `json:"ssh_url"`
Private bool `json:"private"`
Fork bool `json:"fork"`
Owner struct {
Login string `json:"login"`
} `json:"owner"`
}

if err := json.NewDecoder(resp.Body).Decode(&repos); err != nil {
return nil, err
}

repositories := make([]*GitRepository, 0, len(repos))
for _, repo := range repos {
repositories = append(repositories, &GitRepository{
Id: strconv.Itoa(repo.ID),
Name: repo.Name,
Branch: repo.DefaultBranch,
Url: repo.HTMLURL,
Owner: repo.Owner.Login,
Source: "gitee.com",
})
}

return repositories, nil
}

func (g *GiteeGitProvider) ParseStaticGitContext(repoUrl string) (*StaticGitContext, error) {
// Handle SSH URLs ([email protected]:owner/repo.git)
if strings.HasPrefix(repoUrl, "git@") {
sshParts := strings.Split(strings.TrimPrefix(repoUrl, "[email protected]:"), "/")
if len(sshParts) < 2 {
return nil, fmt.Errorf("invalid SSH URL format")
}
owner := sshParts[0]
name := strings.TrimSuffix(sshParts[1], ".git")
return &StaticGitContext{
Source: "gitee.com",
Owner: owner,
Name: name,
Url: repoUrl,
}, nil
}

// Handle HTTPS URLs
u, err := url.Parse(repoUrl)
if err != nil {
return nil, err
}

if !strings.Contains(u.Host, "gitee.com") {
return nil, fmt.Errorf("not a Gitee URL")
}

parts := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(parts) < 2 {
return nil, fmt.Errorf("invalid URL format")
}

context := &StaticGitContext{
Source: u.Host,
Owner: parts[0],
Name: parts[1],
Url: repoUrl,
}

if len(parts) > 2 {
switch parts[2] {
case "tree":
if len(parts) > 3 {
branch := parts[3]
context.Branch = &branch
}
case "commit":
if len(parts) > 3 {
sha := parts[3]
context.Sha = &sha
}
case "blob":
if len(parts) > 4 {
branch := parts[3]
context.Branch = &branch
path := strings.Join(parts[4:], "/")
context.Path = &path
}
case "pulls":
if len(parts) > 3 {
prNum, err := strconv.ParseUint(parts[3], 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid pull request number: %v", err)
}
prNumber := uint32(prNum)
context.PrNumber = &prNumber
}
}
}

return context, nil
}

func (g *GiteeGitProvider) GetUrlFromRepo(repo *GitRepository, branch string) string {
if branch == "" {
return fmt.Sprintf("https://gitee.com/%s/%s", repo.Owner, repo.Name)
}
return fmt.Sprintf("https://gitee.com/%s/%s/tree/%s", repo.Owner, repo.Name, branch)
}
Loading