二丫讲梵 二丫讲梵
首页
  • 最佳实践
  • 迎刃而解
  • Nginx
  • Php
  • Zabbix
  • Prometheus
  • Grafana
  • CentOS
  • Systemd
  • Docker
  • Rancker
  • Ansible
  • Ldap
  • Gitlab
  • GitHub
  • Etcd
  • Consul
  • RabbitMQ
  • Kafka
  • MySql
  • MongoDB
  • OpenVPN
  • KVM
  • VMware
  • Other
  • ELK
  • K8S
  • Nexus
  • Jenkins
  • 随写编年
  • 家人物语
  • 追忆青春
  • 父亲的朋友圈
  • 电影音乐
  • 效率工具
  • 博客相关
  • Shell
  • 前端实践
  • Vue学习笔记
  • Golang学习笔记
  • Golang编程技巧
  • 学习周刊
  • Obsidian插件周刊
关于
友链
推广
  • 本站索引

    • 分类
    • 标签
    • 归档
  • 本站页面

    • 导航
    • 打赏
  • 我的工具

    • 备忘录清单 (opens new window)
    • 网站状态 (opens new window)
    • json2go (opens new window)
    • 微信MD编辑 (opens new window)
    • 国内镜像 (opens new window)
    • 出口IP查询 (opens new window)
    • 代码高亮工具 (opens new window)
  • 外站页面

    • 开往 (opens new window)
    • ldapdoc (opens new window)
    • HowToStartOpenSource (opens new window)
    • vdoing-template (opens new window)
GitHub (opens new window)

二丫讲梵

行者常至,为者常成
首页
  • 最佳实践
  • 迎刃而解
  • Nginx
  • Php
  • Zabbix
  • Prometheus
  • Grafana
  • CentOS
  • Systemd
  • Docker
  • Rancker
  • Ansible
  • Ldap
  • Gitlab
  • GitHub
  • Etcd
  • Consul
  • RabbitMQ
  • Kafka
  • MySql
  • MongoDB
  • OpenVPN
  • KVM
  • VMware
  • Other
  • ELK
  • K8S
  • Nexus
  • Jenkins
  • 随写编年
  • 家人物语
  • 追忆青春
  • 父亲的朋友圈
  • 电影音乐
  • 效率工具
  • 博客相关
  • Shell
  • 前端实践
  • Vue学习笔记
  • Golang学习笔记
  • Golang编程技巧
  • 学习周刊
  • Obsidian插件周刊
关于
友链
推广
  • 本站索引

    • 分类
    • 标签
    • 归档
  • 本站页面

    • 导航
    • 打赏
  • 我的工具

    • 备忘录清单 (opens new window)
    • 网站状态 (opens new window)
    • json2go (opens new window)
    • 微信MD编辑 (opens new window)
    • 国内镜像 (opens new window)
    • 出口IP查询 (opens new window)
    • 代码高亮工具 (opens new window)
  • 外站页面

    • 开往 (opens new window)
    • ldapdoc (opens new window)
    • HowToStartOpenSource (opens new window)
    • vdoing-template (opens new window)
GitHub (opens new window)
  • 最佳实践

    • 一句话经验
    • 基于CNAME解析实践的域名优雅方案
    • 如何配置历史命令中有详细的时间戳
    • 建设一个运维外挂的最佳实践
    • 关于打包压缩几种格式(gzip,bzip2,xz)的试验对比
    • 利用谷歌浏览器自定义agent监听日志来调试服务
    • fdisk,parted使用非交互式方式对磁盘进行分区操作
    • GitLab部署的最佳实践
    • GitLab全局搜索之SourceGraph
    • vector实践-性能吊打logstash
    • 规范编码之利用pre-commit给项目添加提交前检查
      • 前言
      • 关于git hook
      • 安装体验
        • 安装
        • 配置
        • 运行
        • 安装
      • Go 项目实践
      • 补充
        • hooks 配置文件
        • 其他内容快链
        • 一些可用的检查概览
  • 迎刃而解

  • Nginx

  • Php

  • Zabbix

  • Prometheus

  • Grafana

  • CentOS

  • Supervisord

  • Systemd

  • Docker

  • Docker-Compose

  • Rancher

  • Ansible

  • OpenLdap

  • GitLab

  • GitHub

  • Etcd

  • Consul

  • RabbitMQ

  • Kafka

  • Mysql

  • MongoDB

  • OpenVPN

  • Kvm

  • VMware

  • 配置文件详解

  • Other

  • 运维观止
  • 最佳实践
二丫讲梵
2023-02-26
目录

规范编码之利用pre-commit给项目添加提交前检查

这篇文章的发布时间较早,其中的内容可能已经过时,阅读时请注意甄别。

# 前言

日常开发过程中,不论是哪个语言,一定都会有相对应的语法检测工具或者手段来辅助我们检查出编码过程中的一些遗漏或疏忽。但有时候会有一个尴尬的情况就是,把检测的方式配置上去了,却没有运行,最后成了摆设。

今天来介绍一个工具,能够完成在代码提交之前运行指定检测,从而实现代码的自检。

  • 项目:pre-commit (opens new window)
  • 官网: https://pre-commit.com/ (opens new window)
  • 开箱即用:pre-commit-hooks (opens new window)

pre-commit 的运行机制借助于 git hook 来完成提交之前的一些预定义指令的运行,来达到提交前检测的目的。

# 关于git hook

Git 能在特定的重要动作发生时触发自定义脚本钩子。钩子分为两组:

  • 客户端钩子:pre-commit, prepare-commit-msg, commit-msg, post-commit 等,主要在服务端接收提交对象时、推送到服务器之前调用。
  • 服务器钩子:pre-receive, post-receive, update 等,主要在服务端接收提交对象时、推送到服务器之前调用。

git hooks 位置位于每个 git 项目下的 .git/hooks 目录里,进去后会看到这些钩子的官方示例,都是以 .sample 结尾的文件,这些示例脚本是不会执行的,去掉 .sample 后缀可激活该钩子脚本。

PS:GIt hooks 的每个钩子的作用和说明,详细的以官方文档为准: https://git-scm.com/docs/githooks

# 安装体验

# 安装

pre-commit 是一个 Python 语言写的工具,通过如下命令能够轻松安装该工具:

$ pip3 install pre-commit
1

查看版本:

$ pre-commit --version
pre-commit 3.1.0
1
2

如果这条命令运行没有问题,则说明安装成功。

# 配置

现在我拿 learn-github (opens new window) 项目来作为示例进行体验。

pre-commit 以 .pre-commit-config.yaml 文件作为默认的配置文件,在项目根目录执行如下命令生成简单的配置内容:

$ pre-commit sample-config > .pre-commit-config.yaml
1

查看一下配置文件的内容信息:

$ cat .pre-commit-config.yaml
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml
    -   id: check-added-large-files
1
2
3
4
5
6
7
8
9
10
11
  • repos:表示一系列仓库的映射。
    • repo:表示接下来使用的 hooks 脚本从哪个仓库进行拉取。
    • rev:指定将要拉取的 tag 。
    • hooks:钩子脚本列表,这些脚本来自于 repo 定义的仓库中。
      • id:指定将要应用的钩子的名称,就是对应的文件名。

其中 hooks 还有更加丰富的配置信息,这里暂不展开,随后再进行完整介绍。

简单解释下如上配置文件的意思:在代码提交之前,会运行 hooks 列表中的这些检查,这些脚本来自于 https://github.com/pre-commit/pre-commit-hooks 这个仓库的 v4.4.0。四个检查脚本的含义如下:

  • trailing-whitespace :检查修建行尾的空格
  • end-of-file-fixer :确保文件以换行符结尾且仅以换行符结尾。
  • check-yaml :检查 yaml 文件的语法。
  • check-added-large-files :防止提交大文件。(默认检测阈值为 500KB)

# 运行

执行 pre-commit run --all-files 命令,可以手动运行 pre-commit 的检查:

$ pre-commit run --all-files
[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook

Fixing workflows-tested/rss.yml

Check Yaml...............................................................Passed
Check for added large files..............................................Passed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

📢注意: 钩子添加完毕之后,默认情况下,pre-commit 只会检测当次变更了的文件,因此一般建议在添加之后,运行一次针对项目的全面检测。

如上内容表示运行 Fix End of Files 这个脚本的时候发现有不符合检测规范的内容,然后自动 fix 掉了,这些检测脚本都是根据个人需求按需加载,因此这里我就把这个脚本去掉了。

# 安装

上边是手动运行的,我们还应该运行一下安装命令,把 pre-commit 的配置文件加载到 git hooks 当中:

$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
1
2

这个时候,再次运行常规的提交步骤就会触发检测了:

$ gcmsg '添加pre commit'
[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
trim trailing whitespace.................................................Passed
check yaml...............................................................Passed
check for added large files..............................................Passed
[main aeb4728] 添加pre commit
 1 file changed, 9 insertions(+)
 create mode 100644 .pre-commit-config.yaml
1
2
3
4
5
6
7
8
9
10
11

# Go 项目实践

以上内容介绍了 pre-commit 的简单配置以及使用,接下来我们测试一个 go 项目的实践。

然后在项目根目录添加如下配置文件 .pre-commit-config.yaml :

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
    - id: check-yaml
    - id: trailing-whitespace
    - id: check-added-large-files
-   repo: https://github.com/golangci/golangci-lint # golangci-lint hook repo
    rev: v1.47.3 # golangci-lint hook repo revision
    hooks:
    - id: golangci-lint
      name: golangci-lint
      description: Fast linters runner for Go.
      entry: golangci-lint run --fix
      types: [go]
      language: golang
      pass_filenames: false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

然后运行如下命令将 hooks 载入到 git 配置文件中:

$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
1
2

然后将代码某处的 err 错误忽略不做处理,此时提交代码看看是否会检查:

$ gcmsg 'test pre check'
Check Yaml...............................................................Passed
Trim Trailing Whitespace.................................................Passed
Check for added large files..............................................Passed
golangci-lint............................................................Failed
- hook id: golangci-lint
- exit code: 1

config/config.go:30:11: ineffectual assignment to err (ineffassign)
        workDir, err := os.Getwd()
                 ^
1
2
3
4
5
6
7
8
9
10
11

如此就实现了一个简单的提交前的 lint 检查,一些简单的语法问题就能在这里抛出来了。

其他语言同理,pre-commit 官方提供了大量检测脚本集成,各语言都有,大家可按需进行了解使用。

# 补充

# hooks 配置文件

内容摘自官方文档:

id (opens new window) 钩子的 id - 在 pre-commit-config.yaml 中使用。
name (opens new window) 挂钩的名称 - 在挂钩执行期间显示。
entry (opens new window) 入口点 - 要运行的可执行文件。 entry 还可以包含不会被覆盖的参数,例如 entry: autopep8 -i.
language (opens new window) 钩子的语言 - 告诉预提交如何安装钩子。
files (opens new window) (可选:默认 '')要运行的文件模式。
exclude (opens new window) (可选:默认 ^$)排除匹配的文件 files (opens new window)。
types (opens new window) (可选:默认 [file])要运行的文件类型列表(AND)。请参阅 使用类型过滤文件 (opens new window)。
types_or (opens new window) (可选:默认 [])要运行的文件类型列表(或)。请参阅 使用类型过滤文件 (opens new window)。 2.9.0 中的新功能。
exclude_types (opens new window) (可选:默认 [])要排除的文件模式。
always_run (opens new window) (可选:默认 false)即使 true 没有匹配的文件,这个钩子也会运行。
fail_fast (opens new window) (可选:默认 false)如果 true 此挂钩失败,预提交将停止运行挂钩。 2.16.0 中的新功能。
verbose (opens new window) (可选:默认 false)如果 true,即使挂钩通过,也强制打印挂钩的输出。
pass_filenames (opens new window) (可选:默认 true)如果 false 没有文件名将传递给挂钩。
require_serial (opens new window) (可选:默认 false)如果 true 这个钩子将使用单个进程而不是并行执行。
description (opens new window) (可选:默认 '')钩子的描述。仅用于元数据目的。
language_version (opens new window) (可选:默认 default)请参阅 覆盖语言版本 (opens new window)。
minimum_pre_commit_version (opens new window) (可选:默认 '0')允许一个人指示最低兼容的预提交版本。
args (opens new window) (可选:默认 [])要传递给挂钩的附加参数列表。
stages (opens new window) (可选:默认(所有阶段))将挂钩限制在 commit、merge-commit、 push、prepare-commit-msg、commit-msg、post-checkout、post-commit、 post-merge、post-rewrite 和/或 manual 阶段。请参阅 限制挂钩在特定阶段运行 (opens new window)。

# 其他内容快链

大部分内容官方文档已经介绍的很好,这里不再重复介绍,把相关的内容快链如下:

  • 如何设置默认启用 (opens new window)
  • 支持的语言 (opens new window)
  • 按类型过滤文件 (opens new window)
  • 使用徽标标记你的存储库 (opens new window)
  • 与 GitHub Action 的集成 (opens new window)

# 一些可用的检查概览

  • https://github.com/Lucas-C/pre-commit-hooks-nodejs

    • htmlhint:html语法检测

    • markdown-toc:自动给Markdown添加TOC

    • dockerfile_lint:检查 dockerfile 的语法

      repos:
      -   repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
          rev: v1.1.2
          hooks:
          -   id: htmlhint
              # optional custom config:
              args: [--config, .htmlhintrc]
          -   id: htmllint
          -   id: markdown-toc
              # optional custom config:
              args: [--indent, "    ", -i]
          -   id: dockerfile_lint
              # optional custom config:
              args: [--json, --verbose, --dockerfile]
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
  • https://github.com/dnephin/pre-commit-golang

    • go-fmt- 运行gofmt,需要 golang

    • go-vet- 运行go vet,需要 golang

    • go-lint- 运行golint,需要https://github.com/golang/lint但未维护且已弃用,有利于golangci-lint (opens new window)

    • go-imports- 运行goimports,需要 golang.org/x/tools/cmd/goimports

    • go-cyclo- 运行gocyclo,需要https://github.com/fzipp/gocyclo

    • validate-toml- 运行tomlv,需要 https://github.com/BurntSushi/toml/tree/master/cmd/tomlv

    • no-go-testing- 检查没有文件正在使用testing.T,如果您希望开发人员使用不同的测试框架

    • golangci-lint- 运行golangci-lint run ./...,需要 golangci-lint (opens new window)

    • go-critic- 运行gocritic check ./...,需要go-critic (opens new window)

    • go-unit-tests- 跑步go test -tags=unit -timeout 30s -short -v

    • go-build-运行go build,需要golang

    • go-mod-tidy-运行go mod tidy -v,需要golang

    • go-mod-vendor-运行go mod vendor,需要golang

      - repo: https://github.com/dnephin/pre-commit-golang
        rev: master
        hooks:
          - id: go-fmt
          - id: go-vet
          - id: go-lint
          - id: go-imports
          - id: go-cyclo
            args: [-over=15]
          - id: validate-toml
          - id: no-go-testing
          - id: golangci-lint
          - id: go-critic
          - id: go-unit-tests
          - id: go-build
          - id: go-mod-tidy
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
  • https://github.com/detailyang/pre-commit-shell

    • shell-lint:包装shellcheck来检查 shell 脚本

      -   repo: git://github.com/detailyang/pre-commit-shell
          rev: v1.0.6
          hooks:
          - id: shell-lint
            args: [--format=json]
      
      1
      2
      3
      4
      5
  • https://github.com/ansible/ansible-lint

    • ansible-lint:运行ansible语法检测
  • https://github.com/fortman/pre-commit-prometheus

    • check-config - 检查普罗米修斯配置文件
    • check-rules - 检查普罗米修斯规则文件
    • test-rules - 单元测试普罗米修斯规则文件
  • https://github.com/syntaqx/git-hooks

    • circleci-config-validate - 测试 CircleCI 配置是否正确。
    • go-fmt - 运行 go fmt 并断言不需要任何更改。
    • go-test - 运行 go test 并断言没有测试失败。
    • go-mod-tidy - 运行 go mod tidy 以确保 go.mod 与项目源匹配。
    • go-generate - 针对项目 go 文件运行 go generate 。
    • forbid-binary - 禁止提交二进制文件
    • shellcheck - Shell 脚本符合 shellcheck
    • shfmt - 使用 shfmt 检查 shell 样式
  • https://github.com/TekWizely/pre-commit-golang

    • my-cmd - 为每个暂存的 .go 文件运行 '$ARGS[0] [$ARGS[1:]] $FILE'
    • my-cmd-mod - 运行 'cd $(mod_root $FILE); $ARGS[0] [$ARGS[1:]] ./...' 对于每个暂存的 .go 文件
    • my-cmd-pkg - 为每个暂存的 .go 文件运行 '$ARGS[0] [$ARGS[1:]] ./$(dirname $FILE)'
    • my-cmd-repo - 在 repo 根文件夹中运行“$ARGS[0] [$ARGS[1:]]”
    • my-cmd-repo-mod - 运行'cd $(mod_root); $ARGS[0] [$ARGS[1:]] /...' 用于 repo 中的每个模块
    • my-cmd-repo-pkg - 在 repo 根文件夹中运行“$ARGS[0] [$ARGS[1:]] ./...”
    • go-build-mod - 运行 'cd $(mod_root $FILE); go build -o /dev/null [$ARGS] ./...' 对于每个暂存的 .go 文件
    • go-build-pkg - 为每个暂存的 .go 文件运行“go build -o /dev/null [$ARGS] ./$(dirname $FILE)”
    • go-build-repo-mod - 运行'cd $(mod_root); go build -o /dev/null [$ARGS] ./...' 为 repo 中的每个模块
    • go-build-repo-pkg - 在 repo 根文件夹中运行“go build -o /dev/null [$ARGS] ./...”
    • go-critic - 为每个暂存的 .go 文件运行“gocritic check [$ARGS] $FILE”
    • go-fmt - 为每个暂存的 .go 文件运行 'gofmt -l -d [$ARGS] $FILE'
    • go-fmt-repo - 运行“gofmt -l -d [$ARGS]”。在回购根文件夹中
    • go-fumpt - 为每个暂存的 .go 文件运行 'fumpt -l -d [$ARGS] $FILE'
    • go-fumpt-repo - 运行“fumpt -l -d [$ARGS]”。在回购根文件夹中
    • go-imports - 为每个暂存的 .go 文件运行“goimports -l -d [$ARGS] $FILE”
    • go-imports-repo - 运行“goimports -l -d [$ARGS]”。在回购根文件夹中
    • go-lint - 为每个暂存的 .go 文件运行“golint -set_exit_status [$ARGS] $FILE”
    • go-mod-tidy - 运行 'cd $(mod_root $FILE); go mod tidy [$ARGS]' 为每个暂存的 .go 文件
    • go-mod-tidy-repo - 运行'cd $(mod_root); go mod tidy [$ARGS]' 为 repo 中的每个模块
    • go-returns - 为每个暂存的 .go 文件运行 'goreturns -l -d [$ARGS] $FILE'
    • go-returns-repo - 运行“goreturns -l -d [$ARGS]”。在回购根文件夹中
    • go-revive - 为每个暂存的 .go 文件运行'revive [$ARGS] $FILE'
    • go-revive-mod - 运行 'cd $(mod_root $FILE); 为每个暂存的 .go 文件恢复 [$ARGS] ./...'
    • go-revive-repo-mod - 运行'cd $(mod_root); 为 repo 中的每个模块恢复 [$ARGS] ./...'
    • go-sec-mod - 运行 'cd $(mod_root $FILE); gosec [$ARGS] ./...' 用于每个暂存的 .go 文件
    • go-sec-pkg - 为每个暂存的 .go 文件运行 'gosec [$ARGS] ./$(dirname $FILE)'
    • go-sec-repo-mod - 运行'cd $(mod_root); gosec [$ARGS] ./...' 用于 repo 中的每个模块
    • go-sec-repo-pkg - 在 repo 根文件夹中运行“gosec [$ARGS] ./...”
    • go-staticcheck-mod - 运行 'cd $(mod_root $FILE); staticcheck [$ARGS] ./...' 用于每个暂存的 .go 文件
    • go-staticcheck-pkg - 为每个暂存的 .go 文件运行 'staticcheck [$ARGS] ./$(dirname $FILE)'
    • go-staticcheck-repo-mod - 运行'cd $(mod_root); staticcheck [$ARGS] ./...' 用于 repo 中的每个模块
    • go-staticcheck-repo-pkg - 在 repo 根文件夹中运行“staticcheck [$ARGS] ./...”
    • go-structslop-mod - 运行 'cd $(mod_root $FILE); structslop [$ARGS] ./...' 用于每个暂存的 .go 文件
    • go-structslop-pkg - 为每个暂存的 .go 文件运行“structslop [$ARGS] ./$(dirname $FILE)”
    • go-structslop-repo-mod - 运行'cd $(mod_root); structslop [$ARGS] ./...' 用于 repo 中的每个模块
    • go-structslop-repo-pkg - 在 repo 根文件夹中运行“structslop [$ARGS] ./...”
    • go-test-mod - 运行 'cd $(mod_root $FILE); go test [$ARGS] ./...' 为每个暂存的 .go 文件
    • go-test-pkg - 为每个暂存的 .go 文件运行“go test [$ARGS] ./$(dirname $FILE)”
    • go-test-repo-mod - 运行'cd $(mod_root); 去测试 [$ARGS] ./...' 为 repo 中的每个模块
    • go-test-repo-pkg - 在 repo 根文件夹中运行“go test [$ARGS] ./...”
    • go-vet-mod - 运行 'cd $(mod_root $FILE); go vet [$ARGS] ./...' 为每个上演的 .go 文件
    • go-vet-pkg - 为每个暂存的 .go 文件运行“go vet [$ARGS] ./$(dirname $FILE)”
    • go-vet-repo-mod - 运行'cd $(mod_root); go vet [$ARGS] ./...' 对于 repo 中的每个模块
    • go-vet-repo-pkg - 在 repo 根文件夹中运行“go vet [$ARGS] ./...”
    • go-vet - 为每个暂存的 .go 文件运行“go vet [$ARGS] $FILE”
    • golangci-lint-mod - 运行 'cd $(mod_root $FILE); golangci-lint 为每个暂存的 .go 文件运行 [$ARGS] ./...'
    • golangci-lint-pkg - 为每个暂存的 .go 文件运行“golangci-lint run [$ARGS] ./$(dirname $FILE)”
    • golangci-lint-repo-mod - 运行'cd $(mod_root); golangci-lint 为 repo 中的每个模块运行 [$ARGS] ./...'
    • golangci-lint-repo-pkg - 在 repo 根文件夹中运行“golangci-lint run [$ARGS] ./...”
    • golangci-lint - 为每个暂存的 .go 文件运行“golangci-lint run [$ARGS] $FILE”
  • https://github.com/PeterMosmans/jenkinslint

    • jenkinslint - 使用 Jenkins 服务器验证 Jenkinsfiles
  • https://github.com/mrtazz/checkmake

    • checkmake - Makefile linter/分析器
微信 支付宝
上次更新: 2023/02/28, 10:58:33

← vector实践-性能吊打logstash 记一次关于tomcat的踩坑的经历→

最近更新
01
go-cache包的使用简析
03-19
02
学习周刊-总第98期-2023年第11周
03-17
03
使用retry-go给项目添加重试机制
03-15
更多文章>
Theme by Vdoing | Copyright © 2017-2023 | 点击查看十年之约 | 浙ICP备18057030号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式