3 个稳定版本

1.1.0 2022年9月10日
1.0.1 2022年9月5日
1.0.0 2022年9月4日

开发工具 中排名 #2074

MIT 许可证

120KB
2.5K SLoC

monorail

将任何 git 仓库转换为单轨仓库。

Build Status Cargo

monorail 将任何 git 仓库转换为基于主干的开发单轨仓库。它使用一个描述各种目录路径及其之间关系的文件,从上次标签以来提取 git 的更改,并推导出已更改的内容。然后可以将这些更改提供给其他程序(例如 monorail-bash),这些程序可以对这些更改采取行动。虽然 monorail 目前仅支持 git 作为 VCS 后端,但可以添加对其他后端的支持。

monorail 简而言之

  1. 一个 Monorail.toml 文件,描述您的仓库布局
  2. 命令 monorail inspect change,它读取 Monorail.toml,分析 refs 之间的 git 状态(通常是在最近的 git 注释标签和 HEAD 之间),并返回已更改的内容
  3. 程序 monorail-bash,它直接执行用户定义的 bash 脚本,或者根据 monorail inspect change 的输出执行
  4. 命令 monorail release,它创建注释标签(即 monorail 变更检测的“检查点”)

下面是一个教程,介绍了如何使用 monorailmonorail-bash

安装

请确保以下内容已安装并可在系统路径上使用

  • Rust
  • bash
  • jq,由 monorail-bash 扩展用于默认的 --output-formatjson

可以通过克隆仓库并从仓库根目录执行以下命令从源安装 monorail 和所有扩展

./install.sh 

默认情况下,它将把这些放在/usr/local/bin,如果希望放在其他位置,请使用以下命令:./install.sh <destination>

请注意,虽然monorail可以通过cargo install从crates.io安装,但cargo不支持安装额外的资源,例如monorail-bash的脚本入口点。install.sh脚本处理二进制和扩展安装。

命令

使用monorail help

配置

在你想使用monorail和扩展的存储库根目录中创建一个Monorail.toml配置文件,参考Monorail.reference.toml文件以获取注释示例。

词汇表

  • 项目:一个作为单个单元开发/部署/测试的路径
  • 组:包含一组项目和相关配置的路径
  • 依赖:项目可以声明为依赖项的路径,该路径上的任何更改都将视为对依赖于该路径的项目进行更改
  • 链接:可以声明为组中所有项目的自动依赖项的路径,该路径上的任何更改都将视为所有项目的更改
  • 目标:一个可以检测更改的路径的总称,可以对其运行命令。组、项目、依赖或链接之一。
  • 扩展:运行用户定义的以受支持语言编写的代码;例如bash
  • 命令:由目标定义的函数,可以通过执行器依赖的方式调用

教程

注意:本教程假定您在一个类UNIX环境中。

在本教程中,您将学习

  • 如何声明组、项目、依赖和链接
  • 如何检查更改
  • 如何定义命令
  • 如何执行命令
  • 如何发布

首先,创建一个新的git仓库,并创建另一个作为远程仓库

注意:这假设有一个init.defaultBranchmaster或空字符串,这是git的默认值。如果您的不同,请将git命令中的git push命令中的master更改为该值。

git init monorail-tutorial
git init monorail-tutorial-remote
REMOTE_TRUNK=$(git -C monorail-tutorial-remote branch --show-current)
git -C monorail-tutorial-remote checkout -b x
pushd monorail-tutorial
git remote add origin ../monorail-tutorial-remote
git commit --allow-empty -m "HEAD"
git push --set-upstream origin $REMOTE_TRUNK
popd

注意:提交是为了创建有效的HEAD引用,而在远程分支上的分支检出是为了避免在教程中推送时从git中产生的receive.denyCurrentBranch错误。

要开始,请使用以下shell命令生成以下目录结构

cd monorail-tutorial
mkdir -p group1/project1
touch Monorail.toml

... 生成以下目录结构

├── Monorail.toml
└── group1
    └── project1

注意:本教程的其余部分将使用heredoc字符串更新Monorail.toml文件,以方便起见。

执行以下命令,在Monorail.toml中指定新组、项目和随后在教程中使用的extension

cat <<EOF > Monorail.toml
[vcs]
use = "git"

[vcs.git]
trunk = "$(git branch --show-current)"

[extension] 
use = "bash"

[[group]]
path = "group1"

  [[group.project]]
  	path = "project1"

EOF

总结

您的monorail配置文件(默认:Monorail.toml)以monorail概念术语描述您现有的存储库布局。一个project是一个作为单元开发/部署/测试的路径,例如后端服务、Web应用程序等。一个group是一组相关项目,并定义了项目之间可以共享的内容(关于共享的更多内容将在后面介绍)。

最后,许多单轨系统的功能是基于路径的。我们对group.path(相对于仓库根目录)和project.path(相对于指定的group.path)的定义声明了这些对象在我们仓库中的位置。

检查更改

monorail会检测自上次发布以来的更改;参见:发布。对于git,这意味着自上次由monorail release创建的标注标签以来的未提交、已提交和已推送的文件。

显示无更改的检查

首先查看inspect change的输出

monorail inspect change | jq .
{
  "group": {
    "group1": {
      "change": {
        "file": [],
        "project": [],
        "link": [],
        "depend": []
      }
    }
  }
}

如预期,没有更改,并且monorail能够成功查询git并使用Monorail.toml配置。随着教程的进行,将解释fileprojectlinkdepend字段的含义。

显示更改的检查

为了展示在monorail输出中实际更改的样子,在project1中创建一个新文件

touch group1/project1/foo.txt

再次查看更改

monorail inspect change | jq .
{
  "group": {
    "group1": {
      "change": {
        "file": [
          {
            "name": "group1/project1/foo.txt",
            "project": "group1/project1",
            "action": "use",
            "reason": "project_match"
          }
        ],
        "project": [
          "group1/project1"
        ],
        "link": [],
        "depend": []
      }
    }
  }
}

monorail已确定新添加的文件代表了一次有意义的更改,这是基于我们在Monorail.toml中的配置。

change.file数组包含一个包含检测到的更改元数据的对象的列表。它包含文件的name(相对于仓库根目录的路径),文件所属的project》,monorail在更改检测过程中采取的action(例如,它被use了),以及采取action的原因(例如,它与声明的项目匹配)。

change.project数组包含一个包含检测到更改的项目相对于仓库根目录的路径列表。该列表在所有change.file条目中去重;项目最多在此列表中显示一次。

在提交或推送后显示更改的检查

这种对已更改内容的理解在提交和推送之间持续存在。提交您的更改

git add * && git commit -am "x"

然后,再次查看更改

monorail inspect change | jq .
{
  "group": {
    "group1": {
      "change": {
        "file": [
          {
            "name": "group1/project1/foo.txt",
            "project": "group1/project1",
            "action": "use",
            "reason": "project_match"
          }
        ],
        "project": [
          "group1/project1"
        ],
        "link": [],
        "depend": []
      }
    }
  }
}

monorail仍然知道对项目group1/project1的更改。

推送,并再次查看更改

git push && monorail inspect change | jq .

输出保持不变。

monorail允许项目依赖于声明路径之外的路径。这允许引用路径包含实用代码、序列化文件(例如protobuf定义)、配置等。当这些路径有更改时,它将触发依赖于它们的项目的更改。

依赖关系

首先创建一个用作依赖项的目录,以及一个用于存放新项目project2的目录

mkdir -p group1/common/library1
mkdir group1/project2

执行以下命令以调整Monorail.toml中的[[group]]部分,将library1添加为可依赖的路径,指定project2,并使project2依赖于library1

cat <<EOF > Monorail.toml
[vcs]
use = "git"

[vcs.git]
trunk = "$(git branch --show-current)"

[extension] 
use = "bash"

[[group]]
path = "group1"
depend = [
	"common/library1"
]

  [[group.project]]
  	path = "project1"

  [[group.project]]
  	path = "project2"

  	depend = [
  		"common/library1"
  	]

EOF

group 部分中的 depend 声明表示此路径 可以被 依赖。在 project.depend 中指定项目所依赖的零个或多个此类路径。

要触发变更检测,在 library1 中创建一个文件

touch group1/common/library1/foo.proto

然后 monorail inspect change | jq .

{
  "group": {
    "group1": {
      "change": {
        "file": [
        	{
            "name": "group1/common/library1/foo.proto",
            "project": null,
            "action": "use",
            "reason": "project_depend_effect"
          },
          {
            "name": "group1/project1/foo.txt",
            "project": "group1/project1",
            "action": "use",
            "reason": "project_match"
          }
        ],
        "project": [
          "group1/project2",
          "group1/project1"
        ],
        "link": [],
        "depend": [
          "group1/common/library1"
        ]
      }
    }
  }
}

我们的原始文件条目仍然存在,但出现了新创建文件的另一个条目。它有一个 project 值为 null,因为它不在项目的路径中,并且有一个 reason 表示它被使用是因为项目依赖于包含此文件的路径(project_depend_effect)。

group.project 中出现了一个 group1/project2 条目,表示该项目现在是已更改项目集合的一部分。我们没有在 project2 中更改任何文件(实际上,根本不存在!)但修改了 project2 依赖的路径。

此外,在我们的库路径中,在 group.depend 中出现了一个条目。

链接与 depend 的工作方式类似,但它应用于组中的所有项目,而无需它们进行选择。为了演示,我们将创建第三个项目和虚构的 Lockfile 以将所有项目链接到

mkdir group1/project3
touch group1/Lockfile

执行以下操作以调整 [[group]] 部分 Monorail.toml 以指定此新项目,以及一个组 link

cat <<EOF > Monorail.toml
[vcs]
use = "git"

[vcs.git]
trunk = "$(git branch --show-current)"

[extension] 
use = "bash"

[[group]]
path = "group1"
depend = [
	"common/library1"
]
link = [
	"Lockfile"
]

  [[group.project]]
  	path = "project1"

  [[group.project]]
  	path = "project2"

  	depend = [
  		"common/library1"
  	]
  [[group.project]]
  	path = "project3"

EOF

执行 monocle inspect change | jq . 得到

{
  "group": {
    "group1": {
      "change": {
        "file": [
        	{
            "name": "group1/Lockfile",
            "project": null,
            "action": "use",
            "reason": "group_link_effect"
          },
        	{
            "name": "group1/common/library1/foo.proto",
            "project": null,
            "action": "use",
            "reason": "project_depend_effect"
          },
          {
            "name": "group1/project1/foo.txt",
            "project": "group1/project1",
            "action": "use",
            "reason": "project_match"
          }
        ],
        "project": [
          "group1/project3",
          "group1/project2",
          "group1/project1"
        ],
        "link": [
          "group1/Lockfile"
        ],
        "depend": [
          "group1/common/library1"
        ]
      }
    }
  }
}

同样,我们对 project1library1 的原始更改仍然存在。出现了新的 change.file 条目 Lockfile,为 project3 添加了新的 change.project,并且 change.link 现在具有我们更改的 Lockfile 的路径。

请注意,project3 不需要明确依赖于 Lockfile;只需是 group1 的成员即可。

定义命令

命令通过扩展运行,扩展是用户定义代码的“运行器”。我们已经在 Monorail.toml 中指定了以下内容,因此我们将继续使用 monorail-bash

[extension]
use = "bash"

命令存储在针对每个目标的文件中,其路径在 Monorail.toml 中定义。在我们的情况下,该路径将是相对于 group1/project1support/script/monorail-exec.sh(默认值)。

使用以下命令创建此路径

mkdir -p group1/project1/support/script

group1/project1/support/script/monorail-exec.sh 文件中,我们将定义一个包含三个命令的脚本

cat <<"EOF" > group1/project1/support/script/monorail-exec.sh
#!/usr/bin/env bash

function command1() {
  echo "Hello, from command1"
  echo "The calling environment is inherited: ${SOME_EXTRA_VAR}"
  echo "some data" > side_effects.txt
}

function command2() {
  echo "Hello, from command2"
  cat side_effects.txt
}

function setup() {
  echo "Installing everything you need"
}
EOF

命令名称可以是任何有效的 UTF-8 字符串,并且可以执行任何正常 bash 脚本可以执行的操作:源其他脚本、调用外部构建工具、执行网络请求等。使用 monorail 的好处之一是它不会限制您可以使用的构建工具。

执行命令

定义了命令脚本后,可以使用 monorail-bash exec 调用它。这可以通过两种方式之一完成

  • 隐式地,从monorail的变化检测输出中
  • 显式地,通过指定目标列表

隐式

当隐式执行时,monorail-bash exec使用与monorail inspect change相同的进程来推导变化目标并对它们执行命令。为了说明这一点,请使用以下内容(SOME_EXTRA_VAR仅表示父shell值可以被传递给命令)

SOME_EXTRA_VAR=foo monorail-bash -v exec -c command1 -c command2
Sep 10 07:34:07 monorail-bash : 'monorail' path:    monorail
Sep 10 07:34:07 monorail-bash : 'jq' path:          jq
Sep 10 07:34:07 monorail-bash : 'git' path:         git
Sep 10 07:34:07 monorail-bash : use libgit2 status: false
Sep 10 07:34:07 monorail-bash : 'monorail' config:  Monorail.toml
Sep 10 07:34:07 monorail-bash : working directory:  /Users/patrick/lab/github.com/pnordahl/monorail-tutorial
Sep 10 07:34:07 monorail-bash : command:            command1
Sep 10 07:34:07 monorail-bash : command:            command2
Sep 10 07:34:07 monorail-bash : start:              
Sep 10 07:34:07 monorail-bash : end:                
Sep 10 07:34:07 monorail-bash : target (inferred):             group1/Lockfile
Sep 10 07:34:07 monorail-bash : target (inferred):             group1/common/library1
Sep 10 07:34:07 monorail-bash : target (inferred):             group1/project2
Sep 10 07:34:07 monorail-bash : target (inferred):             group1/project3
Sep 10 07:34:07 monorail-bash : target (inferred):             group1/project1
Sep 10 07:34:07 monorail-bash : NOTE: Ignoring command for non-directory target; command: command1, target: group1/Lockfile
Sep 10 07:34:07 monorail-bash : NOTE: Script not found; command: command1, target: group1/common/library1
Sep 10 07:34:07 monorail-bash : NOTE: Script not found; command: command1, target: group1/project2
Sep 10 07:34:07 monorail-bash : NOTE: Script not found; command: command1, target: group1/project3
Sep 10 07:34:07 monorail-bash : Executing command; command: command1, target: group1/project1
Hello, from command1
The calling environment is inherited: foo
Sep 10 07:34:07 monorail-bash : NOTE: Ignoring command for non-directory target; command: command2, target: group1/Lockfile
Sep 10 07:34:07 monorail-bash : NOTE: Script not found; command: command2, target: group1/common/library1
Sep 10 07:34:07 monorail-bash : NOTE: Script not found; command: command2, target: group1/project2
Sep 10 07:34:07 monorail-bash : NOTE: Script not found; command: command2, target: group1/project3
Sep 10 07:34:07 monorail-bash : Executing command; command: command2, target: group1/project1
Hello, from command2
some data

输出的大部分是工作流程和调试信息,但有几个关键点值得关注。

  • 命令按照由-c选项指定的顺序执行。
  • 我们指定的作为dependlink条目的路径可以指定它们自己的command1command2命令的实现
  • 指向单个文件的路径不能指定命令实现(例如,我们的Lockfile)
  • 没有为command1和/或command2指定实现的路径被记录并忽略

monorail检测到的变化执行任意bash函数有许多应用,包括

  • 对您修改的所有项目/依赖项/链接执行命令,而无需特别针对它们;monorail-bash确保对每个变化目标,请求的命令按顺序执行
  • 在CI中运行针对所有变化目标的特定命令(例如,checkbuildtestdeploy等)

显式

手动选择目标使您能够独立于VCS变化检测执行命令。应用包括

  • 帮助新开发者熟悉代码库,因为可以在一组命令中定义所有设置代码并对其执行,例如一个bootstrap命令
  • 运行任何目标在整个仓库中的任何命令

为了说明手动选择目标,我们将运行之前定义但未执行的setup命令。执行以下操作(删除-v以减少视觉噪音)

monorail-bash exec -t group1/project1 -c setup
Installing everything you need

有关更多信息,请执行monorail-bash -hmonorail-bash exec -h

发布

monorail使用后端VCS的本地机制,例如git中的标签作为“发布”标记。这为变化检测创建了一个“检查点”。如果没有发布标签,monorail将被迫搜索git历史记录到第一个提交。这将是不高效的,并且使变化检测变得无用,因为所有目标在足够长的时间线上都被视为已更改。

执行发布时,它适用于自上次发布以来更改的所有目标(或如果尚未存在发布,则为存储库的第一个提交)。

首先,让我们提交并推送我们的当前更改

git add * && git commit -am "update commands" && git push

假设我们已经提交了我们打算提交的内容,并且目标命令已运行并令人满意(例如,CI已通过我们分支的合并),我们可以使用以下命令进行补丁发布的dry-run

monorail release --dry-run -t patch | jq .
{
  "id": "v0.0.1",
  "targets": [
    "group1/Lockfile",
    "group1/common/library1",
    "group1/project1",
    "group1/project2",
    "group1/project3"
  ],
  "dry_run": true
}

monorail使用适合所选VCS约定的id创建发布;在这种情况下,这是git semver标签格式。它还将在targets数组中嵌入包含在本发布中的目标列表;对于git,它将在这个发布消息中嵌入目标列表。

现在,运行一个真正的发布

monorail release -t patch | jq .
{
  "id": "v0.0.1",
  "targets": [
    "group1/Lockfile",
    "group1/common/library1",
    "group1/project1",
    "group1/project2",
    "group1/project3"
  ],
  "dry_run": false
}

为了表明发布清除了monorail对变化的观点,请执行以下操作

monorail inspect change | jq .
{
  "group": {
    "group1": {
      "change": {
        "file": [],
        "project": [],
        "link": [],
        "depend": []
      }
    }
  }
}

最后,我们新推送的标签现在在远程。要查看此内容,请执行以下操作

git -C ../monorail-tutorial-remote show -s --format=%B v0.0.1

... 输出结果

tag v0.0.1
Tagger: you <email@domain.com>

group1/Lockfile
group1/common/library1
group1/project1
group1/project2
group1/project3

本教程到此结束。您已经了解了 monorail 的核心概念及其扩展的工作方式,现在您可以将其用于实际项目。尝试不同的仓库布局、命令、持续集成(CI)以及基于主干开发的工作流程,以适应您的团队。

有关 monorail 及其扩展的各种配置的更多信息,请参阅 Monorail.reference.toml

不变性

为了正常运行,monorail 需要以下不变性得到满足:

  1. 组不能引用其他组中的路径
  2. 目标不能引用其他目标中的路径
  3. 只能共享指定在组中的 linkdepend 配置中的路径

如果违反了其中任何一项,则 monorail 命令分析仓库更改的行为是未定义的。

开发设置

这将构建项目并运行测试

cargo build
cargo test -- --nocapture

您可以使用 install.sh 构建一个 monorail 的发布二进制文件并将其与扩展一起复制到您的 PATH 中。

依赖关系

~14MB
~308K SLoC