Initial commit

This commit is contained in:
HJJ 2024-04-07 17:49:39 +08:00 committed by HJJ
commit e8a74c0293
48 changed files with 3143 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
config/config.yml
bin
.idea

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 HJJ
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

255
README.md Normal file
View File

@ -0,0 +1,255 @@
## Gitee CLI
> 诞生背景:对于开发者来说,日常的开发往往离不开 terminal以往的流程一般是编写代码 -> 提交代码 -> 创建 Pull Request -> 测试 -> Bug Fix -> 重新测试 -> 测试通过 -> Code Review -> 合入主线,
> 创建 Pull Request 等一系列动作往往只能在 web 端进行操作往往需要切换上下文Gitee CLI 因而诞生,旨在减少上下文的切换.
## 部分功能示例
![img.png](doc/images/preview.png)
## Usage
### 构建方式
```shell
git clone https://github.com/JJ-H/gitee_cli.git
cd gitee_cli
mkdir $HOME/.gitee
cp config/config.yml.example $HOME/.gitee/config.yml
go build -o bin/gitee main.go
sudo cp ./bin/gitee /usr/local/bin/gitee
```
### 配置说明
```shell
# 个人私人令牌,用于 V5 鉴权
access_token: xxxxxxxx
api_prefix: https://gitee.com/api/v5
# 用户在 Gitee 上的 ID
user_id: xxxxx
# 用户名
user_name: xxx
# 非仓库目录下执行 gitee cli 命令默认仓库全路径(配置你比较常用的仓库)
default_path_with_namespace: oschina/gitee
# 专业版构建命令前缀
premium_build_prefix: premium_ci_build
# 企业版构建命令前缀
saas_build_prefix: ci_deploy
# cookie 用于企业版 API 鉴权(由于加密登录的问题,此处暂时需要手动复制 cookie[可使用 gitee config cookie xxxxxx]
cookies_jar: xxxxxxx
```
#### 可通过 gitee config [key] [value] 的方式设置
```shell
gitee config access_token xxxxxx
# 用户 ID 可使用如下命令查询(请精准输入你的username)
➜ ~ gitee user search JJ-H
用户 ID7484706
用户名称JJ-H
用户主页https://gitee.com/JJ-H
```
### Tab 自动补全!!!(强烈建议不要跳过这一步)
> 得益于框架的优秀设计Gitee CLI 支持快速生成 Tab 自动补全脚本,目前支持 bash、fish、powershell、zsh
```shell
➜ ~ gitee completion --help
Generate the autocompletion script for gitee for the specified shell.
See each sub-command's help for details on how to use the generated script.
Usage:
gitee completion [command]
Available Commands:
bash Generate the autocompletion script for bash
fish Generate the autocompletion script for fish
powershell Generate the autocompletion script for powershell
zsh Generate the autocompletion script for zsh
Flags:
-h, --help help for completion
Use "gitee completion [command] --help" for more information about a command.
```
#### 下面以 zsh 为例
```shell
# Linux 用户
gitee completion zsh > "${fpath[1]}/_gitee"
# 执行完毕后在 ~/.zshrc 中增加如下语句:
source ${fpath[1]}/_gitee
# macOS 用户
gitee completion zsh > $(brew --prefix)/share/zsh/site-functions/_gitee
# 执行完毕后在 ~/.zshrc 中增加如下语句:
source $(brew --prefix)/share/zsh/site-functions/_gitee
```
重启终端,输入 gitee 按下 tab您将得到如下自动补全提示子命令同样支持
```shell
➜ ~ gitee [press tab]
auth -- Authenticate Gitee CLI with gitee selector
build -- Build a k8s pod by note a specified pull request
completion -- Generate the autocompletion script for the specified shell
config -- Manage Gitee CLI config, Usage: config key [value]
selector -- Manage enterprises
help -- Help about any command
lightPr -- Create a lightPr
pr -- Manage pull requests
ssh-key -- Manage ssh-keys
user -- User related command
```
### 使用方式
```shell
➜ ~ gitee --help
Gitee CLI is a tool which interact with gitee server seamlessly via terminal
Usage:
gitee [command]
Available Commands:
auth Authenticate Gitee CLI with gitee selector_tui
completion Generate the autocompletion script for the specified shell
config Manage Gitee CLI config, Usage: config key [value]
enterprise Manage enterprises
help Help about any command
issue Manage issues
pr Manage pull requests
ssh-key Manage ssh-keys
user User related command
Flags:
-h, --help help for gitee
-v, --version version for gitee
Use "gitee [command] --help" for more information about a command.
```
### Auth 相关
```shell
➜ ~ gitee auth --help
Authenticate Gitee CLI with gitee selector
Usage:
gitee auth [flags]
Flags:
-f, --cookies-file string path to a file containing cookies
-h, --help help for auth
```
### Config 相关
```shell
➜ ~ gitee config --help
Manage Gitee CLI config, Usage: config key [value]
Usage:
gitee config [flags]
Flags:
-h, --help help for config
```
### Pull Request 相关
- 列出当前所在仓库下我审查的 Pull Request `gitee pr list [flags]`
> 说明:列表模式下,按 c 将拷贝 pull request iid 至粘贴板,按 v 预览详情,按 d 预览 diff回车使用浏览器打开
![img.png](doc/images/pull_request_list.png)
- 根据 commit 找到对应的被合入至当前分支的 Pull Request `gitee pr list -c <commit>`
```shell
➜ ~ gitee pr -c "80b4ef95c0d"
请在仓库目录下执行该命令!
➜ ~ cd /home/git/gitee
➜ gitee (master) ✔ gitee pr -c "80b4ef95c0d"
该 commit 由 PR: 「修改仓库模糊查询支持namespace级联查询修复全英文字符查询时只匹配path问题」 合入,访问地址: https://gitee.com/hightest/settings/pulls19977
```
- 创建 pull_request
```shell
➜ ~ gitee pr create
请输入标题feature -> master
请输入目标分支master
? 填写 Pull Request 内容 <Received>
创建 PR「feature -> master」 成功访问地址https://gitee.com/hightest/settings/pulls/3
```
- 评论 Pull request一般用于触发 webhook
```shell
➜ gitee (master) ✔ gitee pr note -i 19995 /approve
评论成功!
```
### Issue 相关
- 创建 issue
```shell
➜ ~ gitee issue create --feature
请选择要创建的任务类型
> 需求
Press q to quit.
请输入标题 这是需求标题
? 填写 Issue 描述 <Received>
创建工作项 「需这是需求标题」成功访问地址https://gitee.com/kepler-planet-wireless/dashboard/issues?id=I9A7ZY
```
- issue 列表
> 说明:列表模式下,按 c 将拷贝 issue ident 至粘贴板,按 v 预览详情,回车使用浏览器打开
![img.png](doc/images/issue_list.png)
### SSH Key 相关
```shell
➜ ~ gitee ssh-key --help
Manage ssh-keys
Usage:
gitee ssh-key [command]
Available Commands:
add Add a ssh pub key for personal
delete delete a specified ssh key
list List personal ssh pub keys
Flags:
-h, --help help for ssh-key
Use "gitee ssh-key [command] --help" for more information about a command.
```
- 获取当前账户所有已上传的公钥 `gitee ssh-key list`
```shell
➜ gitee (master) ✔ gitee ssh-key list
+--------------+----------------------------------------------------+--------------------------------+
| SSH KEY ID | KEY SHA | PREVIEW URL |
+--------------+----------------------------------------------------+--------------------------------+
| 3123223 | ssh-rsa AAAAB3NzaC1yc2EAAAADAAXCSAABAQC6r/S6pJsv8x | https://gitee.com/keys/3123223 |
| 3233333 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQ786AABgQCgiABu1TWbSI | https://gitee.com/keys/3233333 |
| 3242234 | ssh-ed25519 AAAAC3NzaC1lZDI1N765SAAAIISV/On6vy1UNg | https://gitee.com/keys/3242234 |
| 2322332 | ssh-rsa AAAAB3NzaC1yc2EAAAADAADSDAABAQCpKcep+/DlEb | https://gitee.com/keys/2322332 |
| 1233562 | ssh-ed25519 AAAAC3NzaC1lZDIASDSASAAAIA9aZBvftMp1dT | https://gitee.com/keys/1233562 |
+--------------+----------------------------------------------------+--------------------------------+
```
- 添加本地 ssh pub key
```shell
➜ ~ gitee ssh-key add -t "Macbook Pro"
请选择要上传的 SSH 公钥
/Users/JJ-H/.ssh/hexo-deploy-key.pub
/Users/JJ-H/.ssh/id_ed25519.pub
> /Users/JJ-H/.ssh/id_rsa.pub
Press q to quit.
添加 ssh key 「Macbook Pro」 成功访问地址https://gitee.com/keys/449311
```
- 删除已上传的ssh 公钥
```shell
➜ ~ gitee ssh-key delete 449311
删除公钥成功
```
### 其余功能各位自行通过 help 探索~
### 参考文献
- [A Git command to jump from a commit SHA to the PR on GitHub](https://tekin.co.uk/2020/06/jump-from-a-git-commit-to-the-pr-in-one-command)

52
cmd/auth/auth.go Normal file
View File

@ -0,0 +1,52 @@
package auth
import (
"fmt"
"gitee_cli/internal/api/user"
"gitee_cli/utils"
"os"
"gitee_cli/config"
"github.com/spf13/cobra"
)
var (
CookiesFile string
GlobalCookies string
)
var AuthCmd = &cobra.Command{
Use: "auth",
Short: "Authenticate Gitee CLI with gitee selector_tui",
Run: func(cmd *cobra.Command, args []string) {
if CookiesFile != "" {
// Read cookies from file
cookies, err := os.ReadFile(CookiesFile)
if err != nil {
fmt.Println("Failed to read cookies-file:", err)
return
}
GlobalCookies = string(cookies)
}
// Save cookies to file for future usage
if GlobalCookies != "" {
if err := config.Update(map[string]interface{}{
"cookies_jar": GlobalCookies,
}); err != nil {
fmt.Println("Failed to save cookies:", err)
return
}
}
user, err := user.BasicUser()
if err != nil {
fmt.Println("Authorize error: ", err)
return
}
fmt.Println(fmt.Sprintf("Hi「%s」! You've %s authenticated", utils.Cyan(user.Name), utils.Green("successfully")))
},
}
func init() {
AuthCmd.Flags().StringVarP(&CookiesFile, "cookies-file", "f", "", "path to a file containing cookies")
}

48
cmd/config.go Normal file
View File

@ -0,0 +1,48 @@
package cmd
import (
"fmt"
"gitee_cli/config"
// "os"
// "gitee_cli/config"
"github.com/spf13/cobra"
)
var ConfigCmdUsage = "Manage Gitee CLI config, Usage: config key [value]"
var ConfigCmd = &cobra.Command{
Use: "config",
Short: ConfigCmdUsage,
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
fmt.Println("No config key provided.")
return
}
if len(args) == 1 {
key := args[0]
value, err := config.Read(key)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(value)
return
}
if len(args) == 2 {
key := args[0]
value := args[1]
if err := config.Update(map[string]interface{}{
key: value,
}); err != nil {
fmt.Println(err)
return
}
fmt.Println(value)
return
}
fmt.Println(ConfigCmdUsage)
},
}

View File

@ -0,0 +1,11 @@
package enterprise
import (
"github.com/spf13/cobra"
)
var EntCmd = &cobra.Command{
Use: "enterprise",
Aliases: []string{"ent"},
Short: "Manage enterprises",
}

44
cmd/enterprise/list.go Normal file
View File

@ -0,0 +1,44 @@
package enterprise
import (
enterprises2 "gitee_cli/internal/api/enterprises"
"gitee_cli/utils/tui"
"github.com/charmbracelet/bubbles/table"
"github.com/fatih/color"
"github.com/spf13/cobra"
"os"
"strconv"
)
var ListCmd = &cobra.Command{
Use: "list",
Short: "List all enterprises joined by me",
Run: func(cmd *cobra.Command, args []string) {
enterprises, err := enterprises2.List()
if err != nil {
color.Red("获取企业列表失败!")
return
}
columns := []table.Column{
{Title: "ID", Width: 8},
{Title: "Name", Width: 28},
{Title: "Path", Width: 28},
}
rows := make([]table.Row, 0)
for _, ent := range enterprises {
rows = append(rows, table.Row{strconv.Itoa(ent.Id), ent.Name, ent.Path})
}
entTable := tui.NewTable(enterprises2.Enterprise{}, tui.Enterprise, columns, rows)
if _, err := entTable.Run(); err != nil {
color.Red("企业渲染失败!")
os.Exit(1)
}
},
}
func init() {
EntCmd.AddCommand(ListCmd)
}

9
cmd/generate-fig-spec.go Normal file
View File

@ -0,0 +1,9 @@
package cmd
import (
"github.com/withfig/autocomplete-tools/integrations/cobra"
)
func init() {
RootCmd.AddCommand(cobracompletefig.CreateCompletionSpecCommand())
}

180
cmd/issue/create.go Normal file
View File

@ -0,0 +1,180 @@
package issue
import (
"fmt"
"gitee_cli/config"
"gitee_cli/internal/api/issue"
"gitee_cli/internal/api/issue_type"
"gitee_cli/internal/api/member"
"gitee_cli/utils"
"gitee_cli/utils/tui"
"gitee_cli/utils/tui/issue_type_tui"
"gitee_cli/utils/tui/selector_tui"
tea "github.com/charmbracelet/bubbletea"
"github.com/fatih/color"
"github.com/spf13/cobra"
"strings"
)
var CreateCmd = &cobra.Command{
Use: "create",
Short: "Create a issue",
Run: func(cmd *cobra.Command, args []string) {
isBug, _ := cmd.Flags().GetBool("bug")
isRequirement, _ := cmd.Flags().GetBool("feature")
skipBody, _ := cmd.Flags().GetBool("skip-body")
entPath, _ := cmd.Flags().GetString("ent")
parentKeyWord, _ := cmd.Flags().GetString("parent")
assigneeKeyWord, _ := cmd.Flags().GetString("assignee")
candidateAssignees := make([]member.Member, 0)
candidateTasks := make([]issue.Issue, 0)
assigneeId := 0
parentTaskId := 0
if entPath == "" {
if entPath = config.Conf.DefaultEntPath; entPath == "" {
color.Red("请指定企业 path")
return
}
}
if parentKeyWord != "" {
candidateTasks, _ = issue.Find(enterprise.Id, map[string]string{
"search": parentKeyWord,
})
}
if assigneeKeyWord != "" {
candidateAssignees, _ = member.Find(enterprise.Id, map[string]string{
"search": assigneeKeyWord,
})
}
optionMap := make(map[string]int, 0)
options := make([]string, 0)
var category = issue_type.TASK
var categoryText = "请选择任务类型"
if isBug {
category = issue_type.BUG
categoryText = "请选择缺陷类型"
} else if isRequirement {
category = issue_type.REQUIREMENT
categoryText = "请选择需求类型"
}
issueTypes, err := issue_type.List(category, entPath)
if err != nil {
color.Red("获取任务类型失败!")
return
}
// 填充选项
promote := "请选择要创建的工作项类型"
issueTypeSelector := issue_type_tui.NewIssueTypeSelector(categoryText, issueTypes)
var model tea.Model
if model, err = issueTypeSelector.Run(); err != nil {
color.Red("任务类型选择器加载失败!")
return
}
_issueTypeSelector, _ := model.(tui.Table)
issueTypeId, err := issue_type_tui.SelectedValue(issueTypes, _issueTypeSelector.SelectedKey)
if err != nil {
color.Red(err.Error())
return
}
// 传统选择器
var mapSelector selector_tui.MapSelector
var selector *tea.Program
if len(candidateTasks) != 0 {
options = make([]string, 0)
optionMap = make(map[string]int, 0)
optionMap, options = issue.FillOptions(candidateTasks, optionMap, options)
promote = "请选择要关联的父任务"
selector := selector_tui.NewMapSelector(optionMap, options, promote)
if model, err = selector.Run(); err != nil {
color.Red("父任务选择器加载失败!")
return
}
mapSelector, _ = model.(selector_tui.MapSelector)
parentTaskId, err = mapSelector.SelectedValue()
if err != nil {
color.Red(err.Error())
return
}
}
if len(candidateAssignees) != 0 {
options = make([]string, 0)
optionMap = make(map[string]int, 0)
optionMap, options = member.FillOptions(candidateAssignees, optionMap, options)
promote = "请选择指派的负责人"
selector = selector_tui.NewMapSelector(optionMap, options, promote)
if model, err = selector.Run(); err != nil {
color.Red("负责人选择器加载失败!")
return
}
mapSelector, _ = model.(selector_tui.MapSelector)
assigneeId, err = mapSelector.SelectedValue()
if err != nil {
color.Red(err.Error())
return
}
}
var title string
title = utils.ReadFromInput("填写 Issue 标题", title)
title = strings.TrimSpace(title)
if title == "" {
color.Red("请输入任务标题!")
return
}
var description string
// 获取模版
if template, err := issue_type.FetchTemplate(issueTypeId, enterprise.Id); err == nil {
description = template
}
if !skipBody {
description = utils.ReadFromEditor(utils.InitialEditor("填写 Issue 描述", description), description)
}
payload := map[string]interface{}{
"description_type": "md",
"issue_type_id": issueTypeId,
"title": title,
"description": description,
}
if parentTaskId != 0 {
payload["parent_id"] = parentTaskId
}
if assigneeId != 0 {
payload["assignee_id"] = assigneeId
}
issue, err := issue.Create(enterprise.Id, payload)
if err != nil {
color.Red("创建工作项失败!")
return
}
fmt.Printf("创建工作项 「%s」成功访问地址%s\n", utils.Cyan(issue.Title), utils.Blue(issue.Url))
},
}
func init() {
CreateCmd.Flags().BoolP("task", "", true, "create a task")
CreateCmd.Flags().BoolP("bug", "", false, "create a bug")
CreateCmd.Flags().BoolP("feature", "", false, "create a feature")
CreateCmd.Flags().BoolP("skip-body", "", false, "skip edit issue description")
CreateCmd.Flags().StringP("parent", "p", "", "specify the parent task by search")
CreateCmd.Flags().StringP("assignee", "A", "", "specify the assignee by search")
}

36
cmd/issue/issue.go Normal file
View File

@ -0,0 +1,36 @@
package issue
import (
"gitee_cli/config"
"gitee_cli/internal/api/enterprises"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var enterprise enterprises.Enterprise
var IssueCmd = &cobra.Command{
Use: "issue",
Short: "Manage issues",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
entPath, err := cmd.Flags().GetString("ent")
if entPath == "" {
if entPath = config.Conf.DefaultEntPath; entPath == "" {
color.Red("请指定企业 path")
return
}
}
enterprise, err = enterprises.Find(entPath)
if err != nil {
color.Red("企业未找到!")
return
}
},
}
func init() {
IssueCmd.AddCommand(CreateCmd)
IssueCmd.AddCommand(ListCmd)
IssueCmd.AddCommand(ViewCmd)
IssueCmd.PersistentFlags().StringP("ent", "e", "", "specify the selector_tui path")
}

78
cmd/issue/list.go Normal file
View File

@ -0,0 +1,78 @@
package issue
import (
"fmt"
"gitee_cli/internal/api/issue"
"gitee_cli/internal/api/user"
"gitee_cli/utils/tui"
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/fatih/color"
"github.com/pkg/browser"
"github.com/spf13/cobra"
"os"
"strconv"
)
var ListCmd = &cobra.Command{
Use: "list",
Short: "List issues",
Aliases: []string{"search"},
Run: func(cmd *cobra.Command, args []string) {
username, _ := cmd.Flags().GetString("assignee")
entPath, _ := cmd.Flags().GetString("ent")
limit, _ := cmd.Flags().GetInt("limit")
var search string
payload := make(map[string]string)
if len(args) != 0 {
search = args[0]
}
if username == "" && search == "" {
payload["only_related_me"] = "1"
}
if username != "" {
if assignee, err := user.FindUser(username); err == nil {
payload["assignee_id"] = strconv.Itoa(assignee.Id)
}
}
payload["search"] = search
payload["page"] = "1"
payload["per_page"] = strconv.Itoa(limit)
_issues, _ := issue.Find(enterprise.Id, payload)
columns := []table.Column{
{Title: "Ident", Width: 8},
{Title: "Title", Width: 60},
}
rows := make([]table.Row, 0)
for _, issue := range _issues {
rows = append(rows, table.Row{issue.Ident, issue.Title})
}
issueTable := tui.NewTable(enterprise, tui.Issue, columns, rows)
var model tea.Model
var err error
if model, err = issueTable.Run(); err != nil {
color.Red("任务渲染失败!")
os.Exit(1)
}
_table, _ := model.(tui.Table)
ident := _table.SelectedKey
if ident == "" {
os.Exit(0)
}
url := fmt.Sprintf("https://e.gitee.com/%s/dashboard?issue=%s", entPath, ident)
browser.OpenURL(url)
},
}
func init() {
ListCmd.Flags().StringP("assignee", "A", "", "filter issue assignee")
ListCmd.Flags().IntP("limit", "l", 10, "limit the number of issues returned")
}

24
cmd/issue/view.go Normal file
View File

@ -0,0 +1,24 @@
package issue
import (
"gitee_cli/internal/api/issue"
"gitee_cli/utils/tui"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var ViewCmd = &cobra.Command{
Use: "view",
Short: "Display issue detail",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ident := args[0]
if _issue, err := issue.Detail(enterprise.Id, ident); err == nil {
tui.NewPager(_issue.Title, _issue.Description, tui.Markdown).Run()
} else {
color.Red("获取任务详情失败!")
return
}
},
}

View File

@ -0,0 +1,34 @@
package pull_request
import (
"fmt"
"gitee_cli/internal/api/pull_request"
"github.com/fatih/color"
"github.com/spf13/cobra"
"os"
)
var CommentCmd = &cobra.Command{
Use: "comment",
Short: "Comment pull request",
Args: cobra.ExactArgs(1),
Aliases: []string{"note"},
Run: func(cmd *cobra.Command, args []string) {
comment := args[0]
iid, _ := cmd.Flags().GetInt("iid")
if iid == 0 {
color.Red("请给定有效的 pull request 序号!")
os.Exit(1)
}
if err := pull_request.Note(iid, comment); err != nil {
fmt.Println(err.Error())
return
}
color.Green("评论成功!")
},
}
func init() {
CommentCmd.Flags().IntP("iid", "i", 0, "Pull request number")
}

View File

@ -0,0 +1,98 @@
package pull_request
import (
"fmt"
"gitee_cli/internal/api/pull_request"
"gitee_cli/utils"
"gitee_cli/utils/git_utils"
"github.com/fatih/color"
"github.com/spf13/cobra"
"strings"
)
var CreateCmd = &cobra.Command{
Use: "create",
Short: "Create a pull request",
Long: "Create a pull request",
Run: func(cmd *cobra.Command, args []string) {
title, _ := cmd.Flags().GetString("title")
body, _ := cmd.Flags().GetString("body")
base, _ := cmd.Flags().GetString("base")
head, _ := cmd.Flags().GetString("head")
skipBody, _ := cmd.Flags().GetBool("skip-body")
draft, _ := cmd.Flags().GetBool("draft")
assignee, _ := cmd.Flags().GetString("assignees")
tester, _ := cmd.Flags().GetString("testers")
prune, _ := cmd.Flags().GetBool("prune")
if !git_utils.IsGitDir() {
color.Red("请在仓库目录下执行该命令!")
return
}
baseRepo, err := git_utils.ParseCurrentRepo()
if err != nil {
color.Red("获取当前仓库异常!")
return
}
if title == "" {
title = utils.ReadFromInput("请输入标题", title)
title = strings.TrimSpace(title)
}
if head == "" {
branch, _ := git_utils.GetCurrentBranch()
if branch == "" {
branch = utils.ReadFromInput("请输入起始分支", branch)
head = strings.TrimSpace(branch)
if head == "" {
color.Red("无效的起始分支!")
return
}
}
head = branch
}
if base == "" {
base = utils.ReadFromInput("请输入目标分支", base)
base = strings.TrimSpace(base)
if base == "" {
color.Red("无效的目标分支!")
}
}
if body == "" && !skipBody {
body = utils.ReadFromEditor(utils.InitialEditor("填写 Pull Request 内容", ""), body)
}
pullRequest, err := pull_request.CreatePr(baseRepo, base, head, title, body, assignee, tester, draft, prune)
if err != nil {
color.Red(err.Error())
var input string
input = utils.ReadFromInput(utils.Yellow("是否重试?(y/n/q)"), input)
if input == "y" || input == "yes" {
pullRequest, err = pull_request.CreatePr(baseRepo, base, head, title, body, assignee, tester, draft, prune)
if err != nil {
color.Red(err.Error())
return
}
} else {
return
}
}
fmt.Printf("创建 PR「%s」 成功,访问地址:%s\n", utils.Yellow(pullRequest.Title), utils.Cyan(pullRequest.HtmlUrl))
},
}
func init() {
CreateCmd.Flags().StringP("title", "t", "", "Title of the pull request")
CreateCmd.Flags().StringP("body", "b", "", "Body of the pull request")
CreateCmd.Flags().StringP("base", "B", "", "The branch into which you want your code merged")
CreateCmd.Flags().StringP("head", "H", "", "The branch that contains commits for your pull request (default [current branch])")
CreateCmd.Flags().BoolP("skip-body", "", false, "Skip adding a body to the pull request")
CreateCmd.Flags().BoolP("draft", "", false, "Create a draft pull request")
CreateCmd.Flags().StringP("assignees", "a", "", "Assign the pull request to users to code review, multi user split by , user1,user2")
CreateCmd.Flags().StringP("testers", "", "", "Assign the pull request to users to test, multi user split by , user1,user2")
CreateCmd.Flags().BoolP("prune", "", true, "Prune source branch after pr merged")
}

108
cmd/pull_request/list.go Normal file
View File

@ -0,0 +1,108 @@
package pull_request
import (
"fmt"
"gitee_cli/config"
"gitee_cli/internal/api/enterprises"
"gitee_cli/internal/api/pull_request"
"gitee_cli/utils"
"gitee_cli/utils/git_utils"
"gitee_cli/utils/tui"
"github.com/charmbracelet/bubbles/table"
"github.com/fatih/color"
"github.com/pkg/browser"
"github.com/spf13/cobra"
"os"
"strconv"
)
var ListCmd = &cobra.Command{
Use: "list",
Short: "List pull requests related",
Run: func(cmd *cobra.Command, args []string) {
keyword, _ := cmd.Flags().GetString("keyword")
scope, _ := cmd.Flags().GetString("scope")
commitSha, _ := cmd.Flags().GetString("commit")
openInBrowser, _ := cmd.Flags().GetBool("open")
convertEntUrl, _ := cmd.Flags().GetBool("convert")
if commitSha != "" {
pathWithNamespace, err := git_utils.ParseCurrentRepo()
if err != nil {
color.Red(err.Error())
return
}
pr, err := pull_request.FindPullRequestByIid(commitSha, pathWithNamespace)
if err != nil {
color.Red(err.Error())
return
}
if openInBrowser {
browser.OpenURL(pr.HtmlUrl)
return
}
fmt.Printf("该 commit 由 PR: 「%v」 合入,访问地址: %s\n", utils.Green(pr.Title), utils.Blue(pr.HtmlUrl))
return
}
pullRequests := pull_request.List(scope)
if keyword != "" {
pullRequests = pull_request.FuzzySearch(pullRequests, keyword)
}
if len(pullRequests) == 0 {
color.Cyan("无匹配的 Pull requests.")
return
}
columns := []table.Column{
{Title: "PR 标题", Width: 60},
{Title: "IID", Width: 10},
{Title: "创建人", Width: 12},
{Title: fmt.Sprintf("%s审查状态", config.Conf.UserName), Width: 18},
{Title: "冲突", Width: 10},
{Title: "是否可合入", Width: 10},
}
rows := make([]table.Row, 0)
for _, pr := range pullRequests {
mergeCheckMsg := "No"
if pr.CanMergeCheck {
mergeCheckMsg = utils.Green("Yes")
}
conflictMsg := "No"
if !pr.Mergeable {
conflictMsg = utils.Magenta("Yes")
}
acceptMsg := utils.Magenta("未审查")
if pr.User.Id == 0 {
acceptMsg = "-"
} else if pr.User.Accept {
acceptMsg = utils.Green("通过")
}
rows = append(rows, table.Row{pr.Title, strconv.Itoa(pr.Number), pr.Creator.Name, acceptMsg, conflictMsg, mergeCheckMsg})
}
prTable := tui.NewTable(enterprises.Enterprise{}, tui.PullRequest, columns, rows)
if convertEntUrl {
os.Setenv("CONVERT_ENT_URL", "true")
}
defer func() {
os.Unsetenv("CONVERT_ENT_URL")
}()
//var model tea.Model
var err error
if _, err = prTable.Run(); err != nil {
color.Red("pr渲染失败")
os.Exit(1)
}
//utils.PrRender(pullRequests, convert)
},
}
func init() {
ListCmd.Flags().StringP("keyword", "k", "", "filter pr by keyword")
//ListCmd.Flags().BoolP("reviewed", "r", false, "filter pr by review state")
ListCmd.Flags().StringP("scope", "s", "", "filter pr by scope (owner)")
ListCmd.Flags().StringP("commit", "c", "", "find pr by commit")
ListCmd.Flags().BoolP("open", "o", false, "open in browser, only effective for searching pr via commit sha")
ListCmd.Flags().BoolP("convert", "", false, "transfer url in enterprise")
}

17
cmd/pull_request/pr.go Normal file
View File

@ -0,0 +1,17 @@
package pull_request
import (
"github.com/spf13/cobra"
)
var Pr = &cobra.Command{
Use: "pr",
Aliases: []string{"pull_request"},
Short: "Manage pull requests",
}
func init() {
Pr.AddCommand(ListCmd)
Pr.AddCommand(CreateCmd)
Pr.AddCommand(CommentCmd)
}

39
cmd/root.go Normal file
View File

@ -0,0 +1,39 @@
package cmd
import (
"fmt"
"gitee_cli/cmd/auth"
"gitee_cli/cmd/enterprise"
"gitee_cli/cmd/issue"
"gitee_cli/cmd/pull_request"
sshkey "gitee_cli/cmd/ssh-key"
"gitee_cli/cmd/user"
"gitee_cli/utils"
"github.com/spf13/cobra"
"os"
)
var RootCmd = &cobra.Command{
Use: "gitee",
Short: "Gitee In terminal",
Long: "Gitee CLI is a tool which interact with gitee server seamlessly via terminal",
Version: "0.0.1",
}
func init() {
RootCmd.AddCommand(ConfigCmd)
RootCmd.AddCommand(pull_request.Pr)
RootCmd.AddCommand(issue.IssueCmd)
RootCmd.AddCommand(auth.AuthCmd)
RootCmd.AddCommand(enterprise.EntCmd)
RootCmd.AddCommand(sshkey.SshKeyCommand)
RootCmd.AddCommand(user.UserCmd)
RootCmd.SetVersionTemplate(fmt.Sprintf("Gitee CLI Version %s\n", utils.Cyan(RootCmd.Version)))
}
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

61
cmd/ssh-key/add.go Normal file
View File

@ -0,0 +1,61 @@
package ssh_key
import (
"fmt"
"gitee_cli/internal/api/ssh_key"
"gitee_cli/utils"
tui "gitee_cli/utils/tui/ssh_key"
tea "github.com/charmbracelet/bubbletea"
"github.com/fatih/color"
"github.com/spf13/cobra"
"os"
fp "path/filepath"
)
var AddSshKey = &cobra.Command{
Use: "add",
Short: "Add a ssh pub key for personal",
Run: func(cmd *cobra.Command, args []string) {
filepath, _ := cmd.Flags().GetString("filepath")
title, _ := cmd.Flags().GetString("title")
if title == "" {
color.Red("请指定 ssh key 标题")
return
}
if filepath == "" {
homeDir, _ := os.UserHomeDir()
sshDir := fp.Join(homeDir, ".ssh")
files, _ := fp.Glob(fp.Join(sshDir, "*.pub"))
if len(files) == 0 {
color.Red("请先生成 ssh 密钥对!")
return
}
fileSelector := tui.InitialUploadSSHKeyTui(files)
var data tea.Model
var err error
if data, err = fileSelector.Run(); err != nil {
color.Red("公钥选择器出错,请指定公钥地址以上传!")
return
}
fileSelectRes, _ := data.(tui.UploadSSHKeyTui)
if fileSelectRes.Cursor == -1 {
return
}
filepath = fileSelectRes.FileList[fileSelectRes.Cursor]
}
sshKey, err := ssh_key.AddKey(filepath, title)
if err != nil {
color.Red(err.Error())
return
}
fmt.Printf("添加 ssh key 「%s」 成功,访问地址:%s\n", utils.Yellow(sshKey.Title), utils.Cyan(fmt.Sprintf("https://gitee.com/keys/%d", sshKey.Id)))
},
}
func init() {
SshKeyCommand.AddCommand(AddSshKey)
AddSshKey.Flags().StringP("filepath", "f", "", "ssh pub key filepath")
AddSshKey.Flags().StringP("title", "t", "", "title for ssh key")
}

30
cmd/ssh-key/delete.go Normal file
View File

@ -0,0 +1,30 @@
package ssh_key
import (
"gitee_cli/internal/api/ssh_key"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var DeleteSshKey = &cobra.Command{
Use: "delete",
Short: "delete a specified ssh key",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
sshKeyId := args[0]
if sshKeyId == "" {
color.Red("请提供正确的 SSH Key ID")
return
}
err := ssh_key.DeleteKey(sshKeyId)
if err != nil {
color.Red(err.Error())
return
}
color.Green("删除公钥成功")
},
}
func init() {
SshKeyCommand.AddCommand(DeleteSshKey)
}

50
cmd/ssh-key/list.go Normal file
View File

@ -0,0 +1,50 @@
package ssh_key
import (
"fmt"
"gitee_cli/internal/api/enterprises"
"gitee_cli/internal/api/ssh_key"
"gitee_cli/utils/tui"
"github.com/charmbracelet/bubbles/table"
"github.com/fatih/color"
"github.com/spf13/cobra"
"os"
"strconv"
)
var ListSshKey = &cobra.Command{
Use: "list",
Short: "List personal ssh pub keys",
Long: "List personal ssh pub keys",
Run: func(cmd *cobra.Command, args []string) {
sshKeys, err := ssh_key.ListKeys()
if err != nil {
color.Red("获取ssh公钥列表失败")
return
}
if len(sshKeys) == 0 {
color.Green("暂未添加 SSH 公钥")
os.Exit(0)
}
columns := []table.Column{
{Title: "ID", Width: 8},
{Title: "Key Sha", Width: 38},
{Title: "Preview URL", Width: 32},
}
rows := make([]table.Row, 0)
for _, key := range sshKeys {
rows = append(rows, table.Row{strconv.Itoa(key.Id), key.Key[:50], fmt.Sprintf("https://gitee.com/keys/%d", key.Id)})
}
if _, err := tui.NewTable(enterprises.Enterprise{}, tui.SSHKey, columns, rows).Run(); err != nil {
color.Red("获取 SSH 公钥失败!")
os.Exit(1)
}
},
}
func init() {
SshKeyCommand.AddCommand(ListSshKey)
}

10
cmd/ssh-key/ssh_key.go Normal file
View File

@ -0,0 +1,10 @@
package ssh_key
import (
"github.com/spf13/cobra"
)
var SshKeyCommand = &cobra.Command{
Use: "ssh-key",
Short: "Manage ssh-keys",
}

67
cmd/user/search.go Normal file
View File

@ -0,0 +1,67 @@
package user
import (
"fmt"
"gitee_cli/config"
"gitee_cli/internal/api/enterprises"
"gitee_cli/internal/api/user"
"gitee_cli/utils"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var SearchCmd = &cobra.Command{
Use: "search",
Short: "Search for a user info, Usage: gitee user search {username}",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
color.Red("请给定用户名!")
return
}
username := args[0]
isEntPath, _ := cmd.Flags().GetBool("ent")
if isEntPath {
entPath := config.Conf.DefaultEntPath
if entPath == "" {
color.Red("请使用 gitee config default_ent_path xxx 指定默认 path!")
return
}
enterprise, err := enterprises.Find(entPath)
if err != nil {
color.Red("企业未找到!")
return
}
var member user.Member
if member, err = user.FindMember(username, enterprise.Id); err == nil {
fmt.Printf("成员 ID%s\n成员名称%s\n用户名%s\n", utils.Cyan(member.Id), utils.Cyan(member.Remark), utils.Blue(member.UserName))
} else {
color.Red("未查找到对应用户!")
return
}
} else {
user, err := user.FindUser(username)
if err != nil {
color.Red("查询用户失败!%v", err)
return
}
if user.Id == 0 {
color.Red("未查找到对应用户!")
return
}
fmt.Printf("用户 ID%s\n用户名称%s\n用户主页%s\n", utils.Cyan(user.Id), utils.Cyan(user.Name), utils.Blue(user.HtmlUrl))
}
},
}
func init() {
SearchCmd.Flags().BoolP("ent", "e", false, "search member from current enterprise")
}

12
cmd/user/user.go Normal file
View File

@ -0,0 +1,12 @@
package user
import "github.com/spf13/cobra"
var UserCmd = &cobra.Command{
Use: "user",
Short: "User related command",
}
func init() {
UserCmd.AddCommand(SearchCmd)
}

134
config/config.go Normal file
View File

@ -0,0 +1,134 @@
package config
import (
"fmt"
"os"
"path"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
var Conf Config
type Config struct {
AccessToken string `yaml:"access_token"`
ApiPrefix string `yaml:"api_prefix"`
UserId int `yaml:"user_id"`
UserName string `yaml:"user_name"`
DefaultEntPath string `yaml:"default_ent_path"`
DefaultPathWithNamespace string `yaml:"default_path_with_namespace"`
PremiumBuildPrefix string `yaml:"premium_build_prefix"`
SaasBuildPrefix string `yaml:"saas_build_prefix"`
CookiesJar string `yaml:"cookies_jar"`
DefaultEditor string `yaml:"default_editor"`
}
func Read(key string) (string, error) {
switch key {
case "access_token":
return Conf.AccessToken, nil
case "api_prefix":
return Conf.ApiPrefix, nil
case "user_id":
return fmt.Sprintf("%v", Conf.UserId), nil
case "user_name":
return Conf.UserName, nil
case "default_ent_path":
return Conf.DefaultEntPath, nil
case "default_path_with_namespace":
return Conf.DefaultPathWithNamespace, nil
case "premium_build_prefix":
return Conf.PremiumBuildPrefix, nil
case "saas_build_prefix":
return Conf.SaasBuildPrefix, nil
case "cookies_jar":
return Conf.CookiesJar, nil
case "default_editor":
return Conf.DefaultEditor, nil
default:
return "", fmt.Errorf("Unknown config key: %s", key)
}
}
// Update updates the configuration values from a provided map.
func Update(values map[string]interface{}) error {
for key, value := range values {
switch key {
case "access_token":
Conf.AccessToken = value.(string)
case "api_prefix":
Conf.ApiPrefix = value.(string)
case "user_id":
Conf.UserId, _ = strconv.Atoi(parseInput(value))
case "user_name":
Conf.UserName = value.(string)
case "default_ent_path":
Conf.DefaultEntPath = value.(string)
case "default_path_with_namespace":
Conf.DefaultPathWithNamespace = value.(string)
case "premium_build_prefix":
Conf.PremiumBuildPrefix = value.(string)
case "saas_build_prefix":
Conf.SaasBuildPrefix = value.(string)
case "cookies_jar":
Conf.CookiesJar = strings.TrimSpace(value.(string))
case "default_editor":
Conf.DefaultEditor = value.(string)
default:
return fmt.Errorf("Unknown configuration key: %s", key)
}
}
// Save the updated configuration to the file
config, err := yaml.Marshal(&Conf)
if err != nil {
return fmt.Errorf("Error marshalling configuration: %w", err)
}
homeDir, _ := os.UserHomeDir()
configPath := path.Join(homeDir, ".gitee", "config.yml")
err = os.WriteFile(configPath, config, 0644)
if err != nil {
return fmt.Errorf("Error overwriting configuration file: %w", err)
}
return nil
}
func parseInput(input interface{}) string {
switch input.(type) {
case string:
return input.(string)
case int:
return fmt.Sprintf("%v", input.(int))
default:
return ""
}
}
func init() {
homeDir, _ := os.UserHomeDir()
configPath := path.Join(homeDir, ".gitee", "config.yml")
config, err := os.ReadFile(configPath)
if err != nil {
fmt.Printf("读取配置文件失败!请检查 %s 配置内容!\n", configPath)
os.Exit(1)
}
err = yaml.Unmarshal(config, &Conf)
if err != nil {
fmt.Printf("初始化配置文件失败,请检查 %s 配置内容!\n", configPath)
os.Exit(1)
}
// 兼容 bubbletea border 渲染问题
// https://github.com/charmbracelet/lipgloss/issues/40
os.Setenv("RUNEWIDTH_EASTASIAN", "0")
}

17
config/config.yml.example Normal file
View File

@ -0,0 +1,17 @@
# 个人私人令牌,用于 V5 鉴权
access_token: xxxxxxxx
api_prefix: https://gitee.com/api/v5
# 用户在 Gitee 上的 ID
user_id: 123456
# 用户名
user_name: xxx
# 默认企业 path
default_ent_path: oschina
# 非仓库目录下执行 gitee cli 命令默认仓库全路径(配置你比较常用的仓库)
default_path_with_namespace: oschina/gitee
# 专业版构建命令前缀
premium_build_prefix: premium_ci_build
# 企业版构建命令前缀
saas_build_prefix: ci_deploy
# cookie 用于企业版 API 鉴权
cookies_jar: xxxxxxx

BIN
doc/images/issue_list.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
doc/images/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

51
go.mod Normal file
View File

@ -0,0 +1,51 @@
module gitee_cli
go 1.19
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/glamour v0.7.0
github.com/charmbracelet/lipgloss v0.10.0
github.com/fatih/color v1.16.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/spf13/cobra v1.8.0
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/alecthomas/chroma/v2 v2.13.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/yuin/goldmark v1.7.0 // indirect
github.com/yuin/goldmark-emoji v1.0.2 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)

145
go.sum Normal file
View File

@ -0,0 +1,145 @@
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
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/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1 h1:+dBg5k7nuTE38VVdoroRsT0Z88fmvdYrI2EjzJst35I=
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1/go.mod h1:nmuySobZb4kFgFy6BptpXp/BBw+xFSyvVPP6auoJB4k=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,50 @@
package enterprises
import (
"encoding/json"
"errors"
"fmt"
"gitee_cli/utils/http_utils"
)
type Enterprise struct {
Id int `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
}
func List() ([]Enterprise, error) {
url := "https://api.gitee.com/enterprises/list"
giteeClient := http_utils.NewGiteeClient("GET", url, nil, nil)
giteeClient.SetCookieAuth()
_, err := giteeClient.Do()
if err != nil || giteeClient.IsFail() {
return nil, err
}
data, _ := giteeClient.GetRespBody()
type res struct {
Data []Enterprise `json:"data"`
TotalCount int `json:"total_count"`
}
var _data res
json.Unmarshal(data, &_data)
return _data.Data, nil
}
func Find(path string) (Enterprise, error) {
url := fmt.Sprintf("https://gitee.com/api/v5/enterprises/%s", path)
giteeClient := http_utils.NewGiteeClient("GET", url, nil, nil)
if _, err := giteeClient.Do(); err != nil {
return Enterprise{}, errors.New("查询企业失败!")
}
data, _ := giteeClient.GetRespBody()
ent := Enterprise{}
json.Unmarshal(data, &ent)
return ent, nil
}

View File

@ -0,0 +1,88 @@
package issue
import (
"encoding/json"
"errors"
"fmt"
"gitee_cli/utils/http_utils"
)
const Endpoint = "https://api.gitee.com/enterprises/%d/issues"
type Issue struct {
Id int `json:"id"`
Ident string `json:"ident"`
Title string `json:"title"`
Url string `json:"issue_url"`
Description string `json:"description"`
}
func Find(enterpriseId int, params map[string]string) ([]Issue, error) {
url := fmt.Sprintf(Endpoint, enterpriseId)
giteeClient := http_utils.NewGiteeClient("GET", url, params, nil)
giteeClient.SetCookieAuth()
_, err := giteeClient.Do()
if err != nil || giteeClient.IsFail() {
return []Issue{}, err
}
data, _ := giteeClient.GetRespBody()
type res struct {
Data []Issue `json:"data"`
TotalCount int `json:"total_count"`
}
var _data res
json.Unmarshal(data, &_data)
return _data.Data, nil
}
func Create(enterpriseId int, payload map[string]interface{}) (Issue, error) {
url := fmt.Sprintf(Endpoint, enterpriseId)
giteeClient := http_utils.NewGiteeClient("POST", url, nil, payload)
giteeClient.SetCookieAuth()
giteeClient.Do()
if giteeClient.IsFail() {
return Issue{}, errors.New("创建工作项失败!")
}
issue := Issue{}
data, _ := giteeClient.GetRespBody()
json.Unmarshal(data, &issue)
return issue, nil
}
func FillOptions(issues []Issue, optionMap map[string]int, options []string) (map[string]int, []string) {
if len(issues) == 0 {
return optionMap, options
}
for _, issue := range issues {
key := fmt.Sprintf("[%s] %s", issue.Ident, issue.Title)
optionMap[key] = issue.Id
options = append(options, key)
}
return optionMap, options
}
func Detail(enterpriseId int, ident string) (Issue, error) {
url := fmt.Sprintf("https://api.gitee.com/enterprises/%d/issues/%s?qt=ident", enterpriseId, ident)
giteeClient := http_utils.NewGiteeClient("GET", url, nil, nil)
giteeClient.SetCookieAuth()
giteeClient.Do()
if giteeClient.IsFail() {
return Issue{}, errors.New("获取工作想失败!")
}
data, _ := giteeClient.GetRespBody()
issue := Issue{}
json.Unmarshal(data, &issue)
return issue, nil
}

View File

@ -0,0 +1,82 @@
package issue_type
import (
"encoding/json"
"errors"
"fmt"
"gitee_cli/internal/api/enterprises"
"gitee_cli/utils/http_utils"
)
const (
TASK = iota
BUG
REQUIREMENT
)
func typeCategory(t int) string {
return map[int]string{
TASK: "task",
BUG: "bug",
REQUIREMENT: "requirement",
}[t]
}
type IssueType struct {
Id int `json:"id"`
Title string `json:"title"`
Template string `json:"template"`
}
func List(issueType int, entPath string) ([]IssueType, error) {
ent, err := enterprises.Find(entPath)
if err != nil {
return nil, err
}
category := typeCategory(issueType)
if category == "" {
return nil, errors.New("无效的任务类型!")
}
url := fmt.Sprintf("https://api.gitee.com/enterprises/%d/issue_types/enterprise_issue_types?category=%s&page=1&per_page=1000&state=1", ent.Id, category)
giteeClient := http_utils.NewGiteeClient("GET", url, nil, nil)
giteeClient.SetCookieAuth()
if _, err := giteeClient.Do(); err != nil {
return nil, err
}
var res = struct {
Data []IssueType `json:"data"`
TotalCount int `json:"total_count"`
}{}
data, _ := giteeClient.GetRespBody()
json.Unmarshal(data, &res)
return res.Data, nil
}
func FillOptions(issueTypes []IssueType, optionMap map[string]int, options []string) (map[string]int, []string) {
if len(issueTypes) == 0 {
return optionMap, options
}
for _, issueType := range issueTypes {
optionMap[issueType.Title] = issueType.Id
options = append(options, issueType.Title)
}
return optionMap, options
}
func FetchTemplate(issueTypeId, entId int) (string, error) {
url := fmt.Sprintf("https://api.gitee.com/enterprises/%d/issue_types/%d", entId, issueTypeId)
giteeClient := http_utils.NewGiteeClient("GET", url, nil, nil)
giteeClient.SetCookieAuth()
if _, err := giteeClient.Do(); err != nil {
return "", errors.New("获取模板失败!")
}
issueType := IssueType{}
data, _ := giteeClient.GetRespBody()
json.Unmarshal(data, &issueType)
return issueType.Template, nil
}

View File

@ -0,0 +1,50 @@
package member
import (
"encoding/json"
"fmt"
"gitee_cli/utils/http_utils"
)
type Member struct {
Id int `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Remark string `json:"remark"`
}
func Find(enterpriseId int, params map[string]string) ([]Member, error) {
url := fmt.Sprintf("https://api.gitee.com/enterprises/%d/members", enterpriseId)
giteeClient := http_utils.NewGiteeClient("GET", url, params, nil)
giteeClient.SetCookieAuth()
_, err := giteeClient.Do()
if err != nil || giteeClient.IsFail() {
return []Member{}, err
}
data, _ := giteeClient.GetRespBody()
type res struct {
Data []Member `json:"data"`
TotalCount int `json:"total_count"`
}
var _data res
json.Unmarshal(data, &_data)
return _data.Data, nil
}
func FillOptions(members []Member, optionMap map[string]int, options []string) (map[string]int, []string) {
if len(members) == 0 {
return optionMap, options
}
for _, member := range members {
key := fmt.Sprintf("%s(%s)", member.Name, member.Remark)
optionMap[key] = member.Id
options = append(options, key)
}
return optionMap, options
}

View File

@ -0,0 +1,298 @@
package pull_request
import (
"encoding/json"
"errors"
"fmt"
"gitee_cli/config"
"gitee_cli/utils/git_utils"
"gitee_cli/utils/http_utils"
"os/exec"
"strconv"
"strings"
"time"
)
type PullRequest struct {
Id int `json:"id"`
Title string `json:"title"`
HtmlUrl string `json:"html_url"`
Mergeable bool `json:"mergeable"`
CanMergeCheck bool `json:"can_merge_check"`
PatchUrl string `json:"patch_url"`
Draft bool `json:"draft"`
Creator creator `json:"user"`
Assignees []assignee `json:"assignees"`
User assignee `json:"-"`
Number int `json:"number"`
Body string `json:"body"`
}
type assignee struct {
Id int `json:"id"`
Name string `json:"name"`
Accept bool `json:"accept"`
}
type creator struct {
Id int `json:"id"`
Name string `json:"name"`
}
func Note(iid int, note string) error {
// https://gitee.com/api/v5/repos/{owner}/{repo}/pulls/{number}/comments
pathWithNamespace, err := git_utils.ParseCurrentRepo()
if err != nil {
pathWithNamespace = config.Conf.DefaultPathWithNamespace
}
url := fmt.Sprintf(config.Conf.ApiPrefix+"/repos/%s/pulls/%d/comments", pathWithNamespace, iid)
payload := map[string]string{"body": note}
giteeClient := http_utils.NewGiteeClient("POST", url, nil, payload)
giteeClient, _ = giteeClient.Do()
if giteeClient.IsFail() {
return errors.New(fmt.Sprintf("评论 pr %d 失败!", iid))
}
return nil
}
func List(scope string) []PullRequest {
pathWithNamespace, err := git_utils.ParseCurrentRepo()
if err != nil {
pathWithNamespace = config.Conf.DefaultPathWithNamespace
}
url := fmt.Sprintf(config.Conf.ApiPrefix+"/repos/%s/pulls?state=open&sort=created&direction=desc&page=1&per_page=100", pathWithNamespace)
giteeClient := http_utils.NewGiteeClient("GET", url, nil, nil)
pullRequests := make([]PullRequest, 50)
giteeClient.Do()
res, _ := giteeClient.GetRespBody()
err = json.Unmarshal(res, &pullRequests)
if err != nil {
fmt.Println("解析 Pull Request 异常!")
return nil
}
pullRequests = filterPullRequest(pullRequests, scope)
return pullRequests
}
func filterPullRequest(pullRequests []PullRequest, scope string) []PullRequest {
if len(pullRequests) == 0 {
return pullRequests
}
// 过滤指定 User 审查的 PR
filteredPullRequests := make([]PullRequest, 0)
userId := config.Conf.UserId
if scope == "owner" {
for _, pr := range pullRequests {
if pr.Creator.Id == userId {
filteredPullRequests = append(filteredPullRequests, pr)
}
}
} else {
for _, pr := range pullRequests {
assignees := pr.Assignees
for _, assignee := range assignees {
if assignee.Id == userId {
pr.User = assignee
filteredPullRequests = append(filteredPullRequests, pr)
break
}
}
}
}
return filteredPullRequests
}
func (pr PullRequest) TransferUrlToEnt() string {
url := pr.HtmlUrl
data := strings.Split(url, "/")
iid := data[len(data)-1]
pathWithName, err := git_utils.ParseCurrentRepo()
if err != nil {
pathWithName = config.Conf.DefaultPathWithNamespace
}
return fmt.Sprintf("https://e.gitee.com/oschina/repos/%s/pulls/%s", pathWithName, iid)
}
func FuzzySearch(pullRequests []PullRequest, keyword string) []PullRequest {
if len(pullRequests) == 0 {
return pullRequests
}
// 模糊搜索
fuzzyPullRequests := make([]PullRequest, 0)
for _, pr := range pullRequests {
if strings.Contains(pr.Title, keyword) {
fuzzyPullRequests = append(fuzzyPullRequests, pr)
}
}
return fuzzyPullRequests
}
func FindPullRequestByIid(commitSha, pathWithNamespace string) (PullRequest, error) {
currentBranch, err := git_utils.GetCurrentBranch()
if err != nil {
return PullRequest{}, err
}
iid, err := findPrIidBySha(commitSha, git_utils.CurrentDir(), currentBranch)
if err != nil {
return PullRequest{}, err
}
url := fmt.Sprintf("https://gitee.com/api/v5/repos/%s/pulls/%d", pathWithNamespace, iid)
giteeClient := http_utils.NewGiteeClient("GET", url, nil, nil)
giteeClient.Do()
if giteeClient.IsFail() {
return PullRequest{}, errors.New("查找 pr 异常!")
}
res, _ := giteeClient.GetRespBody()
pullRequest := PullRequest{}
err = json.Unmarshal(res, &pullRequest)
if err != nil {
fmt.Println("解析 Pull Request 异常!")
return PullRequest{}, errors.New("解析 Pull Request 异常!")
}
return pullRequest, nil
}
func findPrIidBySha(commitSha, dir, currentBranch string) (int, error) {
command := fmt.Sprintf("git log --merges --ancestry-path --oneline %s..%s | grep 'pull request' | tail -n1 | awk '{print $2\";\"$3}'", commitSha, currentBranch)
cmd := exec.Command("/bin/sh", "-c", command)
cmd.Dir = dir
res, err := cmd.CombinedOutput()
if err != nil {
fmt.Println(err.Error())
return 0, errors.New("获取 Pull Request ID 失败!")
}
result := strings.Split(string(res), ";")
if len(result) != 2 {
return 0, errors.New("未找到匹配的 Pull Request")
}
iid, _ := strconv.Atoi(strings.TrimPrefix(result[0], "!"))
title := result[1]
if title == "" {
return 0, errors.New("获取 Pull Request ID 失败!")
}
return iid, nil
}
func CreatePr(baseRepo, baseRef, headRef, title, body, assignees, testers string, draft bool, prune bool) (PullRequest, error) {
url := fmt.Sprintf("https://gitee.com/api/v5/repos/%s/pulls", baseRepo)
payload := map[string]interface{}{
"base": baseRef,
"head": headRef,
"title": title,
"body": body,
"assignees": assignees,
"testers": testers,
"draft": draft,
"prune_source_branch": prune,
}
giteeClient := http_utils.NewGiteeClient("POST", url, nil, payload)
giteeClient, err := giteeClient.Do()
if err != nil {
return PullRequest{}, errors.New("GiteeCilent 异常!")
}
res, _ := giteeClient.GetRespBody()
if giteeClient.IsFail() {
errResponse := http_utils.ErrMsgV5{}
err := json.Unmarshal(res, &errResponse)
if err != nil {
return PullRequest{}, errors.New("创建 pull request 失败!")
}
return PullRequest{}, errors.New(errResponse.Message)
}
pullRequest := PullRequest{}
if err := json.Unmarshal(res, &pullRequest); err != nil {
return pullRequest, errors.New("解析响应失败")
}
return pullRequest, nil
}
func CreateLightPr(baseRepo, baseRef, prTitle string) (PullRequest, error) {
content := "test"
message := "test"
unixTime := time.Now().Format("20060102150405")
path := "test_" + unixTime + ".txt"
branch := "test_" + unixTime
// 新建分支
url := fmt.Sprintf("https://gitee.com/api/v5/repos/%s/branches", baseRepo)
payload := map[string]string{"refs": baseRef, "branch_name": branch}
giteeClient := http_utils.NewGiteeClient("POST", url, nil, payload)
giteeClient.Do()
if giteeClient.IsFail() {
return PullRequest{}, errors.New("创建 PR 失败")
}
giteeClient.Payload = map[string]string{"message": message, "content": content, "branch": branch}
giteeClient.Url = fmt.Sprintf("https://gitee.com/api/v5/repos/%s/contents/%s", baseRepo, path)
giteeClient.Do()
if giteeClient.IsFail() {
return PullRequest{}, errors.New("创建 PR 失败")
}
// 创建 pr
giteeClient.Url = fmt.Sprintf("https://gitee.com/api/v5/repos/%s/pulls", baseRepo)
giteeClient.Payload = map[string]string{
"title": prTitle,
"head": branch,
"base": baseRef,
}
giteeClient.Do()
if giteeClient.IsFail() {
return PullRequest{}, errors.New("创建 PR 失败")
}
res, _ := giteeClient.GetRespBody()
pullRequest := PullRequest{}
err := json.Unmarshal(res, &pullRequest)
if err != nil {
fmt.Println("解析 Pull Request 异常!")
return PullRequest{}, errors.New("解析 Pull Request 异常!")
}
return pullRequest, nil
}
func Detail(iid, repoPath string) (PullRequest, error) {
url := fmt.Sprintf("https://gitee.com/api/v5/repos/%s/pulls/%s", repoPath, iid)
giteeClient := http_utils.NewGiteeClient("GET", url, nil, nil)
if _, err := giteeClient.Do(); err != nil || giteeClient.IsFail() {
return PullRequest{}, errors.New("获取 Pull Request 详情失败")
}
_pullRequest := PullRequest{}
data, _ := giteeClient.GetRespBody()
json.Unmarshal(data, &_pullRequest)
return _pullRequest, nil
}
func FetchPatchContent(iid, repoPath string) (string, error) {
url := fmt.Sprintf("https://gitee.com/%s/pulls/%s.diff", repoPath, iid)
giteeClient := http_utils.NewGiteeClient("GET", url, nil, nil)
giteeClient.SetCookieAuth()
if _, err := giteeClient.Do(); err != nil || giteeClient.IsFail() {
return "", errors.New("获取 Pull Request 补丁内容失败")
}
data, _ := giteeClient.GetRespBody()
return string(data), nil
}

View File

@ -0,0 +1,99 @@
package ssh_key
import (
"encoding/json"
"errors"
"fmt"
"gitee_cli/config"
"gitee_cli/utils/http_utils"
"io/ioutil"
"net/http"
"os"
)
type SSHKey struct {
Id int `json:"id"`
Title string `json:"title"`
Url string `json:"url"`
Key string `json:"key"`
}
func AddKey(filepath, title string) (SSHKey, error) {
file, err := os.Open(filepath)
if err != nil {
return SSHKey{}, err
}
data, err := ioutil.ReadAll(file)
if err != nil {
return SSHKey{}, errors.New("读取公钥失败")
}
url := "https://gitee.com/api/v5/user/keys"
payload := map[string]string{
"key": string(data),
"title": title,
}
giteeClient := http_utils.NewGiteeClient("POST", url, nil, payload)
giteeClient.Do()
res, _ := giteeClient.GetRespBody()
if giteeClient.IsFail() {
errResponse := http_utils.ErrMsgV5{}
err := json.Unmarshal(res, &errResponse)
if err != nil {
return SSHKey{}, errors.New("添加公钥失败")
}
return SSHKey{}, errors.New(errResponse.Message)
}
sshKey := SSHKey{}
err = json.Unmarshal(res, &sshKey)
if err != nil {
return sshKey, errors.New("解析响应失败")
}
return sshKey, nil
}
func ListKeys() ([]SSHKey, error) {
url := fmt.Sprintf("https://gitee.com/api/v5/users/%s/keys?access_token=%s", config.Conf.UserName, config.Conf.AccessToken)
req, err := http.Get(url)
if err != nil {
return nil, err
}
sshKeys := make([]SSHKey, 0)
res, err := ioutil.ReadAll(req.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(res, &sshKeys)
if err != nil {
return nil, err
}
return sshKeys, nil
}
func DeleteKey(sshKeyId string) error {
url := fmt.Sprintf("https://gitee.com/api/v5/user/keys/%s", sshKeyId)
giteeClient := http_utils.NewGiteeClient("DELETE", url, nil, nil)
giteeClient.Do()
data, _ := giteeClient.GetRespBody()
if giteeClient.IsFail() {
if giteeClient.Response.StatusCode == http.StatusNotFound {
return errors.New("公钥不存在")
}
errMsg := http_utils.ErrMsgV5{}
json.Unmarshal(data, &errMsg)
return errors.New(errMsg.Message)
}
return nil
}

90
internal/api/user/user.go Normal file
View File

@ -0,0 +1,90 @@
package user
import (
"encoding/json"
"errors"
"fmt"
"gitee_cli/utils/http_utils"
)
type User struct {
Id int `json:"id"`
Name string `json:"name"`
HtmlUrl string `json:"html_url"`
}
type Member struct {
Id int `json:"id"`
Remark string `json:"remark"`
UserName string `json:"username"`
}
func FindUser(username string) (User, error) {
url := fmt.Sprintf("https://gitee.com/api/v5/search/users?q=%s", username)
giteeClient := http_utils.NewGiteeClient("GET", url, nil, nil)
giteeClient.Do()
if giteeClient.IsFail() {
return User{}, errors.New("查询用户失败!")
}
users := make([]User, 0)
data, _ := giteeClient.GetRespBody()
if err := json.Unmarshal(data, &users); err != nil {
return User{}, errors.New("查询用户失败,解析响应失败!")
}
if len(users) == 0 {
return User{}, nil
}
return users[0], nil
}
func FindMember(keyword string, enterpriseId int) (Member, error) {
url := fmt.Sprintf("https://api.gitee.com/enterprises/%d/members?search=%s", enterpriseId, keyword)
giteeClient := http_utils.NewGiteeClient("GET", url, nil, nil)
giteeClient.SetCookieAuth()
giteeClient.Do()
data, _ := giteeClient.GetRespBody()
if giteeClient.IsFail() {
return Member{}, errors.New("查询企业成员失败!")
}
members := struct {
Data []Member `json:"data"`
TotalCount int `json:"total_count"`
}{}
if err := json.Unmarshal(data, &members); err != nil {
return Member{}, errors.New("查询用户失败,解析响应失败!")
}
if len(members.Data) == 0 {
return Member{}, nil
}
return members.Data[0], nil
}
func BasicUser() (User, error) {
url := "https://api.gitee.com/enterprises/users"
giteeClient := http_utils.NewGiteeClient("GET", url, nil, nil)
giteeClient.SetCookieAuth()
_, err := giteeClient.Do()
if err != nil || giteeClient.IsFail() {
return User{}, errors.New("查询用户失败!")
}
user := User{}
data, _ := giteeClient.GetRespBody()
if err := json.Unmarshal(data, &user); err != nil {
return User{}, errors.New("查询用户失败,解析响应失败!")
}
return user, nil
}

10
main.go Normal file
View File

@ -0,0 +1,10 @@
package main
import (
"gitee_cli/cmd"
_ "gitee_cli/config"
)
func main() {
cmd.Execute()
}

36
utils/color.go Normal file
View File

@ -0,0 +1,36 @@
package utils
import "github.com/fatih/color"
var (
green func(a ...interface{}) string = color.New(color.FgGreen).SprintFunc()
blue func(a ...interface{}) string = color.New(color.FgBlue).SprintFunc()
yellow func(a ...interface{}) string = color.New(color.FgYellow).SprintFunc()
cyan func(a ...interface{}) string = color.New(color.FgCyan).SprintFunc()
red func(a ...interface{}) string = color.New(color.FgRed).SprintFunc()
magenta func(a ...interface{}) string = color.New(color.FgMagenta).SprintFunc()
)
func Green(a ...interface{}) string {
return green(a...)
}
func Blue(a ...interface{}) string {
return blue(a...)
}
func Yellow(a ...interface{}) string {
return yellow(a...)
}
func Cyan(a ...interface{}) string {
return cyan(a...)
}
func Red(a ...interface{}) string {
return red(a...)
}
func Magenta(a ...interface{}) string {
return magenta(a...)
}

45
utils/editor.go Normal file
View File

@ -0,0 +1,45 @@
package utils
import (
"fmt"
"gitee_cli/config"
"github.com/AlecAivazis/survey/v2"
"os"
)
func InitialEditor(message string, defaultContent string) *survey.Editor {
return &survey.Editor{
Editor: config.Conf.DefaultEditor,
Default: fmt.Sprint(defaultContent),
AppendDefault: true,
Message: message,
FileName: "*.md",
}
}
func ReadFromEditor(editor *survey.Editor, content string) string {
survey.EditorQuestionTemplate = `
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
{{- if .ShowAnswer}}
{{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}}
{{- else }}
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}
{{- color "cyan"}}[Enter to launch editor] {{color "reset"}}
{{- end}}`
if err := survey.AskOne(editor, &content); err != nil {
os.Exit(0)
}
return content
}
func ReadFromInput(message, content string) string {
prompt := &survey.Input{
Message: message,
}
if err := survey.AskOne(prompt, &content); err != nil {
os.Exit(0)
}
return content
}

65
utils/git_utils/git.go Normal file
View File

@ -0,0 +1,65 @@
package git_utils
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"strings"
)
const (
BRANCH_PREFIX = "refs/heads/"
HTTP_PREFIX = "https://gitee.com/"
SSH_PREFIX = "git@gitee.com:"
GIT_SUFFIX = ".git"
)
func CurrentDir() string {
wd, _ := os.Getwd()
return wd
}
func IsGitDir() bool {
wd := CurrentDir()
if _, err := os.Stat(fmt.Sprintf("%s/.git", wd)); err != nil {
return false
}
return true
}
func GetCurrentBranch() (string, error) {
catFile := exec.Command("cat", ".git/HEAD")
extractBranch := exec.Command("awk", "{print $2}")
var output bytes.Buffer
catFile.Stdout = &output
extractBranch.Stdin = &output
err := catFile.Run()
res, err := extractBranch.CombinedOutput()
if err != nil {
fmt.Println(err)
return "", errors.New("获取当前分支异常")
}
return strings.TrimSpace(strings.TrimPrefix(string(res), BRANCH_PREFIX)), nil
}
func ParseCurrentRepo() (string, error) {
var err error
var pathWithNamespace string
if !IsGitDir() {
return "", errors.New("请在仓库目录下执行该命令!")
}
gitRemote := exec.Command("git", "remote")
gitRemote.Dir = CurrentDir()
output, err := gitRemote.CombinedOutput()
getUrl := exec.Command("git", "remote", "get-url", strings.Split(string(output), "\n")[0])
getUrl.Dir = CurrentDir()
output, err = getUrl.CombinedOutput()
gitUrl := strings.Trim(string(output), "\n")
gitUrl = strings.TrimPrefix(gitUrl, HTTP_PREFIX)
gitUrl = strings.TrimPrefix(gitUrl, SSH_PREFIX)
pathWithNamespace = strings.TrimSuffix(gitUrl, GIT_SUFFIX)
return pathWithNamespace, err
}

View File

@ -0,0 +1,111 @@
package http_utils
import (
"bytes"
"encoding/json"
"gitee_cli/config"
"github.com/fatih/color"
"io/ioutil"
"net/http"
"net/url"
"os"
)
type GiteeClient struct {
Url string
Method string
Payload interface{}
Headers map[string]string
Response *http.Response
CookieAuth bool
Query map[string]string
}
type ErrMsgV5 struct {
Message string `json:"message"`
}
func NewGiteeClient(method, urlString string, query map[string]string, payload interface{}) *GiteeClient {
parsedUrl, err := url.Parse(urlString)
if err != nil {
panic(err)
}
if query != nil {
queryParams := parsedUrl.Query()
for k, v := range query {
queryParams.Set(k, v)
}
parsedUrl.RawQuery = queryParams.Encode()
}
return &GiteeClient{
Method: method,
Url: parsedUrl.String(),
Payload: payload,
Query: query,
}
}
func (g *GiteeClient) SetHeaders(headers map[string]string) *GiteeClient {
g.Headers = headers
return g
}
func (g *GiteeClient) Do() (*GiteeClient, error) {
// 多次调用首先置空
g.Response = nil
_payload, _ := json.Marshal(g.Payload)
req, _ := http.NewRequest(g.Method, g.Url, bytes.NewReader(_payload))
req.Header.Set("Content-Type", "application/json")
cookie := config.Conf.CookiesJar
accessToken := config.Conf.AccessToken
if accessToken != "" && !g.CookieAuth {
req.Header.Set("Authorization", "Bearer "+accessToken)
} else if cookie != "" {
req.Header.Set("Cookie", cookie)
} else {
color.Red("授权错误!")
os.Exit(1)
}
for key, value := range g.Headers {
req.Header.Set(key, value)
}
client := &http.Client{}
var resp *http.Response
var err error
if resp, err = client.Do(req); err != nil {
return g, err
}
g.Response = resp
return g, nil
}
func (g *GiteeClient) IsSuccess() bool {
if g.Response == nil {
return false
}
successMap := map[int]struct{}{
http.StatusOK: struct{}{},
http.StatusCreated: struct{}{},
http.StatusNoContent: struct{}{},
}
if _, ok := successMap[g.Response.StatusCode]; ok {
return true
}
return false
}
func (g *GiteeClient) IsFail() bool {
return !g.IsSuccess()
}
func (g *GiteeClient) GetRespBody() ([]byte, error) {
return ioutil.ReadAll(g.Response.Body)
}
func (g *GiteeClient) SetCookieAuth() {
g.CookieAuth = true
}

View File

@ -0,0 +1,35 @@
package issue_type_tui
import (
"errors"
"gitee_cli/internal/api/enterprises"
"gitee_cli/internal/api/issue_type"
"gitee_cli/utils/tui"
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"os"
)
func NewIssueTypeSelector(category string, issueTypes []issue_type.IssueType) *tea.Program {
rows := make([]table.Row, 0)
columns := []table.Column{{Title: category, Width: 20}}
for _, issueType := range issueTypes {
rows = append(rows, []string{issueType.Title})
}
t := tui.NewTableModel(enterprises.Enterprise{}, tui.IssueType, columns, rows)
return tea.NewProgram(t)
}
func SelectedValue(issueTypes []issue_type.IssueType, selectedKey string) (int, error) {
if selectedKey == "" {
os.Exit(0)
}
for _, issueType := range issueTypes {
if issueType.Title == selectedKey {
return issueType.Id, nil
}
}
return 0, errors.New("无效的选项!")
}

139
utils/tui/pager.go Normal file
View File

@ -0,0 +1,139 @@
package tui
import (
"fmt"
"github.com/charmbracelet/glamour"
"strings"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// You generally won't need this unless you're processing stuff with
// complicated ANSI escape sequences. Turn it on if you notice flickering.
//
// Also keep in mind that high performance rendering only works for programs
// that use the full size of the terminal. We're enabling that below with
// tea.EnterAltScreen().
const useHighPerformanceRenderer = true
const (
Markdown = iota
Diff
)
var (
titleStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Right = "├"
return lipgloss.NewStyle().BorderStyle(b).BorderForeground(lipgloss.Color("#008800")).Padding(0, 1)
}()
infoStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Left = "┤"
return titleStyle.Copy().BorderStyle(b)
}()
)
type model struct {
title string
content string
ready bool
viewport viewport.Model
}
func (m model) Init() tea.Cmd {
return nil
}
func NewPager(title, content string, contentType int) *tea.Program {
switch contentType {
case Markdown:
content, _ = glamour.Render(content, "dark")
}
return tea.NewProgram(model{title: title, content: content}, tea.WithAltScreen(), tea.WithMouseCellMotion())
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
switch msg := msg.(type) {
case tea.KeyMsg:
if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
return m, tea.Quit
}
case tea.WindowSizeMsg:
headerHeight := lipgloss.Height(m.headerView())
footerHeight := lipgloss.Height(m.footerView())
verticalMarginHeight := headerHeight + footerHeight
if !m.ready {
// Since this program is using the full size of the viewport we
// need to wait until we've received the window dimensions before
// we can initialize the viewport. The initial dimensions come in
// quickly, though asynchronously, which is why we wait for them
// here.
m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
m.viewport.YPosition = headerHeight
m.viewport.HighPerformanceRendering = useHighPerformanceRenderer
m.viewport.SetContent(m.content)
m.ready = true
// This is only necessary for high performance rendering, which in
// most cases you won't need.
//
// Render the viewport one line below the header.
m.viewport.YPosition = headerHeight + 1
} else {
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height - verticalMarginHeight
}
if useHighPerformanceRenderer {
// Render (or re-render) the whole viewport. Necessary both to
// initialize the viewport and when the window is resized.
//
// This is needed for high-performance rendering only.
cmds = append(cmds, viewport.Sync(m.viewport))
}
}
// Handle keyboard and mouse events in the viewport
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m model) View() string {
if !m.ready {
return "\n Initializing..."
}
return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
}
func (m model) headerView() string {
title := titleStyle.Render(m.title)
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
}
func (m model) footerView() string {
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}

View File

@ -0,0 +1,86 @@
package selector_tui
import (
"fmt"
"gitee_cli/utils"
tea "github.com/charmbracelet/bubbletea"
"os"
)
type MapSelector struct {
OptionsMap map[string]int
Options []string
Promote string
Cursor int
}
func NewMapSelector(optionsMap map[string]int, options []string, promote string) *tea.Program {
mapSelector := MapSelector{
OptionsMap: optionsMap,
Options: options,
Promote: promote,
}
return tea.NewProgram(mapSelector)
}
func (m MapSelector) Init() tea.Cmd {
return nil
}
func (m MapSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "up", "k":
if m.Cursor > 0 {
m.Cursor--
}
case "down", "j":
if m.Cursor < len(m.Options)-1 {
m.Cursor++
}
case "ctrl+c", "q":
m.Cursor = -1
return m, tea.Quit
case "enter":
return m, tea.Quit
}
}
return m, nil
}
func (m MapSelector) SelectedValue() (int, error) {
if m.Cursor == -1 {
os.Exit(0)
}
option := m.Options[m.Cursor]
if value, ok := m.OptionsMap[option]; ok {
return value, nil
}
return 0, fmt.Errorf("无效的选项")
}
func (m MapSelector) View() string {
promote := m.Promote + "\n"
for i, option := range m.Options {
cursor := " "
if i == m.Cursor {
cursor = utils.Green(">")
option = utils.Yellow(option)
}
promote += fmt.Sprintf("%s %s\n", cursor, option)
}
return promote
}
func chunkOptions(options map[string]int) []string {
_options := make([]string, 0)
for key, _ := range options {
_options = append(_options, key)
if len(_options) == 16 {
break
}
}
return _options
}

View File

@ -0,0 +1,57 @@
package tui
import (
"fmt"
"gitee_cli/utils"
tea "github.com/charmbracelet/bubbletea"
)
func InitialUploadSSHKeyTui(fileList []string) *tea.Program {
return tea.NewProgram(UploadSSHKeyTui{
FileList: fileList,
})
}
type UploadSSHKeyTui struct {
FileList []string
Cursor int
}
func (s UploadSSHKeyTui) Init() tea.Cmd {
return nil
}
func (s UploadSSHKeyTui) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "up", "k":
if s.Cursor > 0 {
s.Cursor--
}
case "down", "j":
if s.Cursor < len(s.FileList)-1 {
s.Cursor++
}
case "ctrl+c", "q":
s.Cursor = -1
return s, tea.Quit
case "enter":
return s, tea.Quit
}
}
return s, nil
}
func (s UploadSSHKeyTui) View() string {
promote := "请选择要上传的 SSH 公钥\n"
for i, file := range s.FileList {
cursor := " "
if i == s.Cursor {
cursor = utils.Green(">")
file = utils.Yellow(file)
}
promote += fmt.Sprintf("%s %s\n", cursor, file)
}
return promote
}

168
utils/tui/table.go Normal file
View File

@ -0,0 +1,168 @@
package tui
import (
"fmt"
"gitee_cli/config"
"gitee_cli/internal/api/enterprises"
"gitee_cli/internal/api/issue"
"gitee_cli/internal/api/pull_request"
"gitee_cli/utils/git_utils"
"github.com/atotto/clipboard"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/fatih/color"
"github.com/pkg/browser"
"os"
"strings"
)
var baseStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("240"))
const (
Issue = iota
IssueType
PullRequest
Enterprise
SSHKey
)
// Table TODO 初始化改成 options 模式
type Table struct {
table table.Model
SelectedKey string
ViewMode bool
ResourceType int
Enterprise enterprises.Enterprise
}
func NewTableModel(enterprise enterprises.Enterprise, resourceType int, columns []table.Column, rows []table.Row) Table {
height := 5
if resourceType == PullRequest {
height = 10
}
t := table.New(
table.WithColumns(columns),
table.WithRows(rows),
table.WithFocused(true),
table.WithHeight(height),
)
s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(false)
s.Selected = s.Selected.
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
if resourceType == PullRequest {
s.Selected = s.Selected.Background(lipgloss.Color("#8B4789"))
// 避免与 diff 快捷键冲突
t.KeyMap.HalfPageDown = key.NewBinding(
key.WithDisabled(),
)
}
t.SetStyles(s)
return Table{table: t, Enterprise: enterprise, ResourceType: resourceType}
}
func NewTable(enterprise enterprises.Enterprise, resourceType int, columns []table.Column, rows []table.Row) *tea.Program {
if resourceType == SSHKey || resourceType == Enterprise {
return tea.NewProgram(NewTableModel(enterprise, resourceType, columns, rows), tea.WithAltScreen())
}
return tea.NewProgram(NewTableModel(enterprise, resourceType, columns, rows))
}
func (t Table) Init() tea.Cmd { return nil }
func (t Table) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
if t.table.Focused() {
t.table.Blur()
} else {
t.table.Focus()
}
case "c":
if t.ResourceType == Issue || t.ResourceType == Enterprise {
clipboard.WriteAll(t.table.SelectedRow()[0])
} else if t.ResourceType == PullRequest {
clipboard.WriteAll(t.table.SelectedRow()[1])
}
case "q", "ctrl+c":
t.SelectedKey = ""
return t, tea.Quit
case "v":
if t.ResourceType == Issue {
if _issue, err := issue.Detail(t.Enterprise.Id, t.table.SelectedRow()[0]); err == nil {
NewPager(_issue.Title, _issue.Description, Markdown).Run()
} else {
color.Red("获取任务详情失败!")
return t, tea.Quit
}
} else if t.ResourceType == PullRequest {
path, _ := git_utils.ParseCurrentRepo()
if path == "" {
path = config.Conf.DefaultPathWithNamespace
}
t.SelectedKey = t.table.SelectedRow()[1]
if pullRequerst, err := pull_request.Detail(t.SelectedKey, path); err == nil {
NewPager(pullRequerst.Title, pullRequerst.Body, Markdown).Run()
} else {
color.Red("获取pr详情失败")
return t, tea.Quit
}
}
case "d":
if t.ResourceType == PullRequest {
path, _ := git_utils.ParseCurrentRepo()
t.SelectedKey = t.table.SelectedRow()[1]
if path == "" {
path = config.Conf.DefaultPathWithNamespace
}
if diff, err := pull_request.FetchPatchContent(t.SelectedKey, path); err == nil {
NewPager(t.table.SelectedRow()[0], diff, Diff).Run()
} else {
color.Red(err.Error())
return t, tea.Quit
}
}
case "enter":
t.SelectedKey = t.table.SelectedRow()[0]
if t.ResourceType == Issue {
url := fmt.Sprintf("https://e.gitee.com/%s/dashboard?issue=%s", t.Enterprise.Path, t.SelectedKey)
browser.OpenURL(url)
} else if t.ResourceType == PullRequest {
path, _ := git_utils.ParseCurrentRepo()
if path == "" {
path = config.Conf.DefaultPathWithNamespace
}
t.SelectedKey = t.table.SelectedRow()[1]
url := fmt.Sprintf("https://gitee.com/%s/pulls/%s", path, t.SelectedKey)
if os.Getenv("CONVERT_ENT_URL") != "" {
url = fmt.Sprintf("https://e.gitee.com/%s/repos/%s/pulls/%s", strings.Split(path, "/")[0], path, t.SelectedKey)
}
browser.OpenURL(url)
} else {
return t, tea.Quit
}
}
}
t.table, cmd = t.table.Update(msg)
return t, cmd
}
func (t Table) View() string {
return baseStyle.Render(t.table.View()) + "\n"
}