mangokit:golang web项目管理工具,使用proto定义http路由和错误
文章目录
- 前言
- 1、mangokit介绍
- 1.1 根据proto文件生成http路由
- 1.2 根据proto文件生成响应码
- 1.3 使用wire来管理依赖注入
- 2、mangokit实现
- 2.1 protobuf插件开发
- 2.2 mangokit工具
- 3、使用示例
- 3.1 创建新项目
- 3.2 添加新的proto文件
- 3.3 代码生成
前言
在使用gin框架开发web应用时,需要我们自己手动完成请求到结构体的反序列化,以及发送响应,如下:
func Handler(ctx *gin.Context) {user := new(User)if err := ctx.ShouldBind(&user); err != nil {...}...resp := serivce()...ctx.Json(http.StatusOk, resp)
}
显然,这些工作都是多余的,和业务无关的,每个handler都需要我们自己处理,非常的麻烦
为了解决这个问题,我们可以使用反射的方式来字段完成请求数据到结构体的映射;对于响应,则定义一个统一的结构体,并且让handler返回这个结构体,如下:
type Response struct {R RespValueStatus int
}type RespValue struct {Data interface{} `json:"data"`Codee int `json:"code"`Messagee string `json:"message"`
}func NewResponse(status, code int, message string, data interface{}) *Response {...
}
func Handler(ctx *gin.Context, user *User) *Response {...resp = service()...return NewResponse(http.StatusOk, 0, "success", resp)
}
在注册路由时,则需要使用反射来对我们的handler进行适配,使用反射机制创建请求参数,然后将数据反序列化为对应的结构体,然后调用我们定义的handler,并且获取到返回值,调用ctx.Json来发送
这种方式方便了我们的开发,但是使用反射会对程序带来一定的性能损失(但是在这里只是简单的使用,性能损失很少),并且使用反射容易出现错误
最近在使用了bilibili的kratos框架后,给了我一些灵感,我们完全可以使用proto来定义http的路由,然后生成反序列化的结构代码,并且可以使用proto来定义返回错误码等。
因此借鉴了kratos的设计,我实现了一个小工具用来加速我的web开发
github:https://github.com/mangohow/mangokit
1、mangokit介绍
mangokit是一个web项目的管理工具,它的功能如下:
- 根据预设的项目结构创建出一个web项目,使用已有的代码框架,减少工作量
- 使用proto来定义http路由以及错误码,使用相关工具生成代码,完成自动结构体反序列化以及返回值响应
- 使用wire来管理依赖注入,减少依赖管理的烦恼
1.1 根据proto文件生成http路由
proto定义文件如下:
hello.proto
syntax = "proto3";package hello.v1;import "google/api/annotations.proto";option go_package = "api/helloworld/v1;v1";// 定义service
service Hello {rpc SayHello (HelloRequest) returns (HelloReply) {option (google.api.http) = {get: "/hello/:name"};}
}message HelloRequest {string name = 1;
}message HelloResponse {string message = 2;
}
然后使用mangokit命令根据proto生成gin框架对应的路由处理器:
mangokit generate proto api
生成的文件如下:
hello.pb.go
hello_http_gin.pb.go
其中hello.pb.go是protoc --go-out生成的,而hello_http_gin.pb.go是我们自己写的proto插件protoc-gen-go-gin生成的
hello_http_gin.pb.go的代码如下:
// Code generated by protoc-gen-go-gin. DO NOT EDIT.
// versions:
// - protoc-gen-go-gin v1.0.0
// - protoc v3.20.1
// source: helloworld/v1/proto/hello.protopackage v1import ("context""net/http""github.com/mangohow/mangokit/serialize""github.com/mangohow/mangokit/transport/httpwrapper"
)type HelloHTTPService interface {SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}func RegisterHelloHTTPService(server *httpwrapper.Server, svc HelloHTTPService) {server.GET("/hello/:name", _Hello_SayHello_HTTP_Handler(svc))
}func _Hello_SayHello_HTTP_Handler(svc HelloHTTPService) httpwrapper.HandlerFunc {return func(ctx *httpwrapper.Context) error {in := new(HelloRequest)if err := ctx.BindRequest(in); err != nil {return err}value := context.WithValue(context.Background(), "gin-ctx", ctx)reply, err := svc.SayHello(value, in)if err != nil {return err}ctx.JSON(http.StatusOK, serialize.Response{Data: reply})return nil}
}
在上面生成的go代码中,包含一个接口的定义,其中包含了我们定义的handler方法
并且提供了RegisterHelloHTTPService函数来注册路由,注册的路由为_Hello_SayHello_HTTP_Handler函数,在这个函数中有反序列化的代码,以及响应代码
因此我们只需要实现HelloHTTPService中的方法,并且调用RegisterHelloHTTPService来注册路由即可,大大的减少了我们的工作量。
这有点类似于grpc的方式。
1.2 根据proto文件生成响应码
有时候只使用http的状态码是不够的,比如200表示请求成功,但是虽然请求成功了,还可能出现其它问题。
比如一个登录的接口,用户登录时可能出现以下的情况:1、用户不存在 2、密码错误 3、用户被封禁了
因此,我们需要定义相关的一些响应码来处理这些情况
proto定义文件如下:
errors.proto
syntax = "proto3";package errors.v1;
import "errors/errors.proto";option go_package = "api/errors/v1;v1";enum ErrorReason {// 设置缺省错误码option (errors.default_code) = 500;Success = 0 [(errors.code) = 200];// 为某个枚举单独设置错误码UserNotFound = 1 [(errors.code) = 200];UserPasswordIncorrect = 2 [(errors.code) = 200];UserBanned = 3 [(errors.code) = 200];
}
在上面的proto文件中,我们使用enum来定义响应码,其中包括int类型的响应码,以及返回的http状态码(errors.code)
然后使用mangokit来生成go代码:
mangokit generate proto api
生成的文件如下:
errors.pb.go
errors_errors.pb.go
其中errors.pb.go是protoc --go_out生成的,而errors_errors.pb.go同样也是自己编写的proto插件protoc-gen-go-error生成的
errors_errors.pb.go中的代码如下:
// Code generated by protoc-gen-go-error. DO NOT EDIT.
// versions:
// - protoc-gen-go-error v1.0.0
// - protoc v3.20.1
// source: errors/v1/proto/errors.protopackage v1import ("fmt""github.com/mangohow/mangokit/errors"
)func ErrorSuccess(format string, args ...interface{}) errors.Error {return errors.New(int32(ErrorReason_Success), 200, ErrorReason_Success.String(), fmt.Sprintf(format, args...))
}// 为某个枚举单独设置错误码
func ErrorUserNotFound(format string, args ...interface{}) errors.Error {return errors.New(int32(ErrorReason_UserNotFound), 200, ErrorReason_UserNotFound.String(), fmt.Sprintf(format, args...))
}func ErrorUserPasswordIncorrect(format string, args ...interface{}) errors.Error {return errors.New(int32(ErrorReason_UserPasswordIncorrect), 200, ErrorReason_UserPasswordIncorrect.String(), fmt.Sprintf(format, args...))
}func ErrorUserBanned(format string, args ...interface{}) errors.Error {return errors.New(int32(ErrorReason_UserBanned), 200, ErrorReason_UserBanned.String(), fmt.Sprintf(format, args...))
}
然后我们就可以调用这些函数来生成具体的响应码,减少我们的代码工作量
1.3 使用wire来管理依赖注入
wire是谷歌开源的一款依赖注入工具,相比于其它的反射式的依赖注入方式,wire采用代码生成的方式来完成依赖注入,代码运行效率更高
代码如下:
//go:generate wire
//go:build wireinject
// +build wireinjectpackage mainimport ("github.com/google/wire""mangokit_test/internal/conf""mangokit_test/internal/dao""mangokit_test/internal/server""mangokit_test/internal/service""github.com/mangohow/mangokit/transport/httpwrapper""github.com/sirupsen/logrus"
)func newApp(serverConf *conf.Server, dataConf *conf.Data, logger *logrus.Logger) (*httpwrapper.Server, func(), error) {panic(wire.Build(dao.ProviderSet, service.ProviderSet, server.NewHttpServer))
}
根据上面的代码,wire即可自动生成依赖创建的代码:
// Code generated by Wire. DO NOT EDIT.//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinjectpackage mainimport ("mangokit_test/internal/conf""mangokit_test/internal/dao""mangokit_test/internal/server""mangokit_test/internal/service""github.com/mangohow/mangokit/transport/httpwrapper""github.com/sirupsen/logrus"
)// Injectors from wire.go:func newApp(serverConf *conf.Server, dataConf *conf.Data, logger *logrus.Logger) (*httpwrapper.Server, func(), error) {db, cleanup, err := dao.NewFakeMysqlClient(dataConf)if err != nil {return nil, nil, err}greeterDao := dao.NewGreeterDao(db)greeterService := service.NewGreeterService(greeterDao, logger)httpwrapperServer := server.NewHttpServer(serverConf, logger, greeterService)return httpwrapperServer, func() {cleanup()}, nil
}
同样的mangokit中也添加了相应的指令来生成wire依赖注入代码
mangokit generate wire
2、mangokit实现
mangokit主要包含三个组件:
protoc-gen-go-ginprotoc-gen-go-errormangokit
protoc-gen-go-gin用于根据proto文件中定义的service来生成gin框架的路由代码
protoc-gen-go-error用于根据proto文件中定义的enum来生成相应的响应错误码
mangokit中则设置了多种指令用于管理项目,比如:
- 使用create命令来生成一个初始项目结构
- 使用add命令来添加proto文件、makefile或Dockerfile
- 使用generate命令来根据proto文件生成go代码、生成openapi以及生成wire依赖注入
2.1 protobuf插件开发
在使用protoc时可以指定其它的插件用于生成代码,比如:
--go_out则会调用protoc-gen-go插件来生成go的代码--go-grpc_out则会调用protoc-gen-go-grpc插件来生成grpc的代码
同样的,我们可以使用go来实现一个类似的插件,从而根据proto文件来生成gin框架的代码以及响应码代码
工作原理:
在使用 protoc --go-gin_out时,protoc会解析proto文件,然后生成抽象语法树,并且它会使用protobuf将语法树序列化为二进制序列,然后使用标准输入将二进制序列传入我们的插件中,然后再使用protobuf进行反序列化,然后我们在自己的程序中就可以根据提供的信息来生成go代码,比如:proto中定义的message、service、enum等
开发proto插件我们可以使用google.golang.org/protobuf/compiler/protogen库
我们可以参考kratos的代码来实现自己的代码:
https://github.com/go-kratos/kratos/tree/main/cmd/protoc-gen-go-errors
首先看main函数:
protogen.Options.Run来运行我们的程序
在传入的匿名函数中,我们会接收到protogen.Plugin参数,该参数中有proto文件中定义的各种结构的详细信息
然后我们可以遍历每个proto文件来生成相应的代码,在generateFile中生成代码
package mainimport ("flag""fmt""google.golang.org/protobuf/compiler/protogen""google.golang.org/protobuf/types/pluginpb"
)var (showVersion = flag.Bool("version", false, "print the version and exit")
)func main() {flag.Parse()if *showVersion {fmt.Printf("protoc-gen-go-gin %v\n", version)return}protogen.Options{ParamFunc: flag.CommandLine.Set,}.Run(func(plugin *protogen.Plugin) error {plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL)for _, f := range plugin.Files {if !f.Generate {continue}generateFile(plugin, f)}return nil})
}
在protogen.File中保存了一个proto文件中定义的各种结构解析后的信息:

详细代码参考:https://github.com/mangohow/mangokit
代码编写好之后编译为二进制程序,在使用protoc时指定插件名称,我们的插件一定要以protoc-gen开头,在指定插件名称时指定protoc-gen后面的部分拼接上_out即可;比如protoc-gen-go-error,在使用时为:protoc --go-error_out=. hello.proto
2.2 mangokit工具
mangokit使用cobra命令行工具开发,包含以下功能:
- 创建基础项目:根据预设的项目目录结构和代码生成
- 添加文件:包括api proto、error proto、makefile和Dockerfile
- 生成代码:包括go代码生成、wire生成和openapi生成

3、使用示例
3.1 创建新项目

首先使用mangokit来创建一个项目,项目目录为mangokit-test,go mod名称为mangokit_test
mangokit create mangokit-test mangokit_test

然后执行cd mangokit-test && go mod tidy来下载依赖
项目目录结构如下:
$ tree
.
|-- api
| |-- errors
| | `-- v1
| | |-- errors.pb.go
| | |-- errors_errors.pb.go
| | `-- proto
| | `-- errors.proto
| `-- helloworld
| `-- v1
| |-- greeter.pb.go
| |-- greeter_http_gin.pb.go
| `-- proto
| `-- greeter.proto
|-- cmd
| `-- server
| |-- main.go
| |-- wire.go
| `-- wire_gen.go
|-- configs
| `-- application.yaml
|-- internal
| |-- conf
| | |-- conf.pb.go
| | `-- conf.proto
| |-- dao
| | |-- dao.go
| | |-- data.go
| | `-- userdao.go
| |-- middleware
| |-- model
| | `-- user.go
| |-- server
| | `-- http.go
| `-- service
| |-- helloservice.go
| `-- service.go
|-- pkg
|-- test
|-- third_party
|-- go.mod
|-- go.sum
|-- makefile
|-- Dockerfile
|-- openapi.yaml32 directories, 52 files
api:api目录用来放置proto文件以及根据proto文件生成的go代码,通常将.proto文件放在proto文件夹下,而生成的代码放在它的上一级目录,这样看起来更清晰一些cmd:cmd目录存放了wire注入代码和main文件configs:configs目录用来放置程序的配置文件internal:internal用来存放本项目依赖的代码,不会暴露给其它的项目,其中包括middleware(中间件)、model(数据库结构体模型)、dao(数据库访问对象)、conf(配置信息代码)、server(服务初始化代码)、service(service的具体实现代码)pkg:用来存放一些共用代码test:存放测试代码third_party:其中包含一些使用到的proto的扩展文件
在创建项目时默认会从github拉取一个预制的项目结构,如果遇到网络问题导致无法拉取,则可以使用-r命令来指定其它的仓库,比如使用gitee:
mangokit create -r https://gitee.com/mangohow/mangokit-template mangokit-test mangokit_test
3.2 添加新的proto文件

可以使用下面的命令来添加新的proto文件
# 添加http api
mangokit add api api/helloworld/v1/proto hello.proto
然后就会在api/helloworld/v1/proto目录下生成一个hello.proto文件
syntax = "proto3";package hello.v1;import "google/api/annotations.proto";option go_package = "api/helloworld/v1;v1";service Hello {}
使用下面的命令来添加error proto
mangokit add error api/errors/v1/proto errorReason.proto
同样的,在api/errors/v1/proto目录下生成了errorReason.proto文件
syntax = "proto3";package errorReason.v1;import "errors/errors.proto";option go_package = "api/errors/v1;v1";enum ErrorReason {option (errors.default_code) = 500;Placeholder = 0 [(errors.code) = 0];}
除了添加proto文件,还可以添加预制的makefile和Dockerfile
3.3 代码生成

根据proto生成代码
# 根据api目录下的proto文件生成go代码
mangokit generate proto api
根据wire依赖注入生成代码:
mangokit generate wire
生成openapi文档
mangokit generate openapi
生成上面所有的三个项目
mangokit generate all
相关文章:
mangokit:golang web项目管理工具,使用proto定义http路由和错误
文章目录 前言1、mangokit介绍1.1 根据proto文件生成http路由1.2 根据proto文件生成响应码1.3 使用wire来管理依赖注入 2、mangokit实现2.1 protobuf插件开发2.2 mangokit工具 3、使用示例3.1 创建新项目3.2 添加新的proto文件3.3 代码生成 前言 在使用gin框架开发web应用时&a…...
微信小程序实现一个简单的登录功能
微信小程序实现一个简单的登录功能 功能介绍login.wxmllogin.jsuserInfo.wxmluserInfo.js解析 功能介绍 微信小程序实现一个简单的登录功能。包括一个登录页面和一个用户信息展示页面。在登录页面中输入用户名和密码,点击登录按钮进行验证,如果验证成功&…...
whisper深入-语者分离
文章目录 学习目标:如何使用whisper学习内容一:whisper 转文字1.1 使用whisper.load_model()方法下载,加载1.2 使用实例对文件进行转录1.3 实战 学习内容二:语者分离(pyannote.audio)pyannote.audio是huggi…...
LuaJava操作Java的方法
最近在学习lua,然后顺便看了下luaj,可能用的人比较少,网上关于luaj的文章较少,其中在网上找到这个博主的相关文章,很详细,对于要学习luaj的小伙伴可以两篇一起查看,本文在此基础上进行扩展。 …...
oracle怎样才算开启了内存大页?
oracle怎样才算开启了内存大页? 关键核查下面三点: 1./etc/sysctl.conf vm.nr_hugepages16384这是给了32G,计划sga给30G,一般需多分配2-4G sysctl -p生效 看cat /proc/meminfo|grep Huge啥结果? 这种明显是配了…...
【halcon深度学习之那些封装好的库函数】determine_dl_model_detection_param
determine_dl_model_detection_param 目标检测的数据准备过程中的有一个库函数determine_dl_model_detection_param “determine_dl_model_detection_param” 直译为 “确定深度学习模型检测参数”。 这个过程会自动针对给定数据集估算模型的某些高级参数,强烈建议…...
跟着我学Python进阶篇:01.试用Python完成一些简单问题
往期文章 跟着我学Python基础篇:01.初露端倪 跟着我学Python基础篇:02.数字与字符串编程 跟着我学Python基础篇:03.选择结构 跟着我学Python基础篇:04.循环 跟着我学Python基础篇:05.函数 跟着我学Python基础篇&#…...
neo4j-Py2neo使用
neo4j-Py2neo(一):基本库介绍使用 py2neo的文档地址:https://neo4j-contrib.github.io/py2neo/ py2neo的本质是可以采用两种方式进行操作,一种是利用cypher语句,一种是使用库提供的DataTypes,Data类的实例需要和远程…...
uint29传输格式
前言 不知道谁想出来的。 反正我是想不到。 我看网上也没人讲这个。 写篇博客帮一下素未谋面的网友。 uint29 本质上是网络传输的时候,借用至多4字节Bytes,表达29位的无符号整数。 读8位数字,判断小于128? 是的话,返回末7位…...
Linux:终端定时自动注销
这样防止了,当我们临时离开电脑这个空隙,被坏蛋给趁虚而入 定几十秒或者分钟,如果这个时间段没有输入东西那么就会自动退出 全局生效 这个系统中的所有用户生效 vim /etc/profile在末尾加入TMOUT10 TMOUT10 这个就是10 秒,按…...
STM32F103RCT6开发板M3单片机教程06--定时器中断
前言 除非特别说明,本章节描述的模块应用于整个STM32F103xx微控制器系列,因为我们使用是STM32F103RCT6开发板是mini最小系统板。本教程使用是(光明谷SUN_STM32mini开发板) STM32F10X定时器(Timer)基础 首先了解一下是STM32F10X…...
数据库故障Waiting for table metadata lock
场景:早上来发现一个程序,链接mysql数据库有点问题,随后排查,因为容器在k8s里面。所以尝试重启了pod没有效果 一、重启pod: 这里是几种在Kubernetes中重启Pod的方法: 删除Pod,利用Deployment重建 kubectl delete pod mypodDepl…...
Springboot数据校验与异常篇
一、异常处理 1.1Http状态码 HTTP状态码是指在HTTP通信过程中,服务器向客户端返回的响应状态。它通过3位数字构成,第一个数字定义了响应的类别,后两位数字没有具体分类作用。以下是常见的HTTP状态码及其含义: - 1xx(信…...
第三十六章 XML 模式的高级选项 - 创建子类型的替换组
文章目录 第三十六章 XML 模式的高级选项 - 创建子类型的替换组创建子类型的替换组将子类限制在替换组中 第三十六章 XML 模式的高级选项 - 创建子类型的替换组 创建子类型的替换组 XML 模式规范还允许定义替换组,这可以是创建选择的替代方法。语法有些不同。无需…...
堆与二叉树(上)
本篇主要讲的是一些概念,推论和堆的实现(核心在堆的实现这一块) 涉及到的一些结论,证明放到最后,可以选择跳过,知识点过多,当复习一用差不多,如果是刚学这一块的,建议打…...
HBase查询的一些限制与解决方案
Apache HBase 是一个开源的、非关系型、分布式数据库,它是 Hadoop 生态系统的一部分,用于存储和处理大量的稀疏数据。HBase 在设计上是为了提供快速的随机读写能力,但与此同时,它也带来了一些查询上的限制: 没有SQL支持…...
软件开发 VS Web开发
我的新书《Android App开发入门与实战》已于2020年8月由人民邮电出版社出版,欢迎购买。点击进入详情 目录 介绍: 角色和职责: 软件开发人员: Web开发人员: 技能: 软件开发人员: Web开发人…...
基于Springboot的旅游网站设计与实现(论文+调试+源码)
项目描述 临近学期结束,还是毕业设计,你还在做java程序网络编程,期末作业,老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下,你想解决的问…...
【从零开始学习--设计模式--策略模式】
返回首页 前言 感谢各位同学的关注与支持,我会一直更新此专题,竭尽所能整理出更为详细的内容分享给大家,但碍于时间及精力有限,代码分享较少,后续会把所有代码示例整理到github,敬请期待。 此章节介绍策…...
条款6:若不想使用编译器自动生成的函数,就该明确拒绝
有些场景我们不需要编译器默认实现的构造函数,拷贝构造函数,赋值函数,这时候我们应该明确的告诉编译器,我们不需要,一个可行的方法是将拷贝构造函数和赋值函数声明为private。 class HomeForSale { ... }; HomeForSal…...
黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
GitHub 趋势日报 (2025年06月08日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...
HarmonyOS运动开发:如何用mpchart绘制运动配速图表
##鸿蒙核心技术##运动开发##Sensor Service Kit(传感器服务)# 前言 在运动类应用中,运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据,如配速、距离、卡路里消耗等,用户可以更清晰…...
安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
springboot 日志类切面,接口成功记录日志,失败不记录
springboot 日志类切面,接口成功记录日志,失败不记录 自定义一个注解方法 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/***…...
git: early EOF
macOS报错: Initialized empty Git repository in /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/.git/ remote: Enumerating objects: 2691797, done. remote: Counting objects: 100% (1760/1760), done. remote: Compressing objects: 100% (636/636…...
