Unix 哲学

提供”锋利“的小工具、其中每一把都意在把一件事情做好。

–《程序员修炼之道 - 从小工到专家》

写在前面

如果你使用Git,那你一定懂得纯文本的魅力并喜爱上shell这样的脚本语言。

在很多时候,我更喜欢能够通过脚本语言进行配置的工具,而不是直接安装到编辑器的工具。一是因为脚本可以放在项目中与更多的人共享,以保持规范一直;二是脚本自动触发的操作无需要记更多的快捷键或者点击一点鼠标;再来则是脚本语言可以做更多灵活的操作,而不受软件开发者的约束。这大概也是我一直喜欢用Git指令,而不是编译器提供给我的Git工具。

本文将继续讲解git hooks,介绍一款能够帮助我们更好地管理和利用git hooks的工具。期望找到的工具有如下的功能:

  • 只需要提供配置文件,自动从中央hooks仓库获取脚本

    • 如果有多个项目,就不需要再每个项目都拷贝一份hooks了
  • 可以定义本地脚本仓库,允许开发人员自定义脚本,且无需修改配置文件
    • 开发人员会有一些脚本以完成的自定义操作
    • 无需修改配置文件是指可以直接指向一个目录,并执行里面的所有hooks或者指定一个无需上传到git的本地配置文件
  • 每个阶段允许定义多个脚本
    • 多个脚本可以使得功能划分而无需整合到一个臃肿的文件中
  • 脚本支持多种语言

pre-commit 概要

不要被这个pre-commit的名字迷惑,这个工具不仅仅可以在pre-commit阶段执行,其实可以在git-hooks的任意阶段,设置自定义阶段执行,见的stages配置的讲解。(这个名字大概是因为他们开始只做了pre-commit阶段的,后续才拓展了其他的阶段)。

安装pre-commit

在系统中安装pre-commit

brew install pre-commit
# 或者
pip install pre-commit# 查看版本
pre-commit --version
# pre-commit 2.12.1 <- 这是我当前使用的版本

在项目中安装pre-commit

cd <git-repo>
pre-commit install
# 卸载
pre-commit uninstall

按照操作将会在项目的.git/hooks下生成一个pre-commit文件(覆盖原pre-commit文件),该hook会根据项目根目录下的.pre-commit-config.yaml执行任务。如果vim .git/hooks/pre-commit可以看到代码的实现,基本逻辑是利用pre-commit文件去拓展更多的pre-commit,这个和我上一篇文章的逻辑是类似的。

安装/卸载其他阶段的hook。

pre-commit install
pre-commit uninstall
-t {pre-commit,pre-merge-commit,pre-push,prepare-commit-msg,commit-msg,post-checkout,post-commit,post-merge}
--hook-type {pre-commit,pre-merge-commit,pre-push,prepare-commit-msg,commit-msg,post-checkout,post-commit,post-merge}# 如 pre-commit install --hook-type prepare-commit-msg

常用指令

# 手动对所有的文件执行hooks,新增hook的时候可以执行,使得代码均符合规范。直接执行该指令则无需等到pre-commit阶段再触发hooks
pre-commit run --all-files
# 执行特定hooks
pre-commit run <hook_id>
# 将所有的hook更新到最新的版本/tag
pre-commit autoupdate
# 指定更新repo
pre-commit autoupdate --repo https://github.com/DoneSpeak/gromithooks

更多指令及指令参数请直接访问pre-commit官方网站。

添加第三方hooks

cd <git-repo>
pre-commit install
touch .pre-commit-config.yaml

如下为一个基本的配置样例。

.pre-commit-config.yaml

# 该config文件为该项目的pre-commit的配置文件,用于指定该项目可以执行的git hooks# 这是pre-commit的全局配置之一
fail_fast: falserepos:
# hook所在的仓库
- repo: https://github.com/pre-commit/pre-commit-hooks# 仓库的版本,可以直接用tag或者分支,但分支是容易发生变化的# 如果使用分支,则会在第一次安装之后不自动更新# 通过 `pre-commit autoupdate`指令可以将tag更新到默认分支的最新tagrev: v4.0.1# 仓库中的hook idhooks:# 定义的hook脚本,在repo的.pre-commit-hooks.yaml中定义- id: check-added-large-files# 移除尾部空格符- id: trailing-whitespace# 传入参数,不处理makedownargs: [--markdown-linebreak-ext=md]# 检查是否含有合并冲突符号- id: check-merge-conflict
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooksrev: v2.0.0hooks:- id: pretty-format-yaml# https://github.com/macisamuele/language-formatters-pre-commit-hooks/blob/v2.0.0/language_formatters_pre_commit_hooks/pretty_format_yaml.py# hook脚本需要的参数,可以在该hook脚本文件中看到args: [--autofix, --indent, '2']

run之后,pre-commit会下载指定仓库代码,并安装配置所需要的运行环境。配置完成之后可以通过pre-commit run --all-files运行一下添加的hooks。下表为.pre-commit-hooks.yaml可选配置项。

key word description
id the id of the hook - used in pre-commit-config.yaml.
name the name of the hook - shown during hook execution.
entry the entry point - the executable to run. entry can also contain arguments that will not be overridden such as entry: autopep8 -i.
language the language of the hook - tells pre-commit how to install the hook.
files (optional: default '') the pattern of files to run on.
exclude (optional: default ^$) exclude files that were matched by files
types (optional: default [file]) list of file types to run on (AND). See Filtering files with types.
types_or (optional: default []) list of file types to run on (OR). See Filtering files with types. new in 2.9.0.
exclude_types (optional: default []) the pattern of files to exclude.
always_run (optional: default false) if true this hook will run even if there are no matching files.
verbose (optional) if true, forces the output of the hook to be printed even when the hook passes. new in 1.6.0.
pass_filenames (optional: default true) if false no filenames will be passed to the hook.
require_serial (optional: default false) if true this hook will execute using a single process instead of in parallel. new in 1.13.0.
description (optional: default '') description of the hook. used for metadata purposes only.
language_version (optional: default default) see Overriding language version.
minimum_pre_commit_version (optional: default '0') allows one to indicate a minimum compatible pre-commit version.
args (optional: default []) list of additional parameters to pass to the hook.
stages (optional: default (all stages)) confines the hook to the commit, merge-commit, push, prepare-commit-msg, commit-msg, post-checkout, post-commit, post-merge, or manual stage. See Confining hooks to run at certain stages.

开发hooks仓库

上面已经讲解了在项目中使用第三方的hooks,但有部分功能是定制化需要的,无法从第三方获得。这时候就需要我们自己开发自己的hooks仓库。

As long as your git repo is an installable package (gem, npm, pypi, etc.) or exposes an executable, it can be used with pre-commit.

只要你的git仓库是可安装的或者暴露为可执行的,它就可以被pre-commit使用。这里演示的项目为可打包的Python项目。也是第一次写这样的项目,花了不少力气。如果是不怎么接触的Python的,可以跟着文末的Packaging Python Projects ,也可以模仿第三方hooks仓库来写。

如下为项目的目录基本结构(完整项目见文末的源码路径):

├── README.md
├── pre_commit_hooks
│   ├── __init__.py
│   ├── cm_tapd_autoconnect.py  # 实际执行的脚本
│   ├── pcm_issue_ref_prefix.py # 实际执行的脚本
│   └── pcm_tapd_ref_prefix.py  # 实际执行的脚本
├── .pre-commit-hooks.yaml # 配置 pre-commit hooks entry
├── pyproject.toml
├── setup.cfg # 项目信息,配置hook entry point执行的脚本
└── setup.py

一个含有pre-commit插件的git仓库,必须含有一个.pre-commit-hooks.yaml文件,告知pre-commit插件信息。.pre-commit-hooks.yaml的配置可选项和.pre-commit-config.yaml是一样的。

.pre-commit-hooks.yaml

# 该项目为一个pre-commit hooks仓库项目,对外提供hooks- id: pcm-issue-ref-prefixname: Add issue reference prefix for commit msgdescription: Add issue reference prefix for commit msg to link commit and issueentry: pcm-issue-ref-prefix# 实现hook所使用的语言language: pythonstages: [prepare-commit-msg]
- id: pcm-tapd-ref-prefixname: Add tapd reference prefix for commit msgdescription: Add tapd reference prefix for commit msgentry: pcm-tapd-ref-prefix# 实现hook所使用的语言language: pythonstages: [prepare-commit-msg]# 强制输出中间日志,这里不做配置,由用户在 .pre-commit-config.yaml 中指定# verbose: true
- id: cm-tapd-autoconnectname: Add tapd reference for commit msgdescription: Add tapd reference for commit msg to connect tapd and commitentry: cm-tapd-autoconnect# 实现hook所使用的语言language: pythonstages: [commit-msg]

其中中的entry为执行的指令,对应在setup.cfg中的[options.entry_points]配置的列表。

setup.cfg

...
[options.entry_points]
console_scripts =cm-tapd-autoconnect = pre_commit_hooks.cm_tapd_autoconnect:mainpcm-tapd-ref-prefix = pre_commit_hooks.pcm_tapd_ref_prefix:mainpcm-issue-ref-prefix = pre_commit_hooks.pcm_issue_ref_prefix:main

以下是pcm-issue-ref-prefix对应的python脚本,该脚本用于根据branch name为commit message添加issue前缀的一个prepare-commit-msg hook。

pre_commit_hooks/pcm_issue_ref_prefix.py

# 根据分支名,自动添加commit message前缀以关联issue和commit。
#
# 分支名  | commit 格式
# --- | ---
# issue-1234  | #1234, message
# issue-1234-fix-bug  | #1234, messageimport sys, os, re
from subprocess import check_output
from typing import Optional
from typing import Sequencedef main(argv: Optional[Sequence[str]] = None) -> int:commit_msg_filepath = sys.argv[1]# 检测我们所在的分支branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip().decode('utf-8')# 匹配如:issue-123, issue-1234-fixresult = re.match('^issue-(\d+)((-.*)+)?$', branch)if not result:# 分支名不符合warning = "WARN: Unable to add issue prefix since the format of the branch name dismatch."warning += "\nThe branch should look like issue-<number> or issue-<number>-<other>, for example: issue-100012 or issue-10012-fix-bug)"print(warning)returnissue_number = result.group(1)with open(commit_msg_filepath, 'r+') as f:content = f.read()if re.search('^#[0-9]+(.*)', content):# print('There is already issue prefix in commit message.')returnissue_prefix = '#' + issue_numberf.seek(0, 0)f.write("%s, %s" % (issue_prefix, content))# print('Add issue prefix %s to commit message.' % issue_prefix)if __name__ == '__main__':exit(main())

这里用commit_msg_filepath = sys.argv[1]获取commit_msg文件的路径,当然,你也可以用argparse获取到。部分阶段的参数列表可以在pre-commit官网的install命令讲解中看到。

import argparse
from typing import Optional
from typing import Sequencedef main(argv: Optional[Sequence[str]] = None) -> int:parser = argparse.ArgumentParser()parser.add_argument('filename', nargs='*', help='Filenames to check.')args = parser.parse_args(argv)# .git/COMMIT_EDITMSGprint("commit_msg file is " + args.filename[0])if __name__ == '__main__':exit(main())

只要在需要配置的项目中按照如下配置.pre-commit-config.yaml即可使用。

repos:
- repo: https://github.com/DoneSpeak/gromithooksrev: v1.0.0hooks:- id: pcm-issue-ref-prefixverbose: true# 指定hook执行的阶段stages: [prepare-commit-msg]

本地hooks

pre-commit 也提供了local的hook,允许在entry中配置执行指令或指向本地一个可执行的脚本文件,使用起来和husky类似。

  • 脚本与代码仓库紧密耦合,并且与代码仓库一起分发。
  • Hooks需要的状态只存在于代码仓库的build artifact中(比如应用程序的pylint的virtualenv)。
  • linter的官方代码仓库没有提供pre-commit metadata.

local hooks可以使用支持additional_dependencies 的语言或者 docker_image / fail / pygrep / script / system

# 定义repo为local,表示该repo为本地仓库
- repo: localhooks:- id: pylintname: pylintentry: pylintlanguage: systemtypes: [python]- id: changelogs-rstname: changelogs must be rstentry: changelog filenames must end in .rstlanguage: fail # fail 是一种用于通过文件名禁止文件的轻语言files: 'changelog/.*(?<!\.rst)$'

自定义本地脚本

在文章开篇也有说到,希望可以提供一个方法让开发人员创建自己的hooks,但提交到公共代码库中。我看完了官方的文档,没有找到相关的功能点。但通过上面的local repo功能我们可以开发符合该需求的功能。

因为local repo允许entry执行本地文件,所以只要为每个阶段定义一个可执行的文件即可。下面的配置中,在项目更目录下创建了一个.git_hooks目录,用来存放开发人员自己的脚本。(可以注意到这里并没有定义出全部的stage,仅仅定义了pre-commit install -t支持的stage)。

- repo: localhooks:- id: commit-msgname: commit-msg (local)entry: .git_hooks/commit-msglanguage: scriptstages: [commit-msg]# verbose: true- id: post-checkoutname:  post-checkout (local)entry: .git_hooks/post-checkoutlanguage: scriptstages: [post-checkout]# verbose: true- id: post-commitname: post-commit (local)entry: .git_hooks/post-commitlanguage: scriptstages: [post-commit]# verbose: true- id: post-mergename: post-merge (local)entry: .git_hooks/post-mergelanguage: scriptstages: [post-merge]# verbose: true- id: pre-commitname: pre-commit (local)entry: .git_hooks/pre-commitlanguage: scriptstages: [commit]# verbose: true- id: pre-merge-commitname: pre-merge-commit (local)entry: .git_hooks/pre-merge-commitlanguage: scriptstages: [merge-commit]# verbose: true- id: pre-pushname: pre-push (local)entry: .git_hooks/pre-pushlanguage: scriptstages: [push]# verbose: true- id: prepare-commit-msgname: prepare-commit-msg (local)entry: .git_hooks/prepare-commit-msglanguage: scriptstages: [prepare-commit-msg]# verbose: true

遵循能够自动化的就自动化的原则。这里提供了自动创建以上所有阶段文件的脚本(如果entry指定的脚本文件不存在会Fail)。install-git-hooks.sh会安装pre-commit和pre-commit支持的stage,如果指定CUSTOMIZED=1则初始化.git_hooks中的hooks,并添加customized local hooks到.pre-commit-config.yaml

install-git-hooks.sh

#!/bin/bash:<<'COMMENT'
chmod +x install-git-hooks.sh
./install-git-hooks.sh
# intall with initializing customized hooks
CUSTOMIZED=1 ./install-git-hooks.sh
COMMENTSTAGES="pre-commit pre-merge-commit pre-push prepare-commit-msg commit-msg post-checkout post-commit post-merge"installPreCommit() {HAS_PRE_COMMIT=$(which pre-commit)if [ -n "$HAS_PRE_COMMIT" ]; thenreturnfiHAS_PIP=$(which pip)if [ -z "$HAS_PIP" ]; thenecho "ERROR:pip is required, please install it mantually."exit 1fipip install pre-commit
}touchCustomizedGitHook() {mkdir .git_hooksfor stage in $STAGESdoSTAGE_HOOK=".git_hooks/$stage"if [ -f "$STAGE_HOOK" ]; thenecho "WARN:Fail to touch $STAGE_HOOK because it exists."continuefiecho -e "#!/bin/bash\n\n# general git hooks is available." > "$STAGE_HOOK"chmod +x "$STAGE_HOOK"done
}preCommitInstall() {for stage in $STAGESdoSTAGE_HOOK=".git/hooks/$stage"if [ -f "$STAGE_HOOK" ]; thenecho "WARN:Fail to install $STAGE_HOOK because it exists."continuefipre-commit install -t "$stage"done
}touchPreCommitConfigYaml() {PRE_COMMIT_CONFIG=".pre-commit-config.yaml"if [ -f "$PRE_COMMIT_CONFIG" ]; thenecho "WARN: abort to init .pre-commit-config.yaml for it's existence."return 1fitouch $PRE_COMMIT_CONFIGecho "# 在Git项目中使用pre-commit统一管理hooks" >> $PRE_COMMIT_CONFIGecho "# https://donespeak.gitlab.io/posts/210525-using-pre-commit-for-git-hooks/" >> $PRE_COMMIT_CONFIG
}initPreCommitConfigYaml() {touchPreCommitConfigYamlif [ "$?" == "1" ]; thenreturn 1fiecho "" >> $PRE_COMMIT_CONFIGecho "repos:" >> $PRE_COMMIT_CONFIGecho "  - repo: local" >>  $PRE_COMMIT_CONFIGecho "    hooks:" >> $PRE_COMMIT_CONFIGfor stage in $STAGESdoecho "      - id: $stage" >> $PRE_COMMIT_CONFIGecho "        name: $stage (local)" >> $PRE_COMMIT_CONFIGecho "        entry: .git_hooks/$stage" >> $PRE_COMMIT_CONFIGecho "        language: script" >> $PRE_COMMIT_CONFIGif [[ $stage == pre-* ]]; thenstage=${stage#pre-}fiecho "        stages: [$stage]" >> $PRE_COMMIT_CONFIGecho "        # verbose: true" >> $PRE_COMMIT_CONFIGdone
}ignoreCustomizedGitHook() {CUSTOMIZED_GITHOOK_DIR=".git_hooks/"GITIGNORE_FILE=".gitignore"if [ -f "$GITIGNORE_FILE" ]; thenif [ "$(grep -c "$CUSTOMIZED_GITHOOK_DIR" $GITIGNORE_FILE)" -ne '0' ]; then# 判断文件中已经有配置returnfifiecho -e "\n# 忽略.git_hooks中文件,使得其中的脚本不提交到代码仓库\n$CUSTOMIZED_GITHOOK_DIR\n!.git_hooks/.gitkeeper" >> $GITIGNORE_FILE
}installPreCommit
if [ "$CUSTOMIZED" == "1" ]; thentouchCustomizedGitHookinitPreCommitConfigYaml
elsetouchPreCommitConfigYaml
fi
preCommitInstall
ignoreCustomizedGitHook

添加Makefile,提供make install-git-hook安装指令。该指令会自动下载git仓库中的install-git-hooks.sh文件,并执行。此外,如果执行CUSTOMIZED=1 make install-git-hook则会初始化customized的hooks。

Makefile

install-git-hooks: install-git-hooks.shchmod +x ./$< && ./$<install-git-hooks.sh:# 如果遇到 Failed to connect to raw.githubusercontent.com port 443: Connection refused# 为DNS污染问题,可在https://www.ipaddress.com/查询域名,然后写入hosts文件中# 见:https://github.com/hawtim/blog/issues/10wget https://raw.githubusercontent.com/DoneSpeak/gromithooks/v1.0.1/install-git-hooks.sh

在.git_hooks中的hook文件可以按照原本在.git/hooks中的脚本写,也可以按照pre-commit的hook写。

prepare-commit-msg

#!/usr/bin/env pythonimport argparse
from typing import Optional
from typing import Sequencedef main(argv: Optional[Sequence[str]] = None) -> int:parser = argparse.ArgumentParser()parser.add_argument('filename', nargs='*', help='Filenames to check.')args = parser.parse_args(argv)# .git/COMMIT_EDITMSGprint("commit_msg file is " + args.filename[0])if __name__ == '__main__':exit(main())

prepare-commit-msg

#!/bin/bashecho "commit_msg file is $1"

到这里pre-commit的主要功能就讲解完成了,如果需要了解更多的功能(如定义git template),可以看官网文档。

相关文章

推荐

  • 本文章源码 Donespeak/Gromithooks
  • 定义全局Git Hooks和自定义Git Hooks
  • 通过Git Hook关联Tapd和Commit

参考

  • pre-commit | A framework for managing and maintaining multi-language pre-commit hooks. @pre-commit.com
  • pre-commit | Supported hooks @pre-commit.com
  • 一个值得参考的.pre-commit-config.yaml @github
  • Git钩子:自定义你的工作流 用python写git hooks
  • Packaging Python Projects @python.org 会给出一个从创建到发布的流程介绍

在Git项目中使用pre-commit统一管理hooks相关推荐

  1. git项目中的子git项目_使用子模块和子树管理Git项目

    git项目中的子git项目 如果您从事开源开发,则可能与Git一起管理源代码. 您可能遇到过具有大量依赖项和/或子项目的项目. 您如何管理它们? 对于开源组织,为社区和产品实现单源文档和依赖性管理可能 ...

  2. Atitit.css 规范 bem  项目中 CSS 的组织和管理

    Atitit.css 规范 bem  项目中 CSS 的组织和管理 1. 什么是BEM?1 1.1. 块(Block)2 1.2. 元素(Element)2 1.3. BEM树(和DOM树类似).3 ...

  3. 【04】进阶:Git系统中的分支操作与管理

    总第95篇 接上篇,本篇文章将详细梳理Git系统分支相关的操作,包括分支的新建与合并.分支的管理及变基等.Git的分支模型非常轻量型,你可以非常方便地创建分支以及在不同的分支间切换.可以说,Git的分 ...

  4. android studio管理依赖,Android Studio 中的 Gradle 依赖统一管理

    在我们的实际项目开发中,通常在一个 Project 项目中会存在多个 Module 的情况,在这些 Module 中会存在一些相同的版本依赖配置,针对进行版本升级的时候需要逐个修改,显得特别麻烦,所以 ...

  5. 在vue项目中引入vuex(全局状态管理器)

    目录 Vuex是什么? State Getter Mutation Action Module 项目结构 Vuex是什么? Vuex是一个专为Vue.js应用程序开发的状态管理模式.它采用集中式存储管 ...

  6. 数据可视化中的多数据源统一管理

    数据可视化软件,为我们的数据展示绝佳效果.今天在使用的时候发现一个组件,可以管控多个数据,精简数据源,它就是"统一数据集",一起来看下. 我们首先在组件中,选择其他组件,找到统一数 ...

  7. Spring Boot中使用LDAP来统一管理用户信息

    很多时候,我们在构建系统的时候都会自己创建用户管理体系,这对于开发人员来说并不是什么难事,但是当我们需要维护多个不同系统并且相同用户跨系统使用的情况下,如果每个系统维护自己的用户信息,那么此时用户信息 ...

  8. java ldap userpassword 解密_Spring Boot中使用LDAP来统一管理用户信息

    LDAP简介 LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)是实现提供被称为目录服务的信息服务.目录服务是一种特殊的数据库系统,其专门针对读 ...

  9. 项目中的外包团队如何管理

    在复杂的项目工作中,对外采购产品.服务变得越来越普遍. 这种基于合同的跨组织合作方式,确实给项目的管理工作带来了更大的挑战.项目经理在管理好自己团队的同时,应该怎样做好针对外包团队的监督与控制工作呢? ...

  10. Java设计模式--在项目中的应用

    原文网址:Java设计模式--在项目中的应用_IT利刃出鞘的博客-CSDN博客 简介 本文介绍我在项目中是如何运用设计模式的. 我实际用到的设计模式有:单例模式.责任链模式.观察者模式.适配器模式.策 ...

最新文章

  1. ORB_SLAM2中Tracking线程的三种追踪方式
  2. Mozilla新特性只支持https网站
  3. C#将DataTable转换成list的方法
  4. OpenCV Java开发简介
  5. c语言二叉树的生成,C语言实现二叉树的创建以及遍历(递归)
  6. java怎么调kettle_通过Java调取Kettle的结果集
  7. mfc globalalloc能分配多大空间_办公家具定制充分利用办公室空间,提高员工之间工作效率...
  8. quartz 2.0持久化到mysql_SpringBoot2.0整合Quartz定时任务(持久化到数据库,更为简单的方式)...
  9. IDEA2019版下载和安装
  10. 寒冬,送点社区温暖。
  11. 傻瓜式PHP模版,poscms傻瓜式开发模块
  12. 耳朵大寿命长 由耳可辨疾病
  13. 花样16流水灯c语言程序,8个花样流水灯c程序
  14. Android手机电池不耐用,手机电池用久了不耐用?教你几招让它“起死回生”!
  15. 常见Andriod游戏破解搜索关键字
  16. 互联网数据中心——IDC(Internet Data Center)
  17. time秒和年月日时分秒的转化
  18. 无损播放器Linux系统,Ubuntu Linux下支持无损CUE的音乐播放器——Qmmp
  19. 2021-08-15 minikube在阿里云centos系统上的安装实践
  20. Jenkins自动构建(CI/DI)项目(一)

热门文章

  1. 罪恶都市联机器无法显示服务器,《GTA:罪恶都市》多人联机!你的童年又回来了,梦想成真了!...
  2. ue编辑器漏洞_UEditor编辑器任意文件上传漏洞分析
  3. tps协议和onvif协议_ONVIF协议解读
  4. STM32F1下载程序方法
  5. 阿里ACP考试题(只供参考)
  6. python做购物系统的实训报告_网上购物系统实训总结
  7. Wireshark安装提示缺少KB3118401和KB2999226文件
  8. python怎么输出键值对_python 获取字典键值对的实现
  9. 压力测试 - Apache JMeter使用教程
  10. 路径规划算法:动态规划