UPGIT切换其他图床

2022-12-24 0 265

原副标题:UPGIT转换其它图床

先说说个人经历,呵呵。

只不过昨晚蹲守发该文,也是即使在玩儿那些。

我以后写过一则该文,透过采用upgit来自动上载typora的相片。

彼时,用的是github,从那个工程项目的英文名字也能窥见,它如果最已经开始是为github写的,因此全力支持的最合适。

只不过github这类无所谓大难题,但即使不可否认的其原因,互联网是个大难题,即使我挂了栅栏,也会有不太好使的这时候。

因此,我预备换。

转换Gitee

看了呵呵它的可扩充条目

UPGIT切换其他图床

gitee码云,如果是合适的代替品,具体来说它是亚洲地区的商品,其二,它也是两个如前所述git的管理辅助工具辅助工具。我能间接把github上的工程项目布季夫到gitee上,把我大部份相片的后缀url换呵呵就好了。

只好,我Shahdol的已经开始换了,但,gitee给了我两个非常大的精采,她说即使检查和到我有很多相片快照,揣测我采用它做图床,不容许我把那个工程项目设成申明职权,换句话说,相片我能上载,但我不能出访。那我要它除了甚么用?

UPGIT切换其他图床

转换七牛云

既然gitee用不了,我总得换,再次浏览了一遍扩充条目,就七牛云比较熟悉,其它的也不知道会不会满足我的要求。既然有两个确定能满足我要求的,那就间接用它得了。

然后,就已经开始了我的踩坑之旅,呵呵。

第一件事,是配置文件的修改。

这块我觉得它文档写得不太详细,我是看了日志,然后又去扫了一遍源码,才知道是个怎么回事的。

按照正常逻辑,我觉得我把那个默认上载器给改了,然后把它相应的配置加上就能用了。

UPGIT切换其他图床

可惜并无法,会报错:

UPGIT切换其他图床

可能是即使一叶障目,我已经开始并没有意识到那个错误是说D:\Program Files\upgit\extensions那个文件夹找不到。我光看到冒号后面的信息了,我即使它说找不到要上载的文件,就上面灰色的那些,我去文件夹里看了一眼,是有的。

后来左看右看,在日志里终只好发现了难题,只好赶紧把文件夹加上吧。

UPGIT切换其他图床

但还是报错了:

UPGIT切换其他图床

我一时间觉得很费解,难道是我的英文名字输错啦?那要是输错了,如果输入甚么呢?我也无所谓头绪。

所幸去源码里搜了呵呵,那个unknown uploader从哪里报出来的。

只好,终于发现了难题,它需要两个相关的配置文件。

并且源码里有:

UPGIT切换其他图床

UPGIT切换其他图床

赶紧把我需要的copy下来。(我感觉吧,那些配置文件,理论上如果间接放在release包里的,还要自己去建文件夹,复制配置文件,感觉怪怪的,文档里提一句也行。)

这这时候,故事才刚刚已经开始,以后提到的github、gitee都能创建两个永久的token,这样把token写进配置文件中,就再也不用改了。

但,七牛云可能是即使上载是免费的,下载才要钱的其原因,因此对上载把控的比较严,因此,并没有永久的token,token有时效,过了时间需要重新请求token。

那么难题来了,upgit只提供了配置token的方式,并不全力支持刷新token。

UPGIT切换其他图床

那个这时候,我陷入了天人交战。

放弃upgit,自己写一套如前所述七牛云的文件上载的具?感觉又犯不着。继续采用upgit,修改呵呵七牛云的逻辑?但upgit是用go语言写的,我对go并不熟。

几番权衡之下,我感觉还是间接改源码比较快,虽然,我对go不熟,但,我看了一眼七牛云的官方文档

UPGIT切换其他图床

签名那些东西,不用我自己写。因此,改动还是比较小的。

我只用把配置文件里的token改成accessKeysecretKeybucket,然后在代码里

想好了,就开干吧。

修改upgit源码

还记得我前面说找unknown uploader报错从哪报的吗?就在那个方法里,虽然看不懂语法细节,但能大体知道这是在干嘛,根据uploaderId来判断要用哪个load,前面几个if如果都是以后特别实现的。

UPGIT切换其他图床

剩下的那些没有特别if的,都归到了extensions里面:

UPGIT切换其他图床

然后,我就照葫芦画瓢,这里抄点代码,哪里抄点代码,进行了如下修改:

具体来说,导包:

UPGIT切换其他图床

en

UPGIT切换其他图床

然后,把token放进config里

UPGIT切换其他图床

那我只不过替换呵呵token的值就行了

UPGIT切换其他图床

(我到这时候,间接把代码放到该文最末尾,省得排版太长)

至此,改动结束。

只不过,我不是太敢做一些结构上的修改。现在这么改一点都不优雅,但小步子总是没错的,先验证自己的想法没有难题,再优化也好,现在的目的仅仅是正确地运行。

代码改完了,下一步是编译啦,要让它变成可执行文件。

我看到根目录上有两个Makefile文件,但windows并没有GCC编译辅助工具,只好,要先安装,我装的是MinGW,参考的这篇该文,我就不详细说了:

https://blog.csdn.net/LinusZhao1018/article/details/82152960?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param

装好之后,到工程项目更目录,执行make命令,但,报错了:

UPGIT切换其他图床

UPGIT切换其他图床

查了呵呵,没有很快找到答案,我也不觉得真的是语法的难题,很可能是系统的难题,可能我是windows系统,作者是在linux上运行的也说不定。懒得去研究makefile的语法难题了。

看得出来真正有意义的是go build……这句话,那我自己按照我的系统拼一条命令出来不就行啦?

于是就有了:

go build -o ./dist/upgit_win_amd64.exe -ldflags=”-s -w” .

运行!

不出所料,又报错啦

ext_cmd.go:11:2:github.com/alexflint/[email protected]: Get“https://proxy.golang.org/github.com/alexflint/go-arg/@v/v1.4.3.zip”: dial tcp 142.251.43.17:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.

连接超时,把我的栅栏打开,也没有用。google了呵呵,看起来是很多人都会遇到的难题,把出访地址改成亚洲地区的就行:

go env -w GOPROXY=https://goproxy.cn

再次运行,还是报错:

ain.go:32:2:no required module provides package github.com/qiniu/go-sdk/v7/auth/qbox;to add it:

go get github.com/qiniu/go-sdk/v7/auth/qbox

main.go:33:5:no required module provides package github.com/qiniu/go-sdk/v7/storage;to add it:

go get github.com/qiniu/go-sdk/v7/storage

接着google,即使我没有在go.mod文件添加相应的require版本:

UPGIT切换其他图床

require github.com/qiniu/go-sdk/v7 v7.13.0

再次运行,还是报错,呵呵:

main.go:32:2:missing go.sum entry for module providing package github.com/qiniu/go-sdk/v7/auth/qbox(importedby github.com/pluveto/upgit);to add:

go get github.com/pluveto/upgit

main.go:33:5:missing go.sum entry for module providing package github.com/qiniu/go-sdk/v7/storage(imported by github.com/pluveto/upgit);toadd:

go get github.com/pluveto/upgit

再次google,好像是光修改go.mod文件不够,还需要运行下面的命令,来更新:

go mod tidy

然后再次运行,虽然还是报错了,但和环境无关了,终于和我的代码有关了,我的语法有些难题。

这么简单分享一点,今天学到的go语法相关知识。

1.:==

:=表示初始化,并赋值

=表示单纯的赋值

2.go的方法,参数在前,类型在后

3.go能返回多个返回值

原本下面这句话,我抄过来的这时候,把err给去掉了

但报错了:

# github.com/pluveto/upgit

.\main.go:329:16: assignment mismatch: 1 variable but xapp.LoadUploaderConfig[map[string]interface{}] returns 2 values

qiniuConfig[“bucket”]

设置值:extConfig[“token”]=upToken

5.go的类型转换是.(type)放在后面的形式

原本我是这么写的:

bucket:=qiniuConfig[“bucket”]

accessKey:=qiniuConfig[“accessKey”]

secretKey:=qiniuConfig[“secretKey”]

但报错了:

.\main.go:334:12: cannot use bucket (variable of type interface{}) as type string in struct literal:

need type assertion

.\main.go:336:22: cannot use accessKey (variable of type interface{}) as type string in argument to qbox.NewMac:

need type assertion

.\main.go:336:33: cannot use secretKey (variable of type interface{}) as type string in argument to qbox.NewMac:

need type assertion

只好,转成string类型:

bucket:=qiniuConfig[“bucket”].(string)

accessKey:=qiniuConfig[“accessKey”].(string)

secretKey:=qiniuConfig[“secretKey”].(string)

至此,我的程序可算是成功编译了。

除了另外两个令人高兴的事情是,我一次就改成功了。

我把配置文件改好之后,就能正常采用了,这篇该文里的相片,是传到七牛云的。

配置文件:

config.toml

# 默认上载器

default_uploader=“qiniu”

# 七牛云存储

[uploaders.qiniu]

bucket=“moqian-public”

accessKey=“xxxxxxxxxxxxxxxxxxx”

secretKey=“xxxxxxxxxxxxxxxxx”

#你的域名后缀

prefix=“http://file.moqian.cn/”

qiniu.jsonc

那个只不过用源码里的就好了,只有两个地方可能要改,那个url,上载失败的话,返回的信息会告诉你,如果用哪个url的。

最后放上main.go的代码:

package main

import(

“errors”

“fmt”

“io/fs”

“io/ioutil”

“os”

“path/filepath”

“runtime”

“strings”

“syscall”

“time”

“github.com/alexflint/go-arg”

“github.com/pelletier/go-toml/v2”

“github.com/pluveto/upgit/lib/model”

“github.com/pluveto/upgit/lib/qcloudcos”

“github.com/pluveto/upgit/lib/result”

“github.com/pluveto/upgit/lib/uploaders”

“github.com/pluveto/upgit/lib/upyun”

“github.com/pluveto/upgit/lib/xapp”

“github.com/pluveto/upgit/lib/xclipboard”

“github.com/pluveto/upgit/lib/xext”

“github.com/pluveto/upgit/lib/xio”

“github.com/pluveto/upgit/lib/xlog”

“github.com/pluveto/upgit/lib/xmap”

“github.com/pluveto/upgit/lib/xpath”

“github.com/pluveto/upgit/lib/xstrings”

“golang.design/x/clipboard”

“gopkg.in/validator.v2”

“github.com/qiniu/go-sdk/v7/auth/qbox”

“github.com/qiniu/go-sdk/v7/storage”

)

func main(){

result.AbortErr = xlog.AbortErr

iflen(os.Args)>=2&& os.Args[1]==“ext”{

extSubcommand()

return

}

mainCommand()

}

func mainCommand(){

// parse cli args

loadCliOpts()

// load config

loadEnvConfig(&xapp.AppCfg)

loadConfig(&xapp.AppCfg)

xlog.GVerbose.TraceStruct(xapp.AppCfg)

// handle clipboard if need

loadClipboard()

// validating args

validArgs()

// executing uploading

dispatchUploader()

if xapp.AppOpt.Wait {

fmt.Scanln()

}

return

}

// loadCliOpts load cli options into xapp.AppOpt

funcloadCliOpts(){

arg.MustParse(&xapp.AppOpt)

xapp.AppOpt.TargetDir = strings.Trim(xapp.AppOpt.TargetDir,“/”)

xapp.AppOpt.ApplicationPath = strings.Trim(xapp.AppOpt.ApplicationPath,“/”)

iflen(xapp.AppOpt.ApplicationPath)>0{

xpath.ApplicationPath = xapp.AppOpt.ApplicationPath

}

if xapp.AppOpt.SizeLimit !=nil&&*xapp.AppOpt.SizeLimit >=0{

xapp.MaxUploadSize =*xapp.AppOpt.SizeLimit

}

iffalse== xapp.AppOpt.NoLog {

xlog.GVerbose.LogEnabled=true

xlog.GVerbose.LogFile = xpath.MustGetApplicationPath(“upgit.log”)

xlog.GVerbose.LogFileMaxSize=2*1024*1024// 2MiB

xlog.GVerbose.Info(“Started”)

xlog.GVerbose.TruncatLog()

}

xlog.GVerbose.VerboseEnabled= xapp.AppOpt.Verbose

xlog.GVerbose.TraceStruct(xapp.AppOpt)

}

func onUploaded(r result.Result[*model.Task]){

if!r.Ok()&& xapp.AppOpt.OutputType == xapp.O_Stdout {

fmt.Println(“Failed: “+ r.Err.Error())

return

}

if xapp.AppOpt.Clean &&!r.Value.Ignored {

err := os.Remove(r.Value.LocalPath)

if err !=nil{

xlog.GVerbose.Info(“Failed to remove %s: %s”, r.Value.LocalPath, err.Error())

}else{

xlog.GVerbose.Info(“Removed %s”, r.Value.LocalPath)

}

}

outputLink(*r.Value)

recordHistory(*r.Value)

}

func mustMarshall(s interface{})string{

b, err := toml.Marshal(s)

if err !=nil{

return“”

}

returnstring(b)

}

func recordHistory(r model.Task){

xio.AppendToFile(xpath.MustGetApplicationPath(“history.log”),[]byte(

`{“time”:”`+time.Now().Local().String()+`”,”rawUrl”:”`+r.RawUrl+`”,”url”:”`+r.Url+`”}`+\n),

)

xlog.GVerbose.Info(mustMarshall(r))

}

funcoutputLink(r model.Task){

outContent, err := outputFormat(r)

xlog.AbortErr(err)

switch xapp.AppOpt.OutputType {

case xapp.O_Stdout:

fmt.Println(outContent)

case xapp.O_Clipboard:

clipboard.Write(clipboard.FmtText,[]byte(outContent))

default:

xlog.AbortErr(errors.New(“unknown output type: “+string(xapp.AppOpt.OutputType)))

}

}

func outputFormat(r model.Task)(content string, err error){

var outUrl string

if xapp.AppOpt.Raw || r.Url ==“”{

outUrl = r.RawUrl

}else{

outUrl = r.Url

}

fmt := xapp.AppOpt.OutputFormat

if fmt ==“”{

return outUrl,nil

}

val, ok :=xapp.AppCfg.OutputFormats[fmt]

if!ok {

return“”, errors.New(“unknown output format: “+ fmt)

}

content =strings.NewReplacer(

“{url}”, outUrl,

“{urlfname}”, filepath.Base(outUrl),

“{fname}”, filepath.Base(r.LocalPath),

).Replace(xstrings.RemoveFmtUnderscore(val))

return

}

func validArgs(){

if errs := validator.Validate(xapp.AppCfg); errs !=nil{

xlog.AbortErr(fmt.Errorf(“incorrect config: “+ errs.Error()))

}

for _, path :=range xapp.AppOpt.LocalPaths {

if strings.HasPrefix(path,“http”){

continue

}

fs, err := os.Stat(path)

if errors.Is(err, os.ErrNotExist){

xlog.AbortErr(fmt.Errorf(“invalid file to upload %s: no such file”, path))

}

if err !=nil{

xlog.AbortErr(fmt.Errorf(“invalid file to upload %s: %s”, path, err.Error()))

}

if fs.Size()==0{

xlog.AbortErr(fmt.Errorf(“invalid file to upload %s: file size is zero”, path))

}

if xapp.MaxUploadSize !=0&& fs.Size()> xapp.MaxUploadSize {

xlog.AbortErr(fmt.Errorf(“invalid file to upload %s: file size is larger than %d bytes”, path, xapp.MaxUploadSize))

}

}

}

// loadConfig loads config from config file to xapp.AppCfg

func loadConfig(cfg *xapp.Config){

homeDir, err := os.UserHomeDir()

if err !=nil{

homeDir =“”

}

appDir := xpath.MustGetApplicationPath(“”)

var configFiles =map[string]bool{

filepath.Join(homeDir,“.upgit.config.toml”):false,

filepath.Join(homeDir, filepath.Join(“.config”,“upgitrc”)):false,

filepath.Join(appDir,“config.toml”):false,

filepath.Join(appDir,“upgit.toml”):false,

}

if xapp.AppOpt.ConfigFile !=“”{

configFiles[xapp.AppOpt.ConfigFile]=true

}

for configFile, required :=range configFiles {

if _, err := os.Stat(configFile); err !=nil{

ifrequired{

xlog.AbortErr(fmt.Errorf(“config file %s not found”, configFile))

}

continue

}

optRawBytes, err :=ioutil.ReadFile(configFile)

if err ==nil{

err = toml.Unmarshal(optRawBytes,&cfg)

}

if err !=nil{

xlog.AbortErr(fmt.Errorf(“invalid config: “+ err.Error()))

}

xapp.ConfigFilePath = configFile

break

}

if xapp.ConfigFilePath==“”{

xlog.AbortErr(fmt.Errorf(“no config file found”))

}

// fill config

xapp.AppCfg.Rename = strings.Trim(xapp.AppCfg.Rename,“/”)

xapp.AppCfg.Rename = xstrings.RemoveFmtUnderscore(xapp.AppCfg.Rename)

// — integrated formats

ifnil== xapp.AppCfg.OutputFormats {

xapp.AppCfg.OutputFormats =make(map[string]string)

}

xapp.AppCfg.OutputFormats[“markdown”]=`![{url_fname}]({url})`

xapp.AppCfg.OutputFormats[“url”]=`{url}`

}

// UploadAll will upload all given file to targetDir.

// If targetDir is not set, it will upload using rename rules.

func UploadAll(uploader model.Uploader, localPaths []string, targetDir string){

for taskId,localPath:=range localPaths {

var ret result.Result[*model.Task]

task := model.Task{

Status: model.TASK_CREATED,

TaskId: taskId,

LocalPath: localPath,

TargetDir: targetDir,

RawUrl:“”,

Url:“”,

CreateTime: time.Now(),

}

varerrerror

// ignore non-local path

if strings.HasPrefix(localPath,“http”){

task.Ignored =true

task.Status =model.TASK_FINISHED

}else{

err = uploader.Upload(&task)

}

if err !=nil{

task.Status = model.TASK_FAILED

ret =result.Result[*model.Task]{

Err: err,

}

}else{

ret = result.Result[*model.Task]{

Value:&task,

}

}

if err ==nil{

xlog.GVerbose.TraceStruct(ret.Value)

}

callback := uploader.GetCallback()

ifnil!= callback {

callback(ret)

}

}

}

funcdispatchUploader(){

uploaderId := xstrings.ValueOrDefault(xapp.AppOpt.Uploader, xapp.AppCfg.DefaultUploader)

xlog.GVerbose.Info(“uploader: “+ uploaderId)

if uploaderId ==“github”{

gCfg, err := xapp.LoadUploaderConfig[uploaders.GithubUploaderConfig](uploaderId)

xlog.AbortErr(err)

err = validator.Validate(&gCfg)

xlog.AbortErr(err)

iflen(gCfg.Branch)==0{

gCfg.Branch = xapp.DefaultBranch

}

uploader := uploaders.GithubUploader{Config: gCfg, OnTaskStatusChanged: onUploaded}

UploadAll(uploader, xapp.AppOpt.LocalPaths, xapp.AppOpt.TargetDir)

return

}

if uploaderId ==“qcloudcos”{

qCfg, err := xapp.LoadUploaderConfig[qcloudcos.COSConfig](uploaderId)

xlog.AbortErr(err)

err = validator.Validate(&qCfg)

xlog.AbortErr(err)

xlog.GVerbose.Trace(“qcloudcos config: “)

xlog.GVerbose.TraceStruct(&qCfg)

uploader := qcloudcos.COSUploader{Config: qCfg,OnTaskStatusChanged: onUploaded}

UploadAll(uploader, xapp.AppOpt.LocalPaths, xapp.AppOpt.TargetDir)

return

}

if uploaderId ==“upyun”{

ucfg, err := xapp.LoadUploaderConfig[upyun.UpyunConfig](uploaderId)

xlog.AbortErr(err)

err =validator.Validate(&ucfg)

xlog.AbortErr(err)

xlog.GVerbose.Trace(“qcloudcos config: “)

xlog.GVerbose.TraceStruct(&ucfg)

uploader := upyun.UpyunUploader{Config: ucfg, OnTaskStatusChanged: onUploaded}

UploadAll(uploader, xapp.AppOpt.LocalPaths, xapp.AppOpt.TargetDir)

return

}

upToken :=“”

if uploaderId ==“qiniu”{

qiniuConfig,err:=xapp.LoadUploaderConfig[map[string]interface{}](uploaderId)

xlog.AbortErr(err)

bucket:=qiniuConfig[“bucket”].(string)

accessKey:=qiniuConfig[“accessKey”].(string)

secretKey:=qiniuConfig[“secretKey”].(string)

putPolicy := storage.PutPolicy{

Scope: bucket,

}

mac := qbox.NewMac(accessKey, secretKey)

upToken =putPolicy.UploadToken(mac)

}

// try http simple uploader

// list file in ./extensions

extDir := xpath.MustGetApplicationPath(“extensions”)

info, err := ioutil.ReadDir(extDir)

xlog.AbortErr(err)

var uploader *uploaders.SimpleHttpUploader

for _, f :=range info {

fname := f.Name()

xlog.GVerbose.Trace(“found file %s”, fname)

if!strings.HasSuffix(fname,“.json”)&&!strings.HasSuffix(fname,“.jsonc”){

xlog.GVerbose.Trace(“ignored file %s”, fname)

continue

}

// load file to json

uploaderDef, err := xext.GetExtDefinitionInterface(extDir, fname)

xlog.AbortErr(err)

if result.From[string](xmap.GetDeep[string](uploaderDef,`meta.id`)).ValueOrExit()!=uploaderId{

continue

}

if result.From[string](xmap.GetDeep[string](uploaderDef,“meta.type”)).ValueOrExit()!=“simple-http-uploader”{

continue

}

uploader =&uploaders.SimpleHttpUploader{OnTaskStatusChanged:onUploaded, Definition: uploaderDef}

extConfig, err := xapp.LoadUploaderConfig[map[string]interface{}](uploaderId)

if err ==nil{

if uploaderId ==“qiniu”{

extConfig[“token”]=upToken

}

uploader.Config = extConfig

xlog.GVerbose.Trace(“uploader config:”)

xlog.GVerbose.TraceStruct(uploader.Config)

}else{

xlog.GVerbose.Trace(“no uploader config found”)

}

break

}

ifnil== uploader {

xlog.AbortErr(errors.New(“unknown uploader: “+uploaderId))

}

UploadAll(uploader, xapp.AppOpt.LocalPaths, xapp.AppOpt.TargetDir)

return

}

func loadClipboard(){

iflen(xapp.AppOpt.LocalPaths)==1&& strings.ToLower(xapp.AppOpt.LocalPaths[0])== xapp.ClipboardPlaceholder {

err:= clipboard.Init()

if err !=nil{

xlog.AbortErr(fmt.Errorf(“failed to init clipboard: “+ err.Error()))

}

tmpFileName:= fmt.Sprint(os.TempDir(),“/upgit_tmp_”, time.Now().UnixMicro(),“.png”)

buf := clipboard.Read(clipboard.FmtImage)

ifnil== buf {

// try second chance for Windows user. To adapt bitmap format (compatible with Snipaste)

if runtime.GOOS ==“windows”{

buf, err = xclipboard.ReadClipboardImage()

}

if err !=nil{

xlog.GVerbose.Error(“failed to read clipboard image: “+ err.Error())

}

}

ifnil== buf {

xlog.AbortErr(fmt.Errorf(“failed: no image in clipboard or unsupported format”))

}

os.WriteFile(tmpFileName, buf, os.FileMode(fs.ModePerm))

xapp.AppOpt.LocalPaths[0]= tmpFileName

xapp.AppOpt.Clean =true

}

}

func loadEnvConfig(cfg *xapp.Config){

ifnil== cfg {

xlog.AbortErr(fmt.Errorf(“unable to load env config: nil config”))

}

if rename, found :=syscall.Getenv(“UPGIT_RENAME”); found {

cfg.Rename = rename

}

}

func loadGithubUploaderEnvConfig(gCfg *uploaders.GithubUploaderConfig){

// TODO: Auto generate env key name and adapt for all uploaders

if pat, found :=syscall.Getenv(“GITHUB_TOKEN”); found {

gCfg.PAT = pat

}

if pat, found := syscall.Getenv(“UPGIT_TOKEN”); found {

gCfg.PAT = pat

}

if username, found := syscall.Getenv(“UPGIT_USERNAME”); found {

gCfg.Username = username

}

if repo,found:= syscall.Getenv(“UPGIT_REPO”); found {

gCfg.Repo = repo

}

if branch, found := syscall.Getenv(“UPGIT_BRANCH”); found {

gCfg.Branch = branch

}

}

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务