Showing preview only (419K chars total). Download the full file or copy to clipboard to get everything.
Repository: baidu/m-git
Branch: master
Commit: 582913c5566c
Files: 109
Total size: 334.7 KB
Directory structure:
gitextract_p54qz427/
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── docs/
│ ├── commands/
│ │ └── commands.md
│ ├── config/
│ │ ├── config-env.md
│ │ └── manifest.md
│ ├── design/
│ │ ├── design.md
│ │ ├── principle.md
│ │ └── workspace.md
│ ├── extension/
│ │ ├── cmd_ext.md
│ │ ├── hooks.md
│ │ ├── plugins.md
│ │ └── submodule.md
│ ├── faq/
│ │ └── faq.md
│ ├── references.md
│ └── use/
│ ├── common-commands.md
│ └── how-to-start.md
├── lib/
│ ├── m-git/
│ │ ├── argv/
│ │ │ ├── opt.rb
│ │ │ ├── opt_list.rb
│ │ │ └── parser.rb
│ │ ├── argv.rb
│ │ ├── base_command.rb
│ │ ├── command/
│ │ │ ├── add.rb
│ │ │ ├── branch.rb
│ │ │ ├── checkout.rb
│ │ │ ├── clean.rb
│ │ │ ├── commit.rb
│ │ │ ├── config.rb
│ │ │ ├── delete.rb
│ │ │ ├── fetch.rb
│ │ │ ├── forall.rb
│ │ │ ├── info.rb
│ │ │ ├── init.rb
│ │ │ ├── log.rb
│ │ │ ├── merge.rb
│ │ │ ├── pull.rb
│ │ │ ├── push.rb
│ │ │ ├── rebase.rb
│ │ │ ├── reset.rb
│ │ │ ├── self.rb
│ │ │ ├── stash.rb
│ │ │ ├── status.rb
│ │ │ ├── sync.rb
│ │ │ └── tag.rb
│ │ ├── command_manager.rb
│ │ ├── error.rb
│ │ ├── foundation/
│ │ │ ├── constants.rb
│ │ │ ├── dir.rb
│ │ │ ├── duration_recorder.rb
│ │ │ ├── git_message_parser.rb
│ │ │ ├── lock.rb
│ │ │ ├── loger.rb
│ │ │ ├── mgit_config.rb
│ │ │ ├── operation_progress_manager.rb
│ │ │ ├── timer.rb
│ │ │ └── utils.rb
│ │ ├── foundation.rb
│ │ ├── hooks_manager.rb
│ │ ├── manifest/
│ │ │ ├── cache_manager.rb
│ │ │ ├── internal.rb
│ │ │ ├── light_repo.rb
│ │ │ ├── light_repo_generator.rb
│ │ │ ├── linter.rb
│ │ │ └── manifest.sample
│ │ ├── manifest.rb
│ │ ├── open_api/
│ │ │ └── script_download_info.rb
│ │ ├── open_api.rb
│ │ ├── output/
│ │ │ └── output.rb
│ │ ├── plugin/
│ │ │ ├── completion.bash
│ │ │ ├── completion.zsh
│ │ │ └── init.sh
│ │ ├── plugin_manager.rb
│ │ ├── repo/
│ │ │ ├── status.rb
│ │ │ └── sync_helper.rb
│ │ ├── repo.rb
│ │ ├── template/
│ │ │ ├── local_manifest.rb
│ │ │ ├── manifest_hook.rb
│ │ │ ├── post_download_hook.rb
│ │ │ ├── post_hook.rb
│ │ │ ├── pre_exec_hook.rb
│ │ │ ├── pre_hook.rb
│ │ │ └── pre_push_hook.rb
│ │ ├── template.rb
│ │ ├── version.rb
│ │ ├── workspace/
│ │ │ ├── path_helper.rb
│ │ │ └── workspace_helper.rb
│ │ └── workspace.rb
│ └── m-git.rb
├── m-git.gemspec
├── manifest.json
├── mgit
└── test/
├── argv/
│ ├── test_opt.rb
│ ├── test_opt_list.rb
│ └── test_parser.rb
├── foundation/
│ ├── test_dir.rb
│ ├── test_mgit_config.rb
│ └── test_utils.rb
├── manifest/
│ └── test_light_repo.rb
├── test_argv.rb
├── test_foundation.rb
├── test_helper.rb
├── test_manifest.rb
├── test_open_api.rb
├── test_plugin_manager.rb
└── test_workspace.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
.bundle
vendor/
*.gem
## IDE
*.xccheckout
.idea
## File
*.svn/
*.DS_Store
.DS_Store?
## other
*.pyc
# log指令缓存
log_cache.json
================================================
FILE: CHANGELOG.md
================================================
## Release Note:
### 2.5.5
-【bug修复】修复 box config -m 命令 不可用问题,修改参数类型为String
### 2.5.4
- mgit增加inject注入文件功能
- 增加manifest.json文件,当前仓库可用于mgit-demo
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at baidu. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: Gemfile
================================================
source "https://gems.ruby-china.com"
# git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
# Specify your gem's dependencies in mgit.gemspec
gemspec
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2020 Baidu, Inc. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# MGit
MGit 是一款 Ruby 封装的基于 Git 的多仓库管理工具,可以高效的、同时的对多个 Git 仓库执行 Git 命令。
适合于在多个仓库中进行关联开发的项目,提高 Git 操作的效率,避免逐个执行 Git 命令带来的误操作风险。
- **易用的命令**
封装 Git 命令,命令和参数均由 Git 衍生而来,会使用 Git 就可以成本低上手 MGit。
- **直观高效的执行命令**
提供图表化的结果展示,开发者可以快速查看命令在多个仓库的执行结果;
多线程并发执行多仓库命令,通过仓库缓存机制提高仓库的拉取效率;
- **安全的执行命令**
在执行命令前对多仓库状态进行安全检查:分支是否异常,工作区是否未提交代码等;
对 .git 进行托管与 Git 工作区分类,避免误删丢失改动或提交;
执行存在风险的操作时,会给与风险操作提示,避免误操作;
- **方便扩展**
支持加载 ruby-gem 包作为插件,gem 包名格式 `m-git-${suffix}`和`mgit-${suffix}`
快速的扩展 MGit 的命令,增加自定义命令,扩展已有命令的功能;
提供类似`git hook`的 hook 点,方便开发者实现自定义逻辑;
## 快速开始
#### 1、安装 MGit 工具
环境要求:
- 系统:支持 macOS、Ubuntu,暂时不支持 Windows
- Ruby版本: >= 2.3.7
```ruby
$ gem install m-git
```
#### 2、初始化多仓库
初始化多仓库使用 `mgit init` 命令;
类似于 Git 从远程 clone 新仓库, 会将多个仓库 clone 到本地;
下面通过一个 demo 体验一下 MGit 命令:
```ruby
# 2.1 建议在一个新文件夹中拉取demo
$ mgit init -g https://github.com/baidu/m-git.git
# 2.2 体验一下mgit命令
$ mgit -l 显示所有mgit管理的仓库
$ mgit branch --compact 查看多仓库的分支
$ mgit status 产看仓库分支超前/落后情况
```
#### 3、已有多仓库如何迁移到 MGit 管理
- 根据文档[配置 manifest.json](docs/config/manifest.md)
将要管理的仓库都配置到 manifest.json 中
- 将 manifest.json 放到一个仓库中管理
这个仓库同样会在 manifest.json 中描述,并且需要配置 "config-repo": true
这个仓库称为配置仓库,也叫做**主仓库**,其他仓库叫做**子仓库**
- 使用 `mgit init -f manifest文件路径` 初始化多仓库,命令测试 manifest.json 配置是否正常
注意这个命令不会重复拉取主仓库,只会拉取所有的子仓库到当前目录,并在当前目录创建一个.mgit
你可以在当前目录中看到每个仓库的源码,他们的路径可以通过 manifest.json 的 dest字段配置
你也可以在 .mgit/source-git/ 下看到所有仓库的.git, 这是 MGit 对所有仓库的托管
- 本地测试成功后,你可以提交主仓库中的 manifest.json,推送主仓库的变更到远端
- 通过 `mgit init -g 主仓库地址` 命令初始化多仓库
#### 4、进一步了解 MGit
[常用命令](docs/use/common-commands.md)
[manifest文件介绍](docs/config/manifest.md)
[配置多仓库](docs/config/config-env.md)
[使用 MGit 管理多仓库的案例](docs/use/how-to-start.md#4-mgit-)
[了解更多](docs/references.md)
## 测试
单测在MGit仓库内的test文件夹下
新建单测文件,必须以‘test_’开头
执行单测:rake (如果报错尝试执行 bundle install)
## 如何贡献
欢迎开发者向 MGit 贡献代码。如果您开发了新功能或发现了 bug,欢迎给我们提交PR。
代码贡献要求:
1. 功能和实现应该具有通用性, 不是为了解决某个具体业务而定制的代码逻辑
2. 代码质量高,符合 Ruby 编码规范
3. 需要补充对应的单测 case
issues贡献: 如在使用中遇到问题,请在 https://github.com/baidu/m-git/issues 新建 issues 反馈问题。
================================================
FILE: Rakefile
================================================
require "bundler/gem_tasks"
task :default => :test
require 'rake/testtask'
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.pattern = 'test/**/test_*.rb'
t.verbose = true
end
================================================
FILE: docs/commands/commands.md
================================================
## Command-line Reference
参考:https://guides.cocoapods.org/terminal/commands.html#pod_spec_lint
# Brief Description Of Commands
## Initalizing
+ mgit
+ config 用于更新多仓库配置信息。
## Creating && Workspace
+ init 初始化多仓库目录。
+ sync 根据配置表(从远端或本地)同步仓库到工作区,包括被锁定仓库,已经在工作的不作处理(默认不执行pull)。
+ info 输出指定仓库的信息。
+ clean 对指定或所有仓库执行"git add . && git reset --hard"操作,强制清空暂存区和工作区。
+ delete 删除指定单个或多个仓库(包含被管理的.git文件和工程文件以及跟该.git关联的所有缓存)。
## Snapshotting
+ add 将文件改动加入暂存区。
+ status 输出所有仓库的状态。包括:"分支","暂存区","工作区","特殊(未跟踪和被忽略)","冲突"。
+ commit 将修改记录到版本库。
+ reset 将当前HEAD指针还原到指定状态。
## Branching
+ branch 列出,创建和删除分支。
+ checkout 切换分支或恢复工作区改动。
+ merge 合并两个或多个开发历史。
+ stash 使用git stash将当前工作区改动暂时存放起来。
+ tag 增删查或验证标签。增加标签示例:mgit tag -a 'v0.0.1' -m 'Tag description message'
+ log 输出指定仓库的提交历史。
## Trunk
+ fetch 从远程仓库下载数据对象和引用。
+ pull 从仓库或本地分支获取数据并合并。
+ rebase 重新将提交应用到其他基点,该命令不执行lock的仓库。
+ push 更新远程分支和对应的数据对象。
## Addition
+ forall 对多仓库批量执行指令。
================================================
FILE: docs/config/config-env.md
================================================
### config.yml 介绍
config.yml 是 MGit 的配置文件;
MGit的配置内容保存在多仓库目录下的`.mgit/config.yml`文件中,配置仅针对当前的多仓库生效;
- 通过 `$ mgit config --list` 可以查看当前config.yml中的配置
```ruby
[是否将.git实体托管给MGit,托管后仓库内的.git目录是软链 (true/false)]
managegit: true
[MGit操作的最大并发数? (Integer)]
maxconcurrentcount: (默认当前机器的逻辑 CPU 数)
[是否按照配置表同步工作区? (true/false)]
syncworkspace: true
[同步工作区时回收的仓库是否保留到缓存中? (true/false)]
savecache: false
[是否开启日志打印,操作日志会收集到.mgit/log目录下? (true/false)]
logenable: true
[日志的打印级别, DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, FATAL = 4]
loglevel: 0
```
- 通过 `$ mgit config --set` 可以对config.yml进行配置,如:"mgit config -s maxconcurrentcount 20"
- 当前支持的几个配置项:
| key | 类型 | 描述 | 默认值 |
| ------------------ | :------ | ------------------------------------------------------------ | ------ |
| managegit | Boolean | 如果该配置为`true`, 那么在通过MGit下载新仓库时(`mgit sync -n`),会将工作区仓库内的`.git`托管给MGit(将`.git`移动到`.mgit/source-git/`下)。若配置为`false`,则任何情况下都不操作`.git`, 如有`.git`已经被托管,则弹出到工作区。 | true |
| maxconcurrentcount | Integer | MGit操作的最大并发数. | 当前机器的逻辑 CPU 数 |
| syncworkspace | Boolean | 当配置表发生改变的时候,工作区内的仓库可能和配置表不匹配,若配置为`true`,会将上次操作和本地操作依据的配置表做对比,将多余的仓库(如有的话)缓存到`/.mgit/source-git/.../cache`目录下。若配置为`false`,则不操作。 | true |
| savecache | Boolean | 同步工作区时回收的仓库被放到缓存中后,若配置为`true`,则一直保留该工作区目录,若配置为`false`,则直接删除(`.git`被托管的仓库可以删除工作区,需要时导出即可)。该配置在`syncworkspace`为`false`时不生效。 | false |
| logenable | Boolean | 是否开启日志打印,操作日志会收集到.mgit/log目录下 | true |
| loglevel | Integer | 日志的打印级别, DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, FATAL = 4 | 1 |
- 关于 managegit 与 syncworkspace 配置的详细说明
建议配置:
- managegit :true
- syncworkspace : true
在使用MGit管理多仓库时,当前分支dev-1分支 有 a、b、c 三个仓库;
1、现在需要checkout到一个旧分支feature-1,feature-1只配置了 a、b仓库,此时c仓库会被MGit回收,不会展示在当前工作区(syncworkspace : true 按照配置表同步工作区)
2、但c仓库还有未推到远端的开发分支和stash的代码,MGit会托管暂时不用的c仓库,保证本地c仓库中的的代码不会丢失( managegit: true 托管工作区)
3、当再checkout到dev-1分支时,会将被托管的c仓库同步回工作区(syncworkspace : true 按照配置表同步工作区)
================================================
FILE: docs/config/manifest.md
================================================
### 1. manifest 配置介绍
manifest.json 配置表定义了多仓库管理所需的仓库信息,采用json格式,示例如下:
```ruby
{
"remote": "https://github.com/baidu",//远程仓库地址
"version":1, //配置文件版本
"mgit-excluded": false,
"dest": "Sources", //本地仓库相对mgit根目录存放路径
"repositories": { //源码列表,包含git仓库和非git仓库
"MainApp": {
"remote-path": "mainapp" //远程仓库名,对于需要mgit管理的仓库是必需字段。此时git地址为:https://github.com/baidu/mainapp
"config-repo": true //指定该仓库为配置仓库,即包含本配置表的仓库,仅允许指定一个仓库
},
"Watch": {
"remote-path": "watch"
"dest":"temp/test" //可选,覆盖最外层定义,在mgit根目录下仓库的父目录。该仓库本地路径为"<mgit根目录>/temp/test/Watch"
},
"BBAAccount": {
"remote-path": "bbaaccount",
"abs-dest":"/Users/someone/baidu/temp/my_account" //仓库本地完整路径(指定后dest无效)
},
"Script": {
"remote-path": "script",
"remote": "https://github.com/some_script",//可选,覆盖最外层定义
"lock": {
"branch":"my_branch"//当前分支
或"tag":"tag1"//tag
或"commit_id":"123456"//HEAD指向的commit id
}//锁定某仓库状态,每次执行指令时会保持该仓库为指定的状态
},
"Some_Repo": {
"remote-path": "some_repo",
"mgit-excluded": true//指定不进行多仓库管理,可选
},
//本地git仓库或非git仓库(mgit不操作)
"New_Repo": {
"dest": "Some/Dir",
"mgit-excluded": true
},
"Test_Repo": {
"dest": "Some/Dir2",
"dummy": true //(2.3.0已废弃)指定该仓库为占位仓库(mgit不操作,EasyBox组装器也不使用)
}
}
}
```
### 2. manifest 配置中的具体字段介绍
#### 2.1 一级字段
| 字段名 | 说明 | 必要性 | 值类型 |
| :-------------- | :----------------------------------------------------------- | :----- | :------------------- |
| `remote` | 远程仓库git地址的根目录。注意,完整地址为`<remote>/<remote-path>`。 | 必需 | `String` |
| `version` | 配置文件版本。 | 必需 | `Number` |
| `dest` | 在mgit根目录下仓库的父目录,此时完整本地目录为:`<mgit根目录>/<dest>/<repo_name>`。 | 必需 | `String` |
| `mgit-excluded` | 指定为`true`则不被mgit纳入多仓库管理,即mgit不会操作该仓库。 | 可选 | `Bool` |
| `repositories` | 源码列表。 | 必需 | `JSON<String, JSON>` |
#### 2.2 `repositories`内的Json数据
| 字段名 | 说明 | 值类型 |
| :------------ | :----------------------------------------------------------- | :----- |
| `<repo_name>` | 仓库唯一标识,值为该仓库的配置。在`dest`生效时,为本地仓库目录名,此时完整本地目录为:`<mgit根目录>/<dest>/<repo_name>`。 | `JSON` |
#### 2.3. 仓库配置字段
| 字段名 | 说明 | 必要性 | 值类型 |
| :-------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- | :------- |
| `remote-path` | 远程仓库git地址相对路径。注意,完整的远程地址为`<remote>/<remote-path>`。 | 有条件可选:在不显式指定`mgit-excluded`为`true`的情况下(即希望该仓库纳入mgit管理的话),是必须字段,否则可选。 | `String` |
| `remote` | 远程仓库地址根目录,如果指定则覆盖一级字段的`remote`。注意,完整的远程地址为`<remote>/<remote-path>`。 | 可选 | `String` |
| `lock` | 指定后会锁定仓库状态,每次执行指令时会保持该仓库为锁定状态。状态可以指定`branch`, `tag`, `commit_id`中的一个,如`lock: { "branch": "master" }`。 | 可选 | `Json` |
| `dest` | 本地仓库相对mgit根目录存放的父目录,如果指定则覆盖一级字段的`dest`。此时完整本地目录为:`<mgit根目录>/<dest>/<repo_name>`。 | 可选 | `String` |
| `abs-dest` | 本地仓库的完整存放路径,如果指定则`dest`失效,此时仓库完整的本地路径为`<abs-dest>`。 | 可选 | `String` |
| `config-repo` | 如果指定为`true`则表明该仓库为包含该配置文件的配置仓库,最多只能指定一个。注意,指定了该仓库后,某些mgit操作会优先处理该仓库,如`checkout`,`merge`,`pull`。 | 可选 | `Bool` |
| `mgit-excluded` | 指定为`true`则mgit不操作,此处指定会覆盖一级字段的`mgit-excluded`。 | 可选 | `Bool` |
| `[2.3.0已废弃] dummy` | 指定为`true`则表明该仓库是占位仓库,不受mgit操作,同时EasyBox组装器也不使用它的**源码**,单纯用于记录仓库信息,如一些完全二进制的三方库。<br><br>**提示**:指定`dummy:true`后,则默认指定`mgit-excluded:true`(如果显示指定了`mgit-excluded:false`,依然将其置为`true`)。该字段用于配合EasyBox进行工程组装,日常使用无需添加。 | 可选 | `Bool` |
- **mgit根目录**是指mgit初始化的目录,在该目录下存在`.mgit`隐藏文件。
- 配置表文件名必须是`manifest.json`
- 配置表中同名字段覆盖优先级:仓库配置 > 全局配置。
- 对于值为`Bool`类型的字段,缺省意味着该字段值为`false`。
- 采用包含配置表的中央仓库的方式管理多仓库时,需要将配置仓库的配置也描述在配置表中。
### 3. 本地配置表 local_manifest
当你想在本地调试时,可以通过创建`local_manifest.json`临时修改多仓库配置;可以在不修改`manifest.json`的情况下对配置进行调整;
可以选择以下方式创建 local_manifest.json:
- 通过 `migt init` 命令自动创建
- 执行命令时不带参数`-l`参数
将自动在配置仓库创建不生效的`local_manifest.json`,内容为:
```ruby
{
"repositories": {
// 可自行根据需要添加/修改仓库配置
}
}
```
- 执行命令时添加参数`-l`
则在配置仓库创建`local_manifest.json`,此时该文件只包含配置仓库信息:
```ruby
// 此配置的含义:让mgit只管理配置仓库,其余仓库均不管理,主要用于简化壳工程初始化。
{
"mgit-excluded": true,
"repositories": {
"config_repo_name": {
"mgit-excluded": false
}
}
}
```
- 通过 `mgit config` 命令创建
指定一个目录,在该目录下创建`local_manifest.json`,若目录不存在则自动创建。如:
```ruby
$ mgit config -c /a/b/c
```
**注意**:
* `/a/b/c`为包含配置文件的文件夹目录,此时生成配置文件`/a/b/c/local_manifest.json`
* 如果未传入值,如:`mgit config -c`,那么若配置仓库存在的话,会在配置仓库中创建空的本地配置文件。
- 手动创建 local_manifest.json
* 手动新建配置文件`local_manifest.json`,放到任意位置,执行以下指令将其托管给mgit:
```
//注意,此后如果local_manifest.json位置发生了移动,需要重新执行该指令使其生效。
$ mgit config -u <path_to>/local_manifest.json
```
* 在不执行指令的情况下,可手动创建`local_manifest.json`并将其放置于下列目录即可自动生效:
将创建的local_manifest.json 放到`manifest.json`所在目录 或 `.mgit/source-config`文件夹内 即可生效
### 4、 对local_manifest.json 校验与合并
- `local_manifest.json`文件的字段与`manifest.json`完全一致,唯一区别是不会对它做字段合法性校验,对于不需要覆盖的字段可以缺省。
- 执行mgit指令时,会将`manifest.json`和`local_manifest.json`合并,配置文件中的字段会被本地配置文件中定义的对应字段覆盖(`repositories`字段除外,而是仓库配置内的对应字段被覆盖),如:
```ruby
// manifest.json:
{
"remote":"https://github.com/baidu",
"version":1,
"dest":"Sources",
"repositories": {
"TestRepo1": {
"remote-path":"test1.git",
"mgit-excluded": false
}
}
}
// local_manifest.json:
{
"remote":"https://github.com/baidu", //覆盖原定义
"repositories": {
//将manifest.json内定义的TestRepo1排除管理
"TestRepo1": {
"mgit-excluded": true
},
//添加一个新仓库,配置完成后执行mgit sync -n可下载该仓库
"TestRepo2": {
"remote-path":"test.git"
}
}
}
// 将合并为:
{
"remote":"https://github.com/baidu", //被覆盖
"version":1,
"dest":"Sources",
"repositories": {
"TestRepo1": {
"remote-path":"test1.git",
"mgit-excluded": true //被覆盖
},
//被添加
"TestRepo2": {
"remote-path":"test.git"
}
}
}
```
================================================
FILE: docs/design/design.md
================================================
## 设计篇(略)
### 基本架构
================================================
FILE: docs/design/principle.md
================================================
## 基本原则
================================================
FILE: docs/design/workspace.md
================================================
### 工作区
================================================
FILE: docs/extension/cmd_ext.md
================================================
### 一、扩展指令
MGit 加载插件有**2种方式**:
- **ruby gem 包加载**
当运行 MGit 命令时,从所有的`ruby-gems`包中查找并加载 MGit 的插件,查找判定条件 gem 包中存在如下文件:
```
- m-git_plugin.rb
- mgit_plugin.rb
```
> Tips:插件功能开发完成后,打包为 gem 包后发布到 gem 源,MGit 执行命令时将自动被加载
- **inject 参数加载**
```
# 执行命令时加载插件
# path_to_plugin:可以是待加载的插件文件或文件夹,
# 如果是文件夹,则从中查找插件文件(m-git_plugin.rb、mgit_plugin.rb)并加载
$ mgit add --inject=${path_to_plugin}
```
### 1分钟极简扩展(以Demo多仓库示例,待修改demo url)
**需求:**假设我们要提供一个命令用于查看 MGit 管理的所有 Git 仓库,创建一个新的指令:hi
```
$ mgit hi //输出当前mgit管理的仓库列表
```
**Step0:**建议在一个新文件夹中拉取demo
```
mgit init -g https://github.com/baidu/m-git.git
```
**Step1:**新建一个文件:hi.rb (无所谓名称)
**Step2:**增加一个继承自 BaseCommand 类的子类`Hi`,指令默认使用子类的名称,可使用`self.cmd`方法重写
```
module MGit
class Hi < BaseCommand
def execute(argv)
# Do something for each repo.
puts all_repos.map(&:name).join(',')
end
end
end
```
**测试:**
```
# inject 指向扩展命令的文件路径(或全路径)
mgit hi --inject=hi.rb
> m-git,DDParser
```
================================================
FILE: docs/extension/hooks.md
================================================
### hook
#### 一、什么是hook
hook是在MGit 仓库中特定事件触发后被调用的脚本。hook最常见的使用场景包括根据仓库状态改变项目环境、接入持续集成工作流等。 由于脚本是可以完全定制,所以你可以用hook来自动化或者优化你开发工作流中任意部分。
#### 二、MGit的hook
hook脚本位于多仓库目录下的`/.mgit/hooks`内,下表列出个各hook的作用
| hook脚本 | 作用 |
| --------------------- | ------------------------------------------------------------ |
| pre_hook.rb | 指令执行前hook(执行时机较早,内部只能获取当前指令和多仓库根目录) |
| post_hook.rb | 指令执行后hook |
| pre_exec_hook.rb | 指令执行前hook(内部除当前指令和多仓库根目录外,还可获取执行仓库的 |
| manifest_hook.rb | mgit读取配置文件前执行的hook |
| post_download_hook.rb | mgit sync下载新仓库后执行的hook |
| pre_push_hook.rb | mgit push前执行的hook(类似pre_exec_hook) |
【注意】执行顺序:
1. mgit指令pre_hook
2. mgit多仓库pre_hook
3. manifest_hook
4. pre_exec_hook/pre_push_hook/post_download_hook
5. git前置hook
6. git后置hook
7. mgit多仓库post_hook
8. mgit指令post_hook
如下代码是 pre_hook.rb 的模板:
```
#coding=utf-8
module MGitTemplate
class PreHook
# hook接口,用于接受本次指令执行前的数据
#
# @param cmd [String] 本次执行指令
#
# @param opts [String] 本次执行指令参数
#
# @param mgit_root [String] mgit根目录
#
def self.run(cmd, opts, mgit_root)
end
end
end
```
================================================
FILE: docs/extension/plugins.md
================================================
### 插件
- 插件介绍,可以做什么
- 如何扩展插件
- 如何引用其他插件
================================================
FILE: docs/extension/submodule.md
================================================
### 用做子工具
- 介绍其他工具如何调用mgit作为模块
================================================
FILE: docs/faq/faq.md
================================================
## F.A.Q
================================================
FILE: docs/references.md
================================================
<center style="font-family:verdana">MGit相关文档</center>
* [介绍](../README.md)
* 使用篇
* [快速开始](use/how-to-start.md)
* [常用命令](use/common-commands.md)
* 配置篇
* [Manifest](config/manifest.md)
* [config.yml](config/config-env.md)
* 扩展篇
* [扩展指令](extension/cmd_ext.md)
* [MGit Hooks](extension/hooks.md)
* [F.A.Q](faq/faq.md)
* [Command-line Reference](commands/commands.md)
================================================
FILE: docs/use/common-commands.md
================================================
## 常用命令
> 指令描述符说明:
>
> < > : 占位参数
>
> [ ] : 可选组合
>
> ( ) :必选组合
>
> | : 互斥参数
>
> ··· :可重复指定前一个参数
### mgit
` mgit [-l [-a]|-s] [-v] [-h]`
| 特有参数 | 说明 |
| ----------------- | ------------------------------------------------------------ |
| `-a, --all` | 指定操作所有(包含不被mgit管理)的仓库,可配合`-l`合并使用: `mgit -al` |
| `-h, --help` | 显示帮助。 |
| `-l, --list` | 显示所有migt管理的仓库。 |
| `-s, --size` | 显示所有仓库的磁盘占用量。 |
| `-v, --version` | 显示mgit版本。 |
| `-w, --workspace` | 显示当前mgit工程管理根目录(`.mgit`所在目录)。 |
【注意】使用`-l, --list`参数列出所有被mgit管理的仓库,以表格的形式输出,其中表格头部为包含该仓库的目录(相对mgit根目录),同时标明本地缺失仓库。
### add
` mgit add [<git-add-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]`
添加文件到暂存区
| 特有参数 | 说明 |
| ------------ | ------------------------------------------------------------ |
| `--mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `--el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `--help` | 显示帮助。 |
【注意】执行前会校验各个仓库所在分支是否一致,否则会提示是否继续:
### branch
`mgit branch [<git-branch-option>|--compact] [(--mrepo|--el-mrepo) <repo>...] [--help]`
对多仓库进行分支操作
| 特有参数 | 说明 |
| ------------ | ------------------------------------------------------------ |
| `--mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `--compact` | 以紧凑的方式显示所有仓库的当前分支。 |
| `--el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `--help` | 显示帮助。 |
【注意】在指定`--compact`的情况下,其余传入的原生`git-branch-option`指令将不生效。指定`--compact`时,会将所有仓库的当前分支做一个归类,便于检查哪些仓库当前处于不同分支,对于特殊状态(HEAD游离和被锁定的仓库)也会显示出来。

### checkout
`mgit checkout [<git-checkout-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]`
克隆远程仓库
| 特有参数 | 说明 |
| ------------ | ------------------------------------------------------------ |
| `--mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `--el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `--help` | 显示帮助。 |
【注意】
1. 若指定了配置仓库,执行该指令时会先checkout配置仓库仓库,如果它本地有修改,则切换失败,需要自行将配置仓库处理干净后重试,也可以选择跳过继续。如果使用checkout切分支,
此处建议先**取消操作并清理配置仓库**的改动再重试,因为切换的目标分支上配置仓库中的配置文件可能与当前不同,不先完成它的切换,后续操作的仓库可能产生遗漏。

2. 在配置仓库切换成功后,会根据配置仓库内的配置表来`checkout`其余仓库,此时如果有异常状态仓库,会给出提示:

3. 在配置仓库切换成功后,配置表可能发生改变,此时可能会遇到仓库缺失问题,如:A分支10个仓库,B分支11个仓库,当前从A分支切换到B分支,B分支多的1个仓库本地没有,此时会自动下载缺失仓库,并根据当前多数仓库所处分支进行推荐切换。

### clean
`mgit clean [(-m|-e) <repo>...] [-h]`
用于清空所有工作区和暂存区,即对所有仓库或指定仓库执行`git add . && git reset --hard`
| 特有参数 | 说明 |
| ---------------- | ------------------------------------------------------------ |
| `-m, --mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `-e, --el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `-h, --help` | 显示帮助。 |
### commit
`mgit commit [<git-commit-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]`
| 特有参数 | 说明 |
| ------------ | ------------------------------------------------------------ |
| `--mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `--el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `--help` | 显示帮助。 |
【注意】
- 如果当前仓库的暂存区没有待提交内容的话,则不会执行`commit`操作。
- 执行前会校验各仓库分支是否统一。
- 本指令不支持交互式操作,因此不支持`--ammend`。
### config
```
mgit config (-m|-u)
mgit config -c
mgit config (-l | -s )
mgit config -h
```
| 特有参数 | 说明 |
| ----------------------- | ------------------------------------------------------------ |
| `-c, --create-local` | 在指定目录下创建本地配置文件,若目录不存在则自动创建。如执行:`mgit config -c /a/b/c`,则生成本地配置文件:`/a/b/c/local_manifest.json`。如果未传入值,如:`mgit config -c`,那么会在配置仓库中创建本地配置文件(若配置仓库存在的话)。 |
| `-m, --update-manifest` | 该指令用于更新mgit所使用的配置文件,如:`mgit config -m manifest.json`。 |
| `-u, --update-local` | 该指令用于更新mgit所使用的本地配置文件,如:`mgit config -u /local_manifest.json`。 |
| `-l, --list` | 列出当前MGit所有配置,无参数,如:`mgit config -l`。 |
| `-s, --set` | 对MGit进行配置,遵守格式:`mgit config -s `,如:`mgit config -s maxconcurrentcount 5`。 |
| `-h, --help` | 显示帮助。 |
命令使用细节请查看[config配置文档](../config/config-env.md)
### delete
`mgit delete <repo>... [-h]`
该指令用于删除单一仓库的,包括被mgit管理存在于.mgit文件夹中的git实体和存在.mgit外部的工程文件。
| 特有参数 | 说明 |
| ------------ | ---------- |
| `-h, --help` | 显示帮助。 |
【注意】如果需要删除某个仓库,仅仅手动删除.mgit外部的工程文件并不完整,还需要删除.mgit/source-git内的git实体,因此如果需要删除一个仓库的话,最好使用该指令来完整删除。
### fetch
`mgit fetch [<git-fetch-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]`
从远程仓库获取代码
| 特有参数 | 说明 |
| ------------ | ------------------------------------------------------------ |
| `--mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `--el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `--help` | 显示帮助。 |
### forall
`mgit forall -c '<instruction>' [(-m|-e) <repo>...] [-n] [-h]`
该指令用于在指定(或所有)仓库上执行自定义指令
| 特有参数 | 说明 |
| ------------------ | ------------------------------------------------------------ |
| `-c, --command` | 指定需要执行的shell命令,如:`mgit -c "git status -s"`(注意要带引号)。 |
| `-n, --concurrent` | 可选参数,若指定,则shell命令以多线程方式执行(若缺省则以单线程串行方式执行)。 |
| `-m, --mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `-e, --el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `-h, --help` | 显示帮助。 |
### info
`mgit info <repo>... [-h]`
产看一个仓库的信息
| 特有参数 | 说明 |
| ------------ | ---------- |
| `-h, --help` | 显示帮助。 |
### log
`mgit log <repo> [-n] [-h]`
输出单一仓库的log
| 特有参数 | 说明 |
| -------------- | -------------------------------------------- |
| `-h, --help` | 显示帮助。 |
| `-n, --number` | 指定需要显示的提交log个数,不指定则默认500。 |
### merge
```
mgit merge [] [--pull] [(--mrepo|--el-mrepo) ...] [--help]
mgit merge --continue
mgit merge --abort
```
| 特有参数 | 说明 |
| ------------ | ------------------------------------------------------------ |
| `--mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `--el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `--pull` | 可选参数,指定后在合并仓库前会自动拉取远程分支更新代码,否则会有交互式询问。如:`mgit merge --pull` |
| `--continue` | MGit自定义参数,仅在操作多仓库过程中出现问题停止,执行状态进入中间态后可用。该参数只能单独使用,解决问题后可执行`mgit merge --continue`继续操作其余仓库。 |
| `--abort` | Git原生参数,用于取消`git merge`中间态,但MGit做了增强,该参数可同时消除`mgit merge`中间态,且只能单独使用:`mgit merge --abort` |
| `--help` | 显示帮助。 |
【注意】
1. 若指定了配置仓库,执行该指令时会先merge配置仓库,然后根据它内部的配置表merge其余仓库。配置仓库merge完成后,配置表可能发生改变,此时可能会检测到部分仓库本地缺失,此时会自动下载并引导分支切换(提示如checkout指令所述)。
2. 执行前会校验各仓库分支是否统一。
3. 中间态:由于该操作分为两段式,第一阶段操作配置仓库,第二阶段操作子仓库,且在操作前均可能拉取最新代码,如果操作失败会进入`中间态`。根据提示解决问题后执行`mgit merge --continue`即可继续执行,也可执行`mgit merge --abort`放弃操作。
4. 该指令默认添加`--no-ff`参数,如果需要fast-forwarding,请手动添加`--ff`。
### pull
```
mgit pull [ [(--mrepo|--el-mrepo) ...] [--auto-exec] [--no-check] [--include-lock] [--help]
mgit pull --continue`
mgit pull --abort
```
| 特有参数 | 说明 |
| ---------------- | ------------------------------------------------------------ |
| `--auto-exec` | 指定该参数会跳过所有交互场景,并自动选择需要的操作执行。该参数主要用于脚本调用mgit进行自动化操作,日常RD开发不应当使用。 |
| `--mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `--el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `--no-check` | 指定该参数意味着执行前跳过仓库的状态检查,直接对指定或所有仓库执行pull操作,有一定风险,请慎重执行。 |
| `--include-lock` | 指定该参数意味着同时也操作lock仓库。 |
| `--continue` | MGit自定义参数,仅在操作多仓库过程中出现问题停止,执行状态进入中间态后可用。该参数只能单独使用,解决问题后可执行`mgit pull --continue`继续操作其余仓库。 |
| `--abort` | MGit自定义参数,仅在操作多仓库过程中出现问题停止,执行状态进入中间态后可用。该参数用于清除操作中间态,且只能单独使用:`mgit pull --abort`。 |
| `--help` | 显示帮助。 |
【注意】
1. 若指定了配置仓库,执行该指令时会先pull配置仓库,然后根据它内部的配置表pull其余仓库。
2. 执行时,会查看当前分支(如A)和远程分支(若有的话,如origin/A),只有当前分支(A)与远程分支(origin/A)出现落后或分叉的仓库才会执行指令。
3. 执行时,如果本地分支A和其跟踪的远程分支origin/A存在分叉,意味着执行pull之后会跟远程分支合并产生新节点,此时mgit会自动输入合法的log信息,并生成新提交。
4. 为了保证本地保存的远程分支(origin/A)为最新,执行前会先fetch一次。
5. 对于本地有异常的仓库,会提示执行操作,需要二次确认。
6. 执行前会校验各仓库分支是否统一。
7. 中间态:操作主仓库后如果导致配置文件冲突,会进入中间态。解决完冲突后,执行`mgit pull --continue`继续操作子仓库,或执行`mgit pull --abort`放弃本次操作。
### push
`mgit push [<git-push-option>|--auto] [(--mrepo|--el-mrepo) <repo>...] [--group-id] [--help]`
| 特有参数 | 说明 |
| ------------ | ------------------------------------------------------------ |
| `--mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `--el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `--group-id` | 指定一个group id,可将本次提交在远程做归类,若未指定则自动生成。生成的id将自动附加在url后:`mgit push --group-id = git push origin HEAD:refs/for/%topic=` |
| `--help` | 显示帮助。 |
【注意】
1. 执行时会查看当前分支(如A)和远程分支(若有的话,如origin/A),只有当前分支(A)超前远程分支(origin/A)的仓库才会执行指令。注意,为了保证本地保存到远程分支(origin/A)为最新,执行前会先fetch一次。
2. 对于本地有异常的仓库,会提示执行操作(如`pull`指令描述所示)。
3. 执行前会校验各仓库分支是否统一。
### rebase
```
mgit rebase [<git-rebase-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]
mgit rebase --continue
mgit rebase --abort
```
| 特有参数 | 说明 |
| ------------ | ------------------------------------------------------------ |
| `--mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `--el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `--continue` | MGit自定义参数,仅在操作多仓库过程中出现问题停止,执行状态进入中间态后可用。该参数只能单独使用,解决问题后可执行`mgit rebase --continue`继续操作其余仓库。 |
| `--abort` | Git原生参数,用于取消`git rebase`中间态,但MGit做了增强,该参数可同时消除`mgit rebase`中间态,且只能单独使用:`mgit rebase --abort` |
| `--help` | 显示帮助。 |
【注意】
1. 执行前会校验各仓库分支是否统一。
2. 该操作禁止使用`-i`和`--interactive`参数。
3. 中间态:同merge操作。
### reset
`mgit reset [<git-reset-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]`
| 特有参数 | 说明 |
| ------------ | ------------------------------------------------------------ |
| `--mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `--el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `--help` | 显示帮助。 |
【注意】执行前会校验各仓库分支是否统一。
### snap
`mgit snap [-m <message>|-r <snapshot_id>|-l] [-h]`
该指令用于根据当前多仓库状态生成快照,也可以从快照中恢复。
| 特有参数 | 说明 |
| --------------- | ------------------------------------------------------------ |
| `-r, --restore` | 从快照中恢复当前仓库,该操作执行前会清空所有未提交的仓库改动,请谨慎执行。用法:`mgit snap -r `。 |
| `-l, --list` | 列出所有本地存储的快照。 |
| `-m, --message` | 执行本次快照时附带的说明信息。用法:`mgit snap -m 'version x'`。 |
| `-h, --help` | 显示帮助。 |
### stash
`mgit stash [<option> <value>...] [(--mrepo|--el-mrepo) <repo>...] [--help]`
| 特有参数 | 说明 |
| ------------ | ------------------------------------------------------------ |
| `--apply` | 恢复储藏,用法:`mgit stash --apply 'stash_name'`。 |
| `--mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `--clear` | 清空所有储藏。 |
| `--el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `--push` | 添加储藏,用法:`mgit stash --push 'stash_name'`。 |
| `--pop` | 恢复储藏,用法:`mgit stash --pop 'stash_name'`。 |
| `--list` | 显示储藏列表。 |
| `--help` | 显示帮助。 |
### status
`mgit status [(-m|-e) <repo>...] [-h]`
| 特有参数 | 说明 |
| ---------------- | ------------------------------------------------------------ |
| `-m, --mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `-e, --el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `-h, --help` | 显示帮助。 |
status指令是一个比较重要的指令,MGit2.0版对其进行了加强,它的显示区域分为两部分:
- 文件状态:显示四个分类的文件状态列表(`暂存区`,`工作区`,`特殊`,`冲突`)
- 分支状态:显示当前分支和远程分支的对比状态(`超前`,`落后`,`同步`,`游离`)
【注意】
1. 文件状态显示区对所有文件进行归类,每个文件左侧会显示当前文件改动状态:`已修改`,`已添加`, `已删除`,`重命名`,`已拷贝`,`未跟踪`,`被忽略`,其中冲突区的文件会显示两个状态,如:`[添加|修改]`,左侧表示当前分支状态,右侧表示合并分支状态,即当前分支上的该文件为添加状态,合并过来的分支上的该文件为修改状态。
2. 对于干净的仓库,无输出。
### sync
`mgit sync [-a|-n|-c] [<repo>...] [-p] [-o] [-h]`
该指令用于根据配置文件同步被锁定的仓库,更新仓库和下载缺失仓库。
| 特有参数 | 说明 |
| ---------------- | ------------------------------------------------------------ |
| `-a, --all` | 对所有(包含**不被mgit管理**的)仓库操作:1.如果本地缺失则下载。2.如果本地存在且被锁定则同步到锁定状态。注意,如果需要下载仓库,需要配置仓库URL,否则跳过。 |
| `-c, --clone` | 下载一组仓库(包含**不被mgit管理**的仓库),如: `mgit sync -c repo1 repo2...`。 |
| `-n, --new-repo` | 下载配置表中指定**被mgit管理**,但本地不存在的仓库,已有仓库不做任何处理。 |
| `-o, --no-link` | 指定后,对于新下载的仓库不会将`.git`实体迁移到.mgit/source-git文件夹中管理。该参数适合开发中途接入mgit的用户使用。 |
| `-p, --pull` | 若被操作仓库本地存在的话,则进一步执行pull操作更新,配合其他指令使用,如: mgit sync -ap。 |
| `-h, --help` | 显示帮助。 |
特殊用法:
- `mgit sync`:作用同`-a`,区别是只操作**被mgit管理的仓库**。
- `mgit sync ...`:作用同`-a`,区别是只操作指定(包含**不被mgit管理**的)仓库。
操作仓库作用域总结:
| 指令 | 被mgit管理的仓库 | 不被mgit管理仓库 |
| --------------- | ---------------- | ---------------- |
| `mgit sync` | ✔ | ✘ |
| `mgit sync ...` | ✔ | ✔ |
| `mgit sync -a` | ✔ | ✔ |
| `mgit sync -c` | ✔ | ✔ |
| `mgit sync -n` | ✔ | ✘ |
【注意】
1. **同步**:是指仓库存在且被锁定(指定了`lock`字段)的情况下,将仓库切换到配置文件指定的锁定状态(`branch`,`tag`或`commit-id`),此时:
2. 需要切换分支的话,若有本地改动,则切换失败;若无本地分支,则提示是否创建并切换;若有对应分支,则直接切换
3. 需要切到具体`tag`或`commit-id`的话,若仓库不存在对应值则失败。
4. 在处理缺失仓库时,如果.mgit/source-git文件夹下有缺失的git实体,那么将直接从该实体clone出来(并将remote url修改为远程地址),否则直接从远程clone。
### tag
`mgit tag [<git-tag-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]`
| 特有参数 | 说明 |
| ------------ | ------------------------------------------------------------ |
| `--mrepo` | 指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。 |
| `--el-mrepo` | 指定需要排除执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:`--el-mrepo boxapp BBAAccount`,若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |
| `--help` | 显示帮助。 |
## 一些使用技巧
| 特有参数 | 说明 |
| -------------------------------------------- | ------------------------- |
| mgit -l | 查看当前被管理的仓库 |
| mgit info <repo1> <repo2>... #repo大小写均可 | 查看某个仓库的详细信息 |
| mgit branch --compact | 查看归并分支 |
| mgit clean | 一键清空工作区 |
| mgit status | 产看仓库分支超前/落后情况 |
| mgit push | 一键推动新分支、新提交 |
================================================
FILE: docs/use/how-to-start.md
================================================
## 快速开始
### 1、安装 MGit 工具
环境要求:
- 系统:支持 macOS、Ubuntu,暂时不支持 window-
- Ruby版本: >= 2.3.7
```ruby
$ gem install m-git
```
### 2、初始化多仓库Demo
初始化多仓库使用 `mgit init` 命令;
类似于 Git 从远程 clone 新仓库, 会将多个仓库 clone 到本地;
下面通过一个demo体验一下MGit命令:
```ruby
# 2.1 建议在一个新文件夹中拉取demo
$ mgit init -g https://github.com/baidu/m-git.git
# 2.2 体验一下mgit命令
$ mgit -l 显示所有mgit管理的仓库
$ mgit branch --compact 查看多仓库的分支
$ mgit status 产看仓库分支超前/落后情况
```
### 3、已有多仓库如何迁移到 MGit 管理
- 根据文档[配置 manifest.json](../config/manifest.md)
将要管理的仓库都配置到 manifest.json 中
- 将 manifest.json 放到一个仓库中管理
这个仓库同样会在 manifest.json 中描述,并且需要配置 "config-repo": true
这个仓库称为配置仓库,也叫做**主仓库**,其他仓库叫做**子仓库**
- 使用 `mgit init -f manifest文件路径` 命令初始化多仓库,测试 manifest.json 配置是否正常
注意这个命令不会重复拉取主仓库,只会拉取所有的子仓库到当前目录,并在当前目录创建一个.mgit
你可以在当前目录中看到每个仓库的源码,他们的路径可以通过 manifest.json 的 dest字段配置
你也可以在 .mgit/source-git/ 下看到所有仓库的.git, 这是 MGit 对所有仓库的托管
- 本地测试成功后,你可以提交主仓库中的 manifest.json,推送主仓库的变更到远端
- 通过 `mgit init -g 主仓库地址` 命令初始化多仓库
### 4、使用 MGit 管理多仓库的案例
推荐使用**同名分支原则**管理多仓库: 子仓库的分支与主仓库保持一致(子仓库单独锁定分支的情况除外)
推荐通过在主仓库中[配置 local_manifest.json](../config/manifest.md#3--local_manifest), 控制要同时操作哪些仓库
例如: 一个工程中有 a、b、c、d 、e、f、g等多个仓库, 当一个需求 A 涉及到三个仓库 a 、b、 c时
- 从 master 新建开发分支 feature_A
- 拉取主仓库到开发分支 feature_A `git checkout -b feature_A`(操作单仓库时可以直接使用git命令)
- [创建 local_manifest.json](../config/manifest.md#3--local_manifest) 在 local_manifest.json 中配置 MGit 只管理主仓库和子仓库 a 、b、 c
```ruby
{
"mgit-excluded": true,
"repositories": {
"config_repo": {
"mgit-excluded": false
},
"a": {
"mgit-excluded": false
},
"b": {
"mgit-excluded": false
},
"c": {
"mgit-excluded": false
}
}
}
```
- push 主仓库的变更到远程 (包含 local_manifest.json配置的变更)
- 拉取子仓库 a 、b、 c 的开发分支 feature_A `mgit checkout -b feature_A`
- 使用 `mgit branch --compact` 命令查看分支状态
- 在 feature_A 分支开发需求
- `mgit status` 查看多仓库状态
- `mgit add .` 添加到暂存区
- `mgit commit -m 'xxx'` 提交多仓库的变更
- `mgit push` 推送多仓库到远程
- 合并 feature_A 到 master 分支
- `mgit merge feature_A -m "xxx comment..."` 将代码 merge 回主干分支
- 删除主仓库中的 local_manifest.json 文件(如果有增、删、改仓库配置的情况,需要更新到 manifest.json)
================================================
FILE: lib/m-git/argv/opt.rb
================================================
#coding=utf-8
module MGit
class ARGV
# 单个选项, 如‘--k1=v1 --k2 v2 v3 --flag’
# '--k1=v1' key: '--k1', value: 'v1'
# '--k2 v2 v3' key: '--k2', value: ['v2', 'v3']
# '--flag' key: '--flag', value: true
class Opt
attr_reader :key
attr_accessor :value
attr_accessor :short_key # 短参数格式,如’--command’对应的‘-c’
attr_accessor :priority # 参数解析优先级
attr_accessor :info # 参数说明
# @!attribute value值类型
# :array :string :boolean
attr_accessor :value_type
def initialize(key, default:nil, short_key:nil, priority:-1, info:nil, type: :array)
raise("初始化Opt选项必须有key") if key.nil?
@key, @value, @short_key, @priority, @info = key, default, short_key, priority, info
@value_type = type
end
def empty?
value.nil? || value == '' || value == [] || value == false
end
def validate?
return false if empty?
value.is_a?(value_type)
end
end
end
end
================================================
FILE: lib/m-git/argv/opt_list.rb
================================================
#coding=utf-8
module MGit
class ARGV
# 参数对象列表
class OptList
# [Array<ARGV::Opt>] 参数对象数组
attr_accessor :opts
# attr_reader :valid_opts
def initialize(opts)
@opts = opts
end
def valid_opts
@opts.select { |e| !e.empty? }
end
# 获取某个参数对象
#
# @param key [String] 参数名,如‘--key’
#
# @return [ARGV::Opt] 参数对象,若参数未设置过则返回nil
#
def opt(key)
valid_opts.find { |e| (e.key == key || e.short_key == key) }
end
alias_method :opt_with, :opt
# 判断参数是否设置过值
#
# @return [Boolean] 参数是否设置过值
#
def did_set_opt?(key)
!opt(key).nil?
end
## all opts ###
# 返回某个注册过的参数对象
#
# @param key [String] 参数名,如‘--key’
#
# @return [ARGV::Opt] 参数对象,无论参数是否设置过,只要注册过就返回
#
def registered_opt(key)
@opts.find { |e| (e.key == key || e.short_key == key) }
end
# 判断参数是否注册过
#
# @return [Boolean] 参数是否注册过
#
def did_register_opt?(key)
!registered_opt(key).nil?
end
# 将参数根据优先级排序(逆序)后返回
#
# @return [Array<ARGV::Opt>] 包含参数对象的数组
#
def opts_ordered_by_priority
# 按照优先级进行降序排序
opts.sort_by { |e| e.priority }.reverse
end
end
end
end
================================================
FILE: lib/m-git/argv/parser.rb
================================================
module MGit
class ARGV
module Parser
# @param [Array] argv
#
# @return [String, Array, Array]
# 返回cmd 和 参数列表
# # 将参数初步分解,便于后续处理
# 如:argv = [command zzzz -u sdfsd sdf --mmmm yoo ssss qqq --test asdfa asd ad f --xxx asd as dfa --yoo="ajsdaf" --ppp -abc]
# 分解为:command, [zzzz], [[-u, sdfsd, sdf], [--mmmm, yoo, ssss, qqq], [--test, asdfa, asd, ad, f], [--xxx, asd, as, dfa], [--yoo, "ajsdaf"], [--ppp], [-a], [-b], [-c]]
#
# 初步解析参数
def self.parse(argv)
absolute_cmd = argv.join(' ')
cmd = argv.shift
pure_opts = argv.join(' ')
# 将参数初步分解,便于后续处理
# 如:zzzz -u sdfsd sdf --mmmm yoo ssss qqq --test asdfa asd ad f --xxx asd as dfa --yoo="ajsdaf" --ppp -abc
# 分解为:[[zzzz], [-u, sdfsd, sdf], [--mmmm, yoo, ssss, qqq], [--test, asdfa, asd, ad, f], [--xxx, asd, as, dfa], [--yoo, "ajsdaf"], [--ppp], [-a], [-b], [-c]]
temp = []
raw_opts = []
argv.each_with_index { |e, idx|
Foundation.help!("参数\"#{e}\"格式错误,请使用格式如:\"--long\"或\"-s\"") if (e =~ /---/) == 0
# 检查是否是带'--'或'-'的参数
if (e =~ /-/) == 0
# 回收缓存
raw_opts.push(temp) if temp.length != 0
# 清空临时缓存
temp = []
# 如果是合并的短指令,如'-al = -a + -l',则分拆后直接装入raw_opts数组
# 因为只有不需要传入值的短参数才能合并,因此不利用临时缓存读取后续参数值
if e.length > 2 && (e =~ /--/).nil? && !e.include?('=')
e.split('')[1..-1].each { |s|
raw_opts.push(["-#{s}"])
}
next
end
# 处理带‘=’的传值参数
loc = (e =~ /=/)
if loc
temp.unshift(e[(loc + 1)..-1])
temp.unshift(e[0..loc - 1])
elsif e.length != 0
temp.push(e)
end
elsif e.length != 0
temp.push(e)
end
if idx == argv.length - 1
raw_opts.push(temp)
end
}
ARGV.new(cmd, pure_opts, absolute_cmd, raw_opts)
end
end
end
end
================================================
FILE: lib/m-git/argv.rb
================================================
#coding=utf-8
module MGit
# 参数处理类
class ARGV
require 'm-git/argv/opt'
require 'm-git/argv/opt_list'
require 'm-git/argv/parser'
# 指令名,如:"mgit checkout -b branch_name"的"checkout"
attr_reader :cmd
# 所有参数,如:"mgit checkout -b branch_name"的"checkout -b branch_name"
attr_reader :pure_opts
# 完整指令,如:"mgit checkout -b branch_name"
attr_reader :absolute_cmd
# 本次传入的mgit指令中自定义的部分,如:"mgit checkout -b branch_name --mrepo boxapp BBAAccount --command test"的"[[--mrepo boxapp BBAAccount],[--command test]]"
attr_reader :raw_opts
# 本次传入的mgit指令中git透传的部分,如:"mgit checkout -b branch_name --mrepo boxapp BBAAccount"的"[[-b branch_name]]"
# has define method git_opts
# 所有已注册的参数列表
attr_reader :opt_list
def initialize(cmd, pure_opts, absolute_cmd, raw_opts)
@cmd = cmd
@pure_opts = pure_opts
@absolute_cmd = absolute_cmd
@raw_opts = raw_opts
@git_opts = []
end
def register_opts(opts)
return if opts.nil?
@opt_list = OptList.new(opts)
end
# 注册解析指令
def resolve!
@raw_opts.each { |raw_opt|
next if @opt_list.did_register_opt?(raw_opt.first)
@git_opts.push(raw_opt)
}
@raw_opts -= @git_opts
__resolve_git_opts
__resolve_raw_opts
end
# 更新指令值
# @!attribute [Array / String / true / false] value
#
def update_opt(key, value, priority:nil, info:nil)
return unless @opt_list.did_register_opt?(key)
opt = @opt_list.registered_opt(key)
case opt.value_type
when Array
opt.value = Array(value)
when String
opt.value = value.is_a?(Array) ? value.first.to_s : value.to_s
else # boolean
opt.value = value
end
opt.priority = priority if !priority.nil?
opt.info = info if info.is_a?(String)
end
# 获取某个option
def opt(key)
@opt_list.opt(key)
end
# 获取某个option的描述信息
def info(key)
return '' unless @opt_list.did_register_opt?(key)
@opt_list.registered_opt(key)&.info
end
# 获取原生git指令(非自定义的指令)
#
# @param raw [Boolean] default: true,true:用空格拼接成一个字符串返回。false:直接返回数组,如‘-k k1 k2’ -> 【'-k','k1','k2'】
#
# @return [Type] description_of_returned_object
#
def git_opts(raw: true)
return @git_opts unless raw
opts = []
@git_opts.each { |e_arr|
opts += e_arr
}
opts.join(' ')
end
# 遍历本次调用中传入过值(或有默认值)的选项,未传入值且无默认值则不遍历
def enumerate_valid_opts
@opt_list.opts_ordered_by_priority.each { |opt|
next unless @opt_list.did_set_opt?(opt.key)
yield(opt) if block_given?
}
end
# 判断一个字符串是否是option(以'--'或'-'开头)
def is_option?(opt_str)
(opt_str =~ /-/) == 0 || (opt_str =~ /--/) == 0
end
# 输出本次指令的具体值信息,调试时使用
def show_detail
@opt_list.opts.each { |opt|
puts '======='
puts "key:#{opt.key}"
puts "value:#{opt.value}"
puts "info:#{opt.info}"
puts "\n"
}
end
# 输出参数说明信息
def show_info
@opt_list.opts.each { |opt|
short_key = "#{opt.short_key}, " if !opt.short_key.nil?
puts "\n"
puts Output.blue_message("[#{short_key}#{opt.key}]")
puts "#{opt.info}"
}
end
private
def __resolve_git_opts
# 统一将值用双引号括起来,避免空格和特殊字符等引起错误
@git_opts.each { |e_arr|
next unless is_option?(e_arr.first)
e_arr.map!.with_index { |e, i|
# 如果是 -- / - 开始的option,则增加"" 避免特殊字符的错误,eg: --yoo=ajsdaf => --yoo="ajsdaf"
# 如果是 repo1 repo2 这样的option,则不作处理,eg: yoo ajsdaf => yoo ajsdaf
e = "\"#{e}\"" if !is_option?(e) && i > 0
e
}
}
end
def __resolve_raw_opts
@raw_opts.each { |raw_opt|
key = raw_opt.first
opt = @opt_list.registered_opt(key)
case opt.value_type
when :boolean
raise "参数#{key}格式错误,禁止传入参数值!(用法:#{key})" if raw_opt.count != 1
opt.value = true
when :string
raise "参数#{key}格式错误,只能传入一个参数值!(用法:#{key} xxx)" if raw_opt.count != 2
opt.value = raw_opt.last.to_s
when :array
raise "参数#{key}格式错误,至少传入一个参数值!(用法:#{key} xxx xxx ...)" if raw_opt.count < 2
opt.value = raw_opt[1..-1]
end
}
end
end
end
================================================
FILE: lib/m-git/base_command.rb
================================================
#coding=utf-8
#
require 'm-git/command_manager'
module MGit
class BaseCommand
def self.inherited(sub_klass)
CommandManager.register_command(sub_klass.cmd, sub_klass)
end
# 当前命令行的命令比如checkout / status /..
def self.cmd
name.split('::').last.downcase
end
# 引入所有自定义指令
Dir[File.join(__dir__, 'command', '*.rb')].each { |cmd|
require cmd
}
HIGH_PRIORITY_OPT_LIST = {
:help => '--help',
:help_s => '-h',
:auto_exec => '--auto-exec'
}.freeze
# 注意使用时跟指令自身设置的参数对比,避免冲突
SELECTABLE_OPT_LIST = {
:mrepo => '--mrepo',
:mrepo_s => '-m',
:exclude_mrepo => '--el-mrepo',
:exclude_mrepo_s => '-e',
:include_lock => '--include-lock',
:include_lock_s => '-i',
:continue => '--continue',
:abort => '--abort',
}.freeze
# 初始化
#
# @param argv [ARGV::Opt] 输入参数对象
#
def initialize(argv)
# 指令解析
setup_argv(argv)
process_highest_priority_option(argv)
validate(argv)
@argv = argv
end
#--- 禁止覆写 ---
# 执行主方法
def run
begin
__config_repo_filter
pre_exec
execute(@argv)
post_exec
rescue SystemExit, Interrupt
did_interrupt
end
end
#---------------
# Workspace Bridge
def all_repos(except_config:false)
Workspace.all_repos(except_config: except_config)
end
def locked_repos
Workspace.locked_repos
end
def exec_light_repos
Workspace.exec_light_repos
end
def generate_config_repo
Workspace.generate_config_repo
end
# -----------
private
def __config_repo_filter
cfg = Workspace.filter_config
cfg.include_lock = !@argv.opt(SELECTABLE_OPT_LIST[:include_lock]).nil? || include_lock_by_default
cfg.select_repos = @argv.opt(SELECTABLE_OPT_LIST[:mrepo])
cfg.exclude_repos = @argv.opt(SELECTABLE_OPT_LIST[:exclude_mrepo])
cfg.auto_exec = @argv.opt_list.did_set_opt?(HIGH_PRIORITY_OPT_LIST[:auto_exec])
end
#--- 基类调用,禁止覆写 ---
# 配置参数对象
def setup_argv(argv)
argv.register_opts(options)
argv.resolve!
argv.opt_list.opts.each do |opt|
next if opt.empty?
revise_option_value(opt)
end
end
# 处理最高优指令
def process_highest_priority_option(argv)
if argv.opt_list.did_set_opt?(HIGH_PRIORITY_OPT_LIST[:help])
usage(argv)
exit
end
end
#-------------------------------------------------------
#--- 可选覆写 ---
# 此处有pre hook,覆写时需要先调用super, 特殊指令除外
def pre_exec
# 开始计时
MGit::DurationRecorder.start
# 配置根目录
Workspace.setup_multi_repo_root
# 配置log
MGit::Loger.config(Workspace.root)
MGit::Loger.info("~~~ #{@argv.absolute_cmd} ~~~")
# 执行前置hook
HooksManager.execute_mgit_pre_hook(@argv.cmd, @argv.pure_opts)
# 解析配置文件
Workspace.setup_config
# 校验实体仓库
Workspace.setup_all_repos
end
# 此处有post hook,覆写时需要最后调用super, 特殊指令除外
def post_exec
# 执行后置hook
HooksManager.execute_mgit_post_hook(@argv.cmd, @argv.pure_opts, Workspace.exec_light_repos)
# 打点结束
duration = MGit::DurationRecorder.end
MGit::Loger.info("~~~ #{@argv.absolute_cmd}, 耗时:#{duration} s ~~~")
end
# 【子类按需覆写修改返回值】返回true表示可以支持自动执行
def enable_auto_execution
false
end
# 【子类按需覆写修改返回值】返回true表示可以支持"--mrepo"等可选选项
def enable_repo_selection
false
end
# 【子类按需覆写修改返回值】是否使默认添加选项(如‘--help’)和可选添加的选项(如‘--mrepo’)支持短指令
def enable_short_basic_option
false
end
# 【子类按需覆写修改返回值】是否添加操作lock仓库的选项(如‘--include-lock’)
def enable_lock_operation
false
end
# 【子类按需覆写修改返回值】是否自动将lock仓库加入到操作集中
def include_lock_by_default
false
end
# 【子类按需覆写修改返回值】是否添加‘--continue’参数,要在指令中自行控制中间态操作
def enable_continue_operation
false
end
# 【子类按需覆写修改返回值】是否添加‘--abort’参数
def enable_abort_operation
false
end
# 【子类按需覆写修改实现】按下ctrl+c后调用
def did_interrupt
end
# 可覆写该方法,返回该指令的描述
def self.description
end
# 可覆写该方法,返回该指令的用法
def self.usage
end
#---------------
#--- 强制覆写 ---
# 子类指令执行主方法
def execute(argv)
Foundation.help!("请覆写父类方法: \"execute(argv)\"")
end
#---------------
#--- 如果要接管某个指令(即为它添加自定义参数) ---
# --- 强制覆写(若该指令不带任何自定义参数,则无须覆写) ---
# 注册选项,覆写时注意返回"[...].concat(super)"
def options
opts = [
ARGV::Opt.new(HIGH_PRIORITY_OPT_LIST[:help],
short_key:(HIGH_PRIORITY_OPT_LIST[:help_s] if enable_short_basic_option),
info:"显示帮助。",
type: :boolean)
]
opts.push(
ARGV::Opt.new(HIGH_PRIORITY_OPT_LIST[:auto_exec],
info:'指定该参数会跳过所有交互场景,并自动选择需要的操作执行。该参数主要用于脚本调用mgit进行自动化操作,日常RD开发不应当使用。',
type: :boolean)
) if enable_auto_execution
opts.push(
ARGV::Opt.new(SELECTABLE_OPT_LIST[:mrepo],
short_key:(SELECTABLE_OPT_LIST[:mrepo_s] if enable_short_basic_option),
info:'指定需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:"--mrepo boxapp BBAAccount",若缺省则对所有仓库执行指令。'),
ARGV::Opt.new(SELECTABLE_OPT_LIST[:exclude_mrepo],
short_key:(SELECTABLE_OPT_LIST[:exclude_mrepo_s] if enable_short_basic_option),
info:'指定不需要执行该指令的仓库,可指定一个或多个,空格隔开,大小写均可,如:"--el-mrepo boxapp BBAAccount",若缺省则对所有仓库执行指令。与"--mrepo"同时指定时无效。')
) if enable_repo_selection
opts.push(
ARGV::Opt.new(SELECTABLE_OPT_LIST[:include_lock], info:'指定该参数意味着同时也操作lock仓库。',
type: :boolean)
) if enable_lock_operation
opts.push(
ARGV::Opt.new(SELECTABLE_OPT_LIST[:continue],
info:'MGit自定义参数,仅在操作多仓库过程中出现问题停止,执行状态进入中间态后可用。该参数只能单独使用,解决问题后可执行"mgit <cmd> --continue"继续操作其余仓库。',
type: :boolean)
) if enable_continue_operation
opts.push(
ARGV::Opt.new(SELECTABLE_OPT_LIST[:abort],
info:'MGit自定义参数,仅在操作多仓库过程中出现问题停止,执行状态进入中间态后可用。该参数用于清除操作中间态,且只能单独使用:"mgit <cmd> --abort"。',
type: :boolean)
) if enable_abort_operation
opts
end
# 解析参数并更新参数值到列表中
def revise_option_value(opt)
end
# --- 可选覆写(若该指令不带任何自定义参数,则无须覆写) ---
# 判断是否有必须参数漏传或数据格式不正确
def validate(argv)
end
# mgit是否输入--continue希望继续上次操作
def mgit_try_to_continue?
@argv.opt_list.did_set_opt?(SELECTABLE_OPT_LIST[:continue])
end
def mgit_try_to_abort?
@argv.opt_list.did_set_opt?(SELECTABLE_OPT_LIST[:abort])
end
# 显示指令使用信息
def usage(argv)
puts "#{Output.blue_message("[指令说明]")}\n#{self.class.description}"
puts "\n#{Output.blue_message("[指令格式]")}\n#{self.class.usage}"
argv.show_info
end
end
end
================================================
FILE: lib/m-git/command/add.rb
================================================
#coding=utf-8
module MGit
# @!scope [command] add
# follow git add
# eg: mgit add .
#
class Add < BaseCommand
# @overload
#
def execute(argv)
Workspace.check_branch_consistency
Output.puts_start_cmd
_, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, all_repos)
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0
end
# @overload
# @return [Boolean]
#
def enable_repo_selection
true
end
# @overload
#
def self.description
"将文件改动加入暂存区。"
end
# @overload
#
def self.usage
"mgit add [<git-add-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]"
end
end
end
================================================
FILE: lib/m-git/command/branch.rb
================================================
#coding=utf-8
module MGit
# @!scope [command] branch
# follow git branch
# eg: mgit branch --compact
#
class Branch < BaseCommand
OPT_LIST = {
:compact => '--compact',
}.freeze
def options
[
ARGV::Opt.new(OPT_LIST[:compact], info:"以归类的方式显示所有仓库的当前分支。", type: :boolean)
].concat(super)
end
def execute(argv)
Output.puts_start_cmd
if argv.opt(OPT_LIST[:compact])
show_compact_branches(argv)
return
end
# 无自定义参数则透传
extcute_as_common(argv)
end
# 常规执行
def extcute_as_common(argv)
error_repos = {}
all_repos.sort_by { |repo| repo.name }.each { |repo|
success, output = repo.execute_git_cmd(argv.cmd, argv.git_opts)
if success && output.length > 0
puts Output.generate_title_block(repo.name) {
output
} + "\n"
elsif !success
error_repos[repo.name] = output
end
}
if error_repos.length > 0
Workspace.show_error(error_repos)
else
Output.puts_succeed_cmd(argv.absolute_cmd)
end
end
# 以紧凑模式执行
def show_compact_branches(argv)
show_branches_for_repos(all_repos, false)
show_branches_for_repos(locked_repos, true)
Output.puts_succeed_cmd(argv.absolute_cmd)
end
# 紧凑地显示一组仓库分支
def show_branches_for_repos(repos, locked)
return if repos.nil?
list = {}
repos.sort_by { |repo| repo.name }.each { |repo|
branch = repo.status_checker.current_branch(strict_mode:false)
branch = 'HEAD游离,不在任何分支上!' if branch.nil?
list[branch] = [] if list[branch].nil?
list[branch].push(repo.name)
}
list.each { |branch, repo_names|
Output.puts_remind_block(repo_names, "以上仓库的当前分支:#{branch}#{' [锁定]' if locked}")
puts "\n"
}
end
def enable_repo_selection
true
end
def self.description
"创建、显示、删除分支。"
end
def self.usage
"mgit branch [<git-branch-option>|--compact] [(--mrepo|--el-mrepo) <repo>...] [--help]"
end
end
end
================================================
FILE: lib/m-git/command/checkout.rb
================================================
#coding=utf-8
module MGit
# @!scope [command] checkout
# follow git checkout
# eg: mgit checkout master
#
class Checkout < BaseCommand
def execute(argv)
Output.puts_start_cmd
# 优先checkout配置仓库
config_repo = generate_config_repo
checkout_config_repo(argv.cmd, argv.git_opts, config_repo)
do_repos = []
dirty_repos = []
Output.puts_processing_message("检查各仓库状态...")
Workspace.serial_enumerate_with_progress(all_repos) { |repo|
if !config_repo.nil? && repo.name == config_repo.name
next
elsif repo.status_checker.status != Repo::Status::GIT_REPO_STATUS[:dirty]
do_repos.push(repo)
else
dirty_repos.push(repo)
end
}
Output.puts_success_message("检查完成!\n")
if dirty_repos.length > 0
remind_repos = []
remind_repos.push(['有本地改动', dirty_repos.map { |e| e.name }]) if dirty_repos.length > 0
Output.interact_with_multi_selection_combined_repos(remind_repos, "以上仓库状态异常", ['a: 跳过并继续', 'b: 强制执行', 'c: 终止']) { |input|
if input == 'b'
do_repos += dirty_repos
do_repos.uniq! { |repo| repo.name }
elsif input == 'c' || input != 'a'
Output.puts_cancel_message
return
end
}
end
if do_repos.length == 0
if config_repo.nil?
Output.puts_nothing_to_do_cmd
else
Output.puts_succeed_cmd(argv.absolute_cmd)
end
else
Output.puts_processing_message("开始checkout子仓库...")
_, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, do_repos)
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0
end
end
def checkout_config_repo(cmd, opts, repo)
return if repo.nil?
if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]
remind_config_repo_fail("主仓库\"#{repo.name}\"有改动,无法执行!")
else
Output.puts_processing_message("开始checkout主仓库...")
success, output = repo.execute_git_cmd(cmd, opts)
if !success
remind_config_repo_fail("主仓库\"#{repo.name}\"执行\"#{cmd}\"失败:\n#{output}")
else
Output.puts_success_message("主仓库checkout成功!\n")
end
# 刷新配置表
Workspace.update_config { |missing_repos|
if missing_repos.length > 0
all_repos.concat(missing_repos)
# missing_repos包含新下载的和当前分支已有的新仓库,其中已有仓库包含在@all_repos内,需要去重
all_repos.uniq! { |repo| repo.name }
end
}
end
end
def remind_config_repo_fail(msg)
Output.puts_fail_message(msg)
return if Output.continue_with_user_remind?("是否继续操作其余仓库?")
Output.puts_cancel_message
exit
end
def enable_repo_selection
true
end
def self.description
"切换分支或恢复工作区改动。"
end
def self.usage
"mgit checkout [<git-checkout-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]"
end
end
end
================================================
FILE: lib/m-git/command/clean.rb
================================================
#coding=utf-8
module MGit
# @!scope [command] clean 清除所有仓库中工作区的变更
# follow git combinatorial command
# eg: git add . && git reset --hard
#
class Clean < BaseCommand
def execute(argv)
Output.puts_start_cmd
# 清除中间态
OperationProgressManager::PROGRESS_TYPE.each { |type, type_value|
if OperationProgressManager.is_in_progress?(Workspace.root, type_value)
Output.puts_processing_message("清除#{type.to_s}中间态...")
OperationProgressManager.remove_progress(Workspace.root, type_value)
Output.puts_success_message("清除成功!")
end
}
do_repos = []
all_repos.each { |repo|
do_repos.push(repo) if repo.status_checker.status != Repo::Status::GIT_REPO_STATUS[:clean]
}
if do_repos.length > 0
Workspace.check_branch_consistency
Output.puts_processing_message("正在将改动加入暂存区...")
_, error_repos1 = Workspace.execute_git_cmd_with_repos('add', '.', do_repos)
Output.puts_processing_message("正在重置...")
_, error_repos2 = Workspace.execute_git_cmd_with_repos('reset', '--hard', do_repos)
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos1.length + error_repos2.length == 0
else
Output.puts_success_message("所有仓库均无改动,无须执行。")
end
end
def validate(argv)
Foundation.help!("输入非法参数:#{argv.git_opts}。请通过\"mgit #{argv.cmd} --help\"查看用法。") if argv.git_opts.length > 0
end
def enable_repo_selection
true
end
def enable_short_basic_option
true
end
def self.description
"强制清空暂存区和工作区,相当于对指定或所有仓库执行\"git add . && git reset --hard\"操作"
end
def self.usage
"mgit clean [(-m|-e) <repo>...] [-h]"
end
end
end
================================================
FILE: lib/m-git/command/commit.rb
================================================
#coding=utf-8
module MGit
# @!scope [command] commit
# follow git commit
# eg: mgit commit -m 'Just for fun'
#
class Commit < BaseCommand
private def validate(argv)
super
# 禁用--amend
if argv.git_opts.include?('--amend')
Output.puts_fail_message("MGit不支持\"--amend\"操作,请重试。")
Output.puts_cancel_message
exit
end
end
def execute(argv)
Workspace.check_branch_consistency
Output.puts_start_cmd
do_repos = []
remind_repos = []
do_nothing_repos = []
Output.puts_processing_message("检查各仓库状态...")
Workspace.serial_enumerate_with_progress(all_repos) { |repo|
if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]
index_dirty_mask = Repo::Status::GIT_REPO_STATUS_DIRTY_ZONE[:index]
# 仅在暂存区有未提交的改动时执行
if repo.status_checker.dirty_zone & index_dirty_mask == index_dirty_mask
do_repos.push(repo)
else
remind_repos.push(repo.name)
end
else
do_nothing_repos.push(repo.name)
end
}
Output.puts_success_message("检查完成!\n")
if remind_repos.length > 0 && !Output.continue_with_interact_repos?(remind_repos, "以上仓库暂存区无可提交内容,仅存在工作区改动或未跟踪文件,若需要提交这些改动请先add到暂存区。是否跳过并继续?")
Output.puts_cancel_message
return
end
if do_repos.length != 0
if argv.git_opts.include?('-m ') || !Output.continue_with_user_remind?("未添加\"-m\"参数,请使用如[ mgit commit -m \"my log\" ]的形式提交。是否取消执行并重新输入(若确实有意执行该指令请忽略本提示)?")
# commit前调用hook
HooksManager.execute_mgit_pre_exec_hook(argv.cmd, argv.pure_opts, do_repos.map { |e| e.config })
msg = ",另有#{do_nothing_repos.length}个仓库暂存区无待提交内容,无须执行" if do_nothing_repos.length > 0
Output.puts_remind_block(do_repos.map { |repo| repo.name }, "开始commit以上仓库#{msg}...")
_, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, do_repos)
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0
else
Output.puts_cancel_message
return
end
else
Output.puts_remind_message("所有仓库均无改动,无须执行!")
end
end
def enable_repo_selection
true
end
def self.description
"将修改记录到版本库。"
end
def self.usage
"mgit commit [<git-commit-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]"
end
end
end
================================================
FILE: lib/m-git/command/config.rb
================================================
#coding=utf-8
module MGit
# @!scope [command] config 配置 .mgit/config.yml 文件信息
#
# eg: mgit config -s key 'value'
#
class Config < BaseCommand
OPT_LIST = {
:create_local => '--create-local',
:create_local_s => '-c',
:update_manifest => '--update-manifest',
:update_manifest_s => '-m',
:update_local => '--update-local',
:update_local_s => '-u',
:list => '--list',
:list_s => '-l',
:set => '--set',
:set_s => '-s'
}.freeze
def options
return [
ARGV::Opt.new(OPT_LIST[:update_manifest],
short_key:OPT_LIST[:update_manifest_s],
info:"该指令用于更新mgit所使用的配置文件,如:\"mgit config -m <new_path>/manifest.json\"。",
type: :string),
ARGV::Opt.new(OPT_LIST[:update_local],
short_key:OPT_LIST[:update_local_s],
info:"该指令用于更新mgit所使用的本地配置文件,如:\"mgit config -u <new_path>/local_manifest.json\"。",
type: :string),
ARGV::Opt.new(OPT_LIST[:create_local],
short_key:OPT_LIST[:create_local_s],
info:"在指定目录下创建本地配置文件,若目录不存在则自动创建。如执行:\"mgit config -c /a/b/c\",则生成本地配置文件:\"/a/b/c/local_manifest.json\"。如果未传入值,如:\"mgit config -c\",那么若配置仓库存在的话,会在配置仓库中创建本地配置文件。",
type: :string),
ARGV::Opt.new(OPT_LIST[:list],
short_key:OPT_LIST[:list_s],
info:"列出当前MGit所有配置,无参数,如:\"mgit config -l\"。",
type: :boolean),
ARGV::Opt.new(OPT_LIST[:set],
short_key:OPT_LIST[:set_s],
info:"对MGit进行配置,遵守格式:\"mgit config -s <key> <value>\",如:\"mgit config -s maxconcurrentcount 5\"。")
].concat(super)
end
def validate(argv)
Foundation.help!("输入非法参数:#{argv.git_opts}。请通过\"mgit #{argv.cmd} --help\"查看用法。") if argv.git_opts.length > 0
if set_kv = argv.opt(OPT_LIST[:set])
Foundation.help!("参数#{OPT_LIST[:set]}格式错误,只需传入key和value两个值!") if set_kv.value.count != 2
end
end
# --- 覆写前后hook,不需要预设操作 ---
def pre_exec
# 开始计时
MGit::DurationRecorder.start
Workspace.setup_multi_repo_root
# 配置log
MGit::Loger.config(Workspace.root)
MGit::Loger.info("~~~ #{@argv.absolute_cmd} ~~~")
end
def post_exec
# 打点结束
duration = MGit::DurationRecorder.end
MGit::Loger.info("~~~ #{@argv.absolute_cmd}, 耗时:#{duration} s ~~~")
end
# --------------------------------
def execute(argv)
argv.enumerate_valid_opts { |opt|
if opt.key == OPT_LIST[:update_manifest]
update_mgit_config(opt.value)
return
elsif opt.key == OPT_LIST[:update_local]
update_local_config(opt.value)
return
elsif opt.key == OPT_LIST[:create_local]
dir = opt.value
if opt.value.is_a?(TrueClass)
Workspace.setup_config
if Workspace.config.config_repo.nil?
Foundation.help!("未找到配置仓库,请为参数\"--create-local\"或\"-c\"指定一个具体文件夹目录并重试!")
else
dir = Workspace.config.config_repo.abs_dest(Workspace.root)
end
end
create_local_config(dir)
return
elsif opt.key == OPT_LIST[:list]
dump_config
elsif opt.key == OPT_LIST[:set]
set_config(opt.value)
end
}
end
# 更新配置表软链接
def update_mgit_config(config_path)
config = Manifest.parse(Utils.expand_path(config_path))
Utils.execute_under_dir("#{File.join(Workspace.root, Constants::PROJECT_DIR[:source_config])}") {
mgit_managed_config_link_path = File.join(Dir.pwd, Constants::CONFIG_FILE_NAME[:manifest])
mgit_managed_config_cache_path = File.join(Dir.pwd, Constants::CONFIG_FILE_NAME[:manifest_cache])
# 在.mgit/source-config文件夹下创建原始配置文件的软连接
if config.path != mgit_managed_config_link_path
Utils.link(config.path, mgit_managed_config_link_path)
end
# 将配置缓存移动到.mgit/source-config文件夹下
if config.cache_path != mgit_managed_config_cache_path
FileUtils.rm_f(mgit_managed_config_cache_path) if File.exist?(mgit_managed_config_cache_path)
FileUtils.mv(config.cache_path, Dir.pwd)
end
Output.puts_success_message("配置文件更新完毕!")
}
end
# 更新本地配置表软链接
def update_local_config(config_path)
config_path = Utils.expand_path(config_path)
Utils.execute_under_dir("#{File.join(Workspace.root, Constants::PROJECT_DIR[:source_config])}") {
mgit_managed_local_config_link_path = File.join(Dir.pwd, Constants::CONFIG_FILE_NAME[:local_manifest])
# 在.mgit/source-config文件夹下创建原始本地配置文件的软连接
if config_path != mgit_managed_local_config_link_path
Utils.link(config_path, mgit_managed_local_config_link_path)
end
Output.puts_success_message("本地配置文件更新完毕!")
}
end
# 新建本地配置表软链接
def create_local_config(dir)
path = Utils.expand_path(File.join(dir, Constants::CONFIG_FILE_NAME[:local_manifest]))
if File.exist?(path) && !Output.continue_with_user_remind?("本地配置文件\"#{path}\"已经存在,是否覆盖?")
Output.puts_cancel_message
return
end
FileUtils.mkdir_p(dir)
file = File.new(path, 'w')
if !file.nil?
file.write(Template.default_template)
file.close
end
Utils.link(path, File.join(Workspace.root, Constants::PROJECT_DIR[:source_config], Constants::CONFIG_FILE_NAME[:local_manifest]))
Output.puts_success_message("本地配置文件生成完毕:#{path}")
end
# 列出所有配置
def dump_config
begin
MGitConfig.dump_config(Workspace.root)
rescue Error => e
Foundation.help!(e.msg)
end
end
# 设置配置
def set_config(key_value_arr)
key = key_value_arr.first
value = key_value_arr.last
begin
MGitConfig.update(Workspace.root) { |config|
if MGitConfig::CONFIG_KEY.keys.include?(key.to_sym)
valid_value = MGitConfig.to_suitable_value_for_key(Workspace.root, key, value)
if !valid_value.nil?
config[key] = valid_value
else
type = MGitConfig::CONFIG_KEY[key.to_sym][:type]
Foundation.help!("#{value}不匹配类型:#{type},请重试。")
end
else
Foundation.help!("非法key值:#{key}。使用mgit config -l查看所有可配置字段。")
end
}
Output.puts_success_message("配置成功!")
rescue Error => e
Foundation.help!(e.msg)
end
end
# 允许使用短命令
def enable_short_basic_option
true
end
def self.description
"用于更新多仓库配置信息。"
end
def self.usage
"mgit config [-s <config_key> <config_value>] [-l]\nmgit config [(-m|-u) <path_to_manifest> | -c <dir_contains_local>]\nmgit config [-h]"
end
end
end
================================================
FILE: lib/m-git/command/delete.rb
================================================
#coding=utf-8
module MGit
# @!scope [command] delete 删除某个仓库的`所有`文件,包括工作区、暂存区和版本库
#
# eg: mgit delete subA
#
class Delete < BaseCommand
# --- 覆写前后hook,不需要预设操作 ---
def pre_exec
MGit::DurationRecorder.start
Workspace.setup_multi_repo_root
# 配置log
MGit::Loger.config(Workspace.root)
MGit::Loger.info("~~~ #{@argv.absolute_cmd} ~~~")
Workspace.setup_config
end
def post_exec
# 打点结束
duration = MGit::DurationRecorder.end
MGit::Loger.info("~~~ #{@argv.absolute_cmd}, 耗时:#{duration} s ~~~")
end
# --------------------------------
def execute(argv)
delete_repo_names = parse_repo_name(argv)
include_central = Workspace.config.light_repos.find do |e|
delete_repo_names.include?(e.name.downcase) && e.is_config_repo
end
Foundation.help!("禁止删除配置仓库=> #{include_central.name}") if include_central
Output.puts_start_cmd
delete_light_repos = Workspace.config.light_repos.select { |e| delete_repo_names.include?(e.name.downcase) }
extra_repo_names = delete_repo_names - delete_light_repos.map { |e| e.name.downcase}
if delete_light_repos.length > 0
error_repos = {}
delete_light_repos.each { |light_repo|
begin
git_dir = light_repo.git_store_dir(Workspace.root)
repo_dir = light_repo.abs_dest(Workspace.root)
if !Dir.exist?(git_dir) && !Dir.exist?(repo_dir)
Output.puts_remind_message("#{light_repo.name}本地不存在,已跳过。")
end
# 删除git实体
if Dir.exist?(git_dir)
Output.puts_processing_message("删除仓库#{light_repo.name}的.git实体...")
FileUtils.remove_dir(git_dir, true)
end
# 删除工作区文件
if Dir.exist?(repo_dir)
Output.puts_processing_message("删除仓库#{light_repo.name}工作区文件...")
FileUtils.remove_dir(repo_dir, true)
end
rescue => e
error_repos[light_repo.name] = e.message
end
}
if error_repos.length > 0
Workspace.show_error(error_repos)
else
Output.puts_succeed_cmd(argv.absolute_cmd)
end
end
if extra_repo_names.length > 0
Output.puts_fail_block(extra_repo_names, "以上仓库配置表中未定义,请重试!")
end
end
def parse_repo_name(argv)
return if argv.git_opts.nil?
repos = argv.git_opts.split(' ')
extra_opts = repos.select { |e| argv.is_option?(e) }
Foundation.help!("输入非法参数:#{extra_opts.join(',')}。请通过\"mgit #{argv.cmd} --help\"查看用法。") if extra_opts.length > 0
Foundation.help!("未输入查询仓库名!请使用这种形式查询:mgit info repo1 repo2 ...") if repos.length == 0
repos.map { |e| e.downcase }
end
def enable_short_basic_option
true
end
def self.description
"删除指定单个或多个仓库(包含被管理的.git文件和工程文件以及跟该.git关联的所有缓存)。"
end
def self.usage
"mgit delete <repo1> <repo2>... [-h]"
end
end
end
================================================
FILE: lib/m-git/command/fetch.rb
================================================
#coding=utf-8
module MGit
# @!scope [command] fetch
# follow git fetch
# eg: mgit fetch
#
class Fetch < BaseCommand
def execute(argv)
Output.puts_start_cmd
_, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, all_repos)
if error_repos.length == 0
Output.puts_succeed_cmd(argv.absolute_cmd)
Timer.show_time_consuming_repos
end
end
def enable_repo_selection
true
end
def self.description
"与远程仓库同步分支引用和数据对象。"
end
def self.usage
"mgit fetch [<git-fetch-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]"
end
end
end
================================================
FILE: lib/m-git/command/forall.rb
================================================
#coding=utf-8
module MGit
# @!scope [command] forall 对管理的仓库依次执行shell命令
#
# eg: mgit forall -c 'git status'
#
class Forall < BaseCommand
OPT_LIST = {
:command => '--command',
:command_s => '-c',
:concurrent => '--concurrent',
:concurrent_s => '-n',
}.freeze
def options
return [
ARGV::Opt.new(OPT_LIST[:command],
short_key:OPT_LIST[:command_s],
info:'必须参数,指定需要执行的shell命令,如:"mgit forall -c \'git status -s\'"(注意要带引号)。',
type: :string),
ARGV::Opt.new(OPT_LIST[:concurrent],
short_key:OPT_LIST[:concurrent_s],
info:'可选参数,若指定,则shell命令以多线程方式执行。',
type: :boolean)
].concat(super)
end
def validate(argv)
Foundation.help!("输入非法参数:#{argv.git_opts}。请通过\"mgit #{argv.cmd} --help\"查看用法。") if argv.git_opts.length > 0
Foundation.help!("请输入必须参数--command,示例:mgit forall -c 'git status'") if argv.opt(OPT_LIST[:command]).nil?
end
def execute(argv)
# 校验分支统一
Workspace.check_branch_consistency
Output.puts_start_cmd
for_all_cmd = argv.opt(OPT_LIST[:command]).value
use_concurrent = !argv.opt(OPT_LIST[:concurrent]).nil?
if use_concurrent
succeed_repos, error_repos = Workspace.execute_common_cmd_with_repos_concurrent(for_all_cmd, all_repos)
else
succeed_repos, error_repos = Workspace.execute_common_cmd_with_repos(for_all_cmd, all_repos)
end
no_output_repos = []
succeed_repos.each { |repo_name, output|
if output.length > 0
puts Output.generate_title_block(repo_name) { output } + "\n"
else
no_output_repos.push(repo_name)
end
}
Output.puts_remind_block(no_output_repos, "以上仓库无输出!") if no_output_repos.length > 0
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0
end
def enable_repo_selection
true
end
def enable_short_basic_option
true
end
def self.description
"对多仓库批量执行指令。"
end
def self.usage
"mgit forall -c '<instruction>' [(-m|-e) <repo>...] [-n] [-h]"
end
end
end
================================================
FILE: lib/m-git/command/info.rb
================================================
#coding=utf-8
module MGit
# @!scope 指定仓库的信息
#
class Info < BaseCommand
def execute(argv)
Output.puts_start_cmd
query_repo_names = parse_repo_name(argv)
quere_repos = (all_repos + locked_repos).select { |e| query_repo_names.include?(e.name.downcase) }
if quere_repos.length > 0
quere_repos.each { |repo|
puts Output.generate_title_block(repo.name) {
info = []
info.push(['仓库位置'.bold, ["#{repo.path}"]])
info.push(['占用磁盘大小'.bold, ["#{calculate_size(repo)}"]])
info.push(['创建时间'.bold, ["#{File.ctime(repo.path)}"]])
current_branch = repo.status_checker.current_branch(strict_mode:false)
branch_message = repo.status_checker.branch_message
info.push(['当前分支'.bold, ["#{current_branch.nil? ? '无' : current_branch}"]])
info.push(['分支状态'.bold, ["#{branch_message}"]])
info.push(['文件改动'.bold, ["#{repo.status_checker.status != Repo::Status::GIT_REPO_STATUS[:clean] ? '有本地改动,请用status指令查看细节' : '本地无修改'}"]])
info.push(['Stash状态'.bold, ["#{check_stash(repo)}"]])
Output.generate_table_combination(info) + "\n\n"
}
}
Output.puts_succeed_cmd(argv.absolute_cmd)
else
Output.puts_fail_message("未找到与输入仓库名匹配的仓库,请重试!")
end
end
def parse_repo_name(argv)
return nil if argv.git_opts.nil?
repos = argv.git_opts.split(' ')
extra_opts = repos.select { |e| argv.is_option?(e) }
Foundation.help!("输入非法参数:#{extra_opts.join(',')}。请通过\"mgit #{argv.cmd} --help\"查看用法。") if extra_opts.length > 0
Foundation.help!("未输入查询仓库名!请使用这种形式查询:mgit info repo1 repo2 ...") if repos.length == 0
repos.map { |e| e.downcase }
end
def calculate_size(repo)
success, output = repo.execute("du -sh #{repo.path} | awk '{print $1}'")
return '计算失败'.red unless success
output.chomp
end
def check_stash(repo)
success, output = repo.execute("git -C \"#{repo.path}\" stash list")
return "查询失败".red unless success
output.length > 0 ? '有内容' : '无内容'
end
def enable_short_basic_option
false
end
def self.description
"输出指定仓库的信息。"
end
def self.usage
"mgit info <repo>... [-h]"
end
end
end
================================================
FILE: lib/m-git/command/init.rb
================================================
#coding=utf-8
module MGit
# @!scope 初始化多仓库命令
#
class Init < BaseCommand
OPT_LIST = {
:git_source => '--git-source',
:git_source_s => '-g',
:config_source => '--config-source',
:config_source_s => '-f',
:branch => '--branch',
:branch_s => '-b',
:local_config => '--local-config',
:local_config_s => '-l',
:all => '--all',
:all_s => '-a'
}.freeze
def options
return [
ARGV::Opt.new(OPT_LIST[:git_source],
short_key:OPT_LIST[:git_source_s],
info:'通过包含多仓库配置表的git仓库来初始化,传入远程仓库地址,如:"mgit init -g https://someone@bitbucket.org/someone"。不可与"-f","--config-source"同时指定。',
type: :string),
ARGV::Opt.new(OPT_LIST[:config_source],
short_key:OPT_LIST[:config_source_s],
info:'通过本地的多仓库配置表来初始化,传入本地配置文件路径,如:"mgit init -f <local_config_path>/manifest.json"。不可与"-g","--git-source"同时指定。',
type: :string),
ARGV::Opt.new(OPT_LIST[:branch],
short_key:OPT_LIST[:branch_s],
info:'指定配置仓库克隆分支。',
type: :string),
ARGV::Opt.new(OPT_LIST[:local_config],
short_key:OPT_LIST[:local_config_s],
info:'指定是否自动生成本地配置文件模版。指定后会在主仓库下生成名为local_manifest.json的本地配置文件,其内容只包含主仓库信息,并指定其余仓库不纳入mgit管理。初始化时指定该参数将只下载主仓库,若同时指定了"-a"或"--all",则其余仓库也会被下载。',
type: :boolean),
ARGV::Opt.new(OPT_LIST[:all],
short_key:OPT_LIST[:all_s],
info:'指定后会下载所有在配置表中配置了远程地址的仓库,无论该仓库是否被纳入mgit管理(无论是否指定"mgit_excluded:true")。',
type: :boolean)
].concat(super)
end
def validate(argv)
Foundation.help!("输入非法参数:#{argv.git_opts}") if argv.git_opts.length > 0
git_source_opt = argv.opt(OPT_LIST[:git_source])
file_source_opt = argv.opt(OPT_LIST[:config_source])
if !git_source_opt.nil? && !file_source_opt.nil?
Foundation.help!("不能同时指定参数\"#{OPT_LIST[:git_source]}\"和\"#{OPT_LIST[:config_source]}!\"")
elsif git_source_opt.nil? && file_source_opt.nil?
Foundation.help!("缺失参数\"#{OPT_LIST[:git_source]}\"或\"#{OPT_LIST[:config_source]}!\"")
end
end
# --- 覆写前后hook,不需要预设操作 ---
def pre_exec
Output.puts_processing_message("开始初始化多仓库...")
# 开始计时
MGit::DurationRecorder.start
initial_multi_repo_root
# 配置log
MGit::Loger.config(Workspace.root)
MGit::Loger.info("~~~ #{@argv.absolute_cmd} ~~~")
end
def post_exec
Output.puts_success_message("多仓库初始化成功!")
# 打点结束
duration = MGit::DurationRecorder.end
MGit::Loger.info("~~~ #{@argv.absolute_cmd}, 耗时:#{duration} s ~~~")
end
# --------------------------------
def execute(argv)
init_dir
begin
git_url_opt = argv.opt(OPT_LIST[:git_source])
local_url_opt = argv.opt(OPT_LIST[:config_source])
clone_all = argv.opt_list.did_set_opt?(OPT_LIST[:all])
if !git_url_opt.nil?
git_url = git_url_opt.value
branch = argv.opt(OPT_LIST[:branch]).value if argv.opt_list.did_set_opt?(OPT_LIST[:branch])
use_local = argv.opt_list.did_set_opt?(OPT_LIST[:local_config])
clone_with_git_url(git_url, branch, use_local, clone_all)
elsif !local_url_opt.nil?
clone_with_local_config(local_url_opt.value, clone_all)
end
rescue Interrupt => e
terminate!(e.message)
end
end
def init_dir
Constants::PROJECT_DIR.each { |key, relative_dir|
abs_dir = File.join(Workspace.root, relative_dir)
FileUtils.mkdir_p(abs_dir)
if key == :hooks
setup_hooks(File.join(abs_dir, Constants::HOOK_NAME[:pre_hook]),
File.join(abs_dir, Constants::HOOK_NAME[:post_hook]),
File.join(abs_dir, Constants::HOOK_NAME[:manifest_hook]),
File.join(abs_dir, Constants::HOOK_NAME[:post_download_hook]))
end
}
end
def write_content(path, content)
file = File.new(path, 'w')
if !file.nil?
file.write(content)
file.close
end
end
def setup_hooks(pre_hook_path, post_hook_path, manifest_hook_path, post_download_hook)
write_content(pre_hook_path, Template::PRE_CUSTOMIZED_HOOK_TEMPLATE)
write_content(post_hook_path, Template::POST_CUSTOMIZED_HOOK_TEMPLATE)
write_content(manifest_hook_path, Template::MANIFEST_HOOK_TEMPLATE)
write_content(post_download_hook, Template::POST_DOWNLOAD_HOOK_TEMPLATE)
end
def initial_multi_repo_root
if exist_root = Workspace.multi_repo_root_path
Foundation.help!("当前已在多仓库目录下,请勿重复初始化!\n`#{exist_root}`")
end
@origin_root = Dir.pwd
tmp_root = Utils.generate_init_cache_path(@origin_root)
FileUtils.mkdir_p(tmp_root)
Workspace.setup_multi_repo_root(tmp_root)
end
def setup_local_config(path, config_repo, use_local)
if use_local
content = Template.local_config_template(config_repo)
else
content = Template.default_template
end
write_content(path, content)
Utils.link(path, File.join(Workspace.root, Constants::PROJECT_DIR[:source_config], Constants::CONFIG_FILE_NAME[:local_manifest]))
end
def clone_with_git_url(git_url, branch, use_local, clone_all)
# 先将主仓库clone到mgit root目录下
central_repo_temp_path = File.join(Workspace.root, Constants::CENTRAL_REPO)
Output.puts_processing_message("正在克隆主仓库...")
Utils.execute_shell_cmd("git clone -b #{branch.nil? ? 'master' : branch} -- #{git_url} #{central_repo_temp_path}") { |stdout, stderr, status|
if status.success?
# 获取主仓库中的配置文件
begin
config = Manifest.parse(central_repo_temp_path, strict_mode:false)
rescue Error => e
terminate!(e.msg)
end
central_light_repo = config.config_repo
terminate!("配置文件中未找到主仓库配置,请添加后重试!") if central_light_repo.nil?
terminate!("配置文件中主仓库url与传入的url不一致, 请处理后重试!") if git_url != central_light_repo.url
central_repo_dest_path = central_light_repo.abs_dest(Workspace.root)
# 如果不是子目录则删除已有
if Dir.exist?(central_repo_dest_path) && !central_repo_temp_path.include?(central_repo_dest_path)
FileUtils.remove_dir(central_repo_dest_path, true)
end
FileUtils.mkdir_p(central_repo_dest_path)
mv_cmd = "mv #{central_repo_temp_path + '/{*,.[^.]*}'} #{central_repo_dest_path}"
`#{mv_cmd}`
FileUtils.rm_rf(central_repo_temp_path)
# 链接.git实体
Utils.link_git(central_repo_dest_path, central_light_repo.git_store_dir(Workspace.root))
Output.puts_success_message("主仓库克隆完成!")
# 链接本地配置文件
setup_local_config(File.join(central_repo_dest_path, Constants::CONFIG_FILE_NAME[:local_manifest]), central_light_repo.name, use_local)
# 由于更新了名字和可能的位置移动,重新解析配置文件
begin
config = Manifest.parse(central_repo_dest_path, strict_mode:false)
rescue Error => e
terminate!(e.msg)
end
# clone其余仓库
clone_sub_repos(config.repo_list(exclusion:[config.config_repo.name], all:clone_all), branch)
finish_init(config)
else
terminate!("主仓库克隆失败,初始化停止,请重试:\n#{stderr}")
end
}
end
def clone_with_local_config(config_path, clone_all)
terminate!("指定路径\"#{config_path}\"文件不存在!") if !File.exist?(config_path)
begin
config = Manifest.parse(Utils.expand_path(config_path), strict_mode:false)
rescue Error => e
terminate!(e.msg)
end
if !config.config_repo.nil?
clone_list = config.repo_list(exclusion: [config.config_repo.name], all:clone_all)
else
clone_list = config.repo_list(all:clone_all)
end
clone_sub_repos(clone_list, 'master')
finish_init(config)
end
def clone_sub_repos(repo_list, default_branch)
if repo_list.length != 0
Output.puts_processing_message("正在克隆子仓库...")
try_repos = repo_list
retry_repos = []
error_repos = {}
mutex = Mutex.new
mutex_progress = Mutex.new
task_count = 0
total_try_count = 4
total_retry_count = total_try_count - 1
# 保证失败仓库有3次重新下载的机会
total_try_count.times { |try_count|
Workspace.concurrent_enumerate(try_repos) { |light_repo|
# 如果mainfest中指定了分支,就替换default branch
branch = light_repo.branch ? light_repo.branch : default_branch
Utils.execute_shell_cmd(light_repo.clone_url(Workspace.root, clone_branch:branch)) { |stdout, stderr, status|
repo_dir = light_repo.abs_dest(Workspace.root)
if status.success?
Utils.link_git(repo_dir, light_repo.git_store_dir(Workspace.root))
mutex_progress.lock
task_count += 1
Output.puts_success_message("(#{task_count}/#{try_repos.length}) \"#{light_repo.name}\"克隆完成!")
mutex_progress.unlock
else
Output.puts_remind_message("\"#{light_repo.name}\"克隆失败,已加入重试队列。") if try_count < total_retry_count
mutex.lock
retry_repos.push(light_repo)
error_repos[light_repo.name] = stderr
FileUtils.remove_dir(repo_dir, true) if Dir.exist?(repo_dir)
mutex.unlock
end
}
}
if retry_repos.length == 0 || try_count >= total_retry_count
break
else
Output.puts_processing_block(error_repos.keys, "以上仓库克隆失败,开始第#{try_count + 1}次重试(最多#{total_retry_count}次)...")
try_repos = retry_repos
retry_repos = []
error_repos = {}
task_count = 0
end
}
if error_repos.length > 0
Workspace.show_error(error_repos)
terminate!("初始化停止,请重试!")
end
end
end
def link_config(config_path, config_cache_path, root)
Utils.execute_under_dir("#{File.join(root, Constants::PROJECT_DIR[:source_config])}") {
# 在.mgit/source-config文件夹下创建原始配置文件的软连接
config_link_path = File.join(Dir.pwd, Constants::CONFIG_FILE_NAME[:manifest])
Utils.link(config_path, config_link_path) if config_path != config_link_path
# 将配置缓存移动到.mgit/source-config文件夹下
FileUtils.mv(config_cache_path, Dir.pwd) if File.dirname(config_cache_path) != Dir.pwd
}
end
def terminate!(msg)
Output.puts_fail_message(msg)
Output.puts_processing_message("删除缓存...")
FileUtils.remove_dir(Workspace.root, true) if Dir.exist?(Workspace.root)
Output.puts_success_message("删除完成!")
exit
end
def move_project_to_root
Dir.foreach(Workspace.root) { |item|
if item != '.' && item != '..' && item != '.DS_Store'
FileUtils.mv(File.join(Workspace.root, item), @origin_root)
end
}
FileUtils.remove_dir(Workspace.root, true) if Dir.exist?(Workspace.root)
end
def finish_init(config)
move_project_to_root
config_path, config_cache_path = config.path.sub(Workspace.root, @origin_root), config.cache_path.sub(Workspace.root, @origin_root)
link_config(config_path, config_cache_path, @origin_root)
end
def enable_short_basic_option
true
end
def self.description
"初始化多仓库目录。"
end
def self.usage
"mgit init (-f <path> | -g <url> [-b <branch>] [-l]) [-a]"
end
end
end
================================================
FILE: lib/m-git/command/log.rb
================================================
#coding=utf-8
module MGit
# @!scope 查询多仓库的日志
#
class Log < BaseCommand
OPT_LIST = {
:number => '--number',
:number_s => '-n'
}.freeze
def options
return [
ARGV::Opt.new(OPT_LIST[:number], short_key:OPT_LIST[:number_s], default:500, info:"指定需要显示的提交log个数,默认500。", type: :string)
].concat(super)
end
def revise_option_value(opt)
opt.value = Integer(opt.value) if opt.key == OPT_LIST[:number]
end
def execute(argv)
repo_name = parse_repo_name(argv)
repo = all_repos.find { |e| e.name.downcase == repo_name.downcase }
number = argv.opt(OPT_LIST[:number]).value
if repo.nil?
Output.puts_fail_message("未找到与输入仓库名\"#{repo_name}\"匹配的仓库,请重试!") && return
return
end
# print(Output.processing_message("正在提取#{repo.name}最新的#{number}条log信息..."))
success, output = repo.execute_git_cmd(argv.cmd, "-n #{number}")
if success
if output.length > 0
Output.puts_in_pager(output.gsub(/commit.*/) { |s| Output.yellow_message(s) })
# print("\r")
else
Output.puts_remind_message("无提交记录")
end
else
Output.puts_fail_message("执行失败:#{output}")
end
end
def parse_repo_name(argv)
return nil if argv.git_opts.nil?
repo = argv.git_opts.split(' ')
extra_opts = repo.select { |e| argv.is_option?(e) }
Foundation.help!("输入非法参数:#{extra_opts.join(',')}。请通过\"mgit #{argv.cmd} --help\"查看用法。") if extra_opts.length > 0
Foundation.help!("未输入查询仓库名!请使用这种形式查询:mgit log some_repo") if repo.length == 0
Foundation.help!("仅允许查询一个仓库!") if repo.length > 1
repo.first
end
def is_integer?(string)
true if Integer(string) rescue false
end
def enable_short_basic_option
true
end
def self.description
"输出指定(单个)仓库的提交历史。"
end
def self.usage
"mgit log <repo> [-n] [-h]"
end
end
end
================================================
FILE: lib/m-git/command/merge.rb
================================================
#coding=utf-8
module MGit
# @!scope 类似 git merge
#
class Merge < BaseCommand
PROGRESS_STAGE = {
:new_start => 0,
:did_pull_config => 1,
:did_refresh_config => 2,
:did_pull_sub => 3
}.freeze
PROGRESS_STAGE_KEY = 'progress_stage'
PROGRESS_AUTO = 'auto_exec'
OPT_LIST = {
:pull => '--pull'
}.freeze
def options
return [
ARGV::Opt.new(OPT_LIST[:pull], info:'可选参数,指定后在合并仓库前会自动拉取远程分支更新代码,否则会有交互式询问。如:"mgit merge --pull"。', type: :boolean)
].concat(super)
end
def __progress_type
OperationProgressManager::PROGRESS_TYPE[:merge]
end
def execute(argv)
return if do_abort(argv)
Workspace.check_branch_consistency
Output.puts_start_cmd
config_repo = generate_config_repo
if mgit_try_to_continue?
# 不处于中间态禁止执行
Foundation.help!("当前并不处于操作中间态,无法进行continue操作!") if !OperationProgressManager.is_in_progress?(Workspace.root, __progress_type)
# 读取指令缓存失败禁止执行
context, _ = OperationProgressManager.load_context(Workspace.root, __progress_type)
Foundation.help!("缓存指令读取失败,continue无法继续进行,请重新执行完整指令。") if context.nil? || !context.validate?
# 分支不匹配禁止执行
Foundation.help!("当前主仓库所在分支跟上次操作时所在分支(#{context.branch})不一致,请切换后重试。") if config_repo.status_checker.current_branch(use_cache:true) != context.branch
if !context.repos.nil?
Output.puts_processing_message("加载上次即将操作的子仓库...")
Workspace.update_all_repos(context.repos)
end
cmd = context.cmd
opts = context.opts
config_error = continue_execute(cmd, opts, config_repo, context.other[PROGRESS_STAGE_KEY], context.other[PROGRESS_AUTO])
if config_error
Output.puts_fail_block([config_repo.name], "主仓库操作失败:#{config_error}")
return
end
else
# 处于中间态则提示
if OperationProgressManager.is_in_progress?(Workspace.root, __progress_type)
if Output.continue_with_user_remind?("当前处于操作中间态,建议取消操作并执行\"mgit merge --continue\"继续操作未完成仓库。\n 继续执行将清除中间态并重新操作所有仓库,是否取消?")
Output.puts_cancel_message
return
end
end
cmd = argv.cmd
opts = argv.git_opts
if !opts.include?('--continue') && !opts.include?('--abort')
# 自动添加--no-ff
if !opts.include?('--no-ff') && !opts.include?('--ff') && !opts.include?('--ff-only') && !opts.include?('--squash')
opts += ' --no-ff'
end
# 检测提示
if !opts.include?('--no-commit') &&
!opts.include?('--commit') &&
!opts.include?('-m ') &&
Output.continue_with_user_remind?("未添加\"-m\"或\"--no-commit\",建议使用:\n 1. mgit merge <branch> -m \"xxx comment...\"\n 2. 或 mgit merge <branch> --no-commit\n 更新本次提交的comment,是否取消并重新输入?")
Output.puts_cancel_message
return
end
end
# 优先操作配置仓库
config_error = merge_config_repo(cmd, opts, config_repo, argv.opt_list.did_set_opt?(OPT_LIST[:pull]))
if config_error
Output.puts_fail_block([config_repo.name], "主仓库操作失败:#{config_error}")
return
end
end
do_repos = []
dirty_repos = []
detached_repos = []
no_tracking_repos = []
Output.puts_processing_message("检查各仓库状态...")
Workspace.serial_enumerate_with_progress(all_repos) { |repo|
next if !config_repo.nil? && repo.name == config_repo.name
status = repo.status_checker.status
branch_status = repo.status_checker.branch_status
if status == Repo::Status::GIT_REPO_STATUS[:clean] &&
branch_status != Repo::Status::GIT_BRANCH_STATUS[:detached] &&
branch_status != Repo::Status::GIT_BRANCH_STATUS[:no_tracking]
do_repos.push(repo)
else
if status == Repo::Status::GIT_REPO_STATUS[:dirty]
dirty_repos.push(repo)
end
if branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]
detached_repos.push(repo)
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]
no_tracking_repos.push(repo)
end
end
}
Output.puts_success_message("检查完成!\n")
if dirty_repos.length > 0 || no_tracking_repos.length > 0 || detached_repos.length > 0
remind_repos = []
remind_repos.push(['有本地改动', dirty_repos.map { |e| e.name }])
remind_repos.push(['未追踪远程分支(建议:mgit branch -u origin/<branch>)', no_tracking_repos.map { |e| e.name }]) if no_tracking_repos.length > 0
remind_repos.push(['HEAD游离,当前不在任何分支上', detached_repos.map { |e| e.name }]) if detached_repos.length > 0
Output.interact_with_multi_selection_combined_repos(remind_repos, "以上仓库状态异常", ['a: 跳过并继续', 'b: 强制执行', 'c: 终止']) { |input|
if input == 'b'
do_repos += dirty_repos
do_repos += detached_repos
do_repos += no_tracking_repos
do_repos.uniq! { |repo| repo.name }
elsif input == 'c' || input != 'a'
Output.puts_cancel_message
return
end
}
end
error_repos = {}
if do_repos.length == 0
Output.puts_remind_message("没有仓库需要执行merge指令!") if config_repo.nil?
else
Output.puts_processing_message("开始merge子仓库...")
_, error_repos = Workspace.execute_git_cmd_with_repos(cmd, opts, do_repos)
end
if config_error.nil? && error_repos.length == 0
Output.puts_succeed_cmd("#{cmd} #{opts}")
end
# 清除中间态
OperationProgressManager.remove_progress(Workspace.root, __progress_type)
end
# 合并主仓库
#
# @param cmd [String] 合并指令
#
# @param opts [String] 合并参数
#
# @param repo [Repo] 配置仓库对象
#
# @param exec_repos [Array<Repo>] 本次操作的所有仓库(含配置仓库)
#
def merge_config_repo(cmd, opts, repo, auto_update)
if !repo.nil?
branch_status = repo.status_checker.branch_status
if branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]
remind_config_repo_fail("主仓库\"#{repo.name}\"HEAD游离,当前不在任何分支上,无法执行!")
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]
remind_config_repo_fail("主仓库\"#{repo.name}\"未跟踪对应远程分支,无法执行!(需要执行'mgit branch -u origin/<branch>')")
elsif repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]
remind_config_repo_fail("主仓库\"#{repo.name}\"有改动,无法执行\"#{cmd}\"!")
else
return continue_execute(cmd, opts, repo, PROGRESS_STAGE[:new_start], auto_update)
end
end
end
def continue_execute(cmd, opts, repo, check_point, auto_update)
# 现场信息
exec_subrepos = all_repos(except_config:true)
is_all = Workspace.is_all_exec_sub_repos?(exec_subrepos)
context = OperationProgressContext.new(__progress_type)
context.cmd = cmd
context.opts = opts
context.repos = is_all ? nil : exec_subrepos.map { |e| e.name } # nil表示操作所有子仓库
context.branch = repo.status_checker.current_branch(use_cache:true)
# 更新主仓库
update_config_repo(repo, context, auto_update) if check_point < PROGRESS_STAGE[:did_pull_config]
if check_point < PROGRESS_STAGE[:did_refresh_config]
# 操作主仓库
config_error = exec_config_repo(repo, cmd, opts)
return config_error if config_error
# 更新配置表
refresh_config(repo, context, auto_update)
end
if check_point < PROGRESS_STAGE[:did_pull_sub]
# 如果本次操作所有子仓库,则再次获取所有子仓库(因为配置表可能已经更新,子仓库列表也有更新,此处获取的仓库包含:已有的子仓库 + 合并后新下载仓库 + 从缓存弹出的仓库)
exec_subrepos = all_repos(except_config:true) if is_all
# 更新子仓库
update_subrepos(exec_subrepos, context, auto_update)
end
config_error
end
def update_config_repo(repo, context, auto)
if auto || Output.continue_with_user_remind?("即将合并主仓库,是否先拉取远程代码更新?")
Output.puts_processing_message("正在更新主仓库...")
success, output = repo.execute_git_cmd('pull', '')
if !success
context.other = {
PROGRESS_STAGE_KEY => PROGRESS_STAGE[:did_pull_config],
PROGRESS_AUTO => auto
}
OperationProgressManager.trap_into_progress(Workspace.root, context)
show_progress_error("主仓库更新失败", "#{output}")
else
Output.puts_success_message("更新成功!\n")
end
end
end
def exec_config_repo(repo, cmd, opts)
error = nil
Output.puts_processing_message("开始操作主仓库...")
success, output = repo.execute_git_cmd(cmd, opts)
if success
Output.puts_success_message("操作成功!\n")
else
Output.puts_fail_message("操作失败!\n")
error = output
end
return error
end
# 刷新配置表
def refresh_config(repo, context, auto)
begin
Workspace.update_config(strict_mode:false) { |missing_repos|
if missing_repos.length > 0
# 这里分支引导仅根据主仓库来进行,如果使用all_repos来作为引导
# 基准,可能不准确(因为all_repos可能包含merge分支已有的本地
# 仓库,而这些仓库所在分支可能五花八门,数量也可能多于处于正确
# 分支的仓库)。
success_missing_repos = Workspace.guide_to_checkout_branch(missing_repos, [repo], append_message:"拒绝该操作本次执行将忽略以上仓库")
all_repos.concat(success_missing_repos)
# success_missing_repos包含新下载的和当前分支已有的新仓库,其中已有仓库包含在@all_repos内,需要去重
all_repos.uniq! { |repo| repo.name }
end
}
refresh_context(context)
rescue Error => e
if e.type == MGIT_ERROR_TYPE[:config_generate_error]
context.other = {
PROGRESS_STAGE_KEY => PROGRESS_STAGE[:did_refresh_config],
PROGRESS_AUTO => auto
}
OperationProgressManager.trap_into_progress(Workspace.root, context)
show_progress_error("配置表生成失败", "#{e.msg}")
end
end
end
def update_subrepos(subrepos, context, auto)
if auto || Output.continue_with_user_remind?("即将合并子仓库,是否先拉取远程代码更新?")
Output.puts_processing_message("正在更新子仓库...")
_, error_repos = Workspace.execute_git_cmd_with_repos('pull', '', subrepos)
if error_repos.length > 0
context.other = {
PROGRESS_STAGE_KEY => PROGRESS_STAGE[:did_pull_sub],
PROGRESS_AUTO => auto
}
OperationProgressManager.trap_into_progress(Workspace.root, context)
show_progress_error("子仓库更新失败", "见上述输出")
else
Output.puts_success_message("更新成功!\n")
end
end
end
def refresh_context(context)
exec_subrepos = all_repos(except_config:true)
is_all = Workspace.is_all_exec_sub_repos?(exec_subrepos)
context.repos = is_all ? nil : exec_subrepos.map { |e| e.name } # nil表示操作所有子仓库
end
def remind_config_repo_fail(msg)
Output.puts_fail_message(msg)
if Output.continue_with_user_remind?("是否继续操作其余仓库?")
return
else
Output.puts_cancel_message
exit
end
end
def do_abort(argv)
if argv.git_opts.include?('--abort')
Output.puts_start_cmd
OperationProgressManager.remove_progress(Workspace.root, __progress_type)
do_repos = all_repos.select { |repo| repo.status_checker.is_in_merge_progress? }
if do_repos.length > 0
append_message = ",另有#{all_repos.length - do_repos.length}个仓库无须操作" if do_repos.length < all_repos.length
Output.puts_processing_block(do_repos.map { |e| e.name }, "开始操作以上仓库#{append_message}...")
_, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, do_repos)
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0
else
Output.puts_success_message("没有仓库需要操作!")
end
return true
else
return false
end
end
def show_progress_error(summary, detail)
error = "#{summary} 已进入操作中间态。
原因:
#{detail}
可选:
- 使用\"mgit merge --continue\"继续合并。
- 使用\"mgit merge --abort\"取消合并。"
Foundation.help!(error, title:'暂停')
end
def enable_repo_selection
return true
end
def enable_continue_operation
return true
end
def self.description
return "合并两个或多个开发历史。"
end
def self.usage
return "mgit merge [<git-merge-option>] [--pull] [(--mrepo|--el-mrepo) <repo>...] [--help]\nmgit merge --continue\nmgit merge --abort"
end
# 判断是否需要fast forward
# def is_fast_forward?(argv, repos)
# git_opts = argv.git_opts(raw:false)
# git_opts.each { |opt_arr|
# if !argv.is_option?(opt_arr.first)
# repos.each { |repo|
# opt_arr.each { |branch|
# if repo.status_checker.is_ancestor_of_branch?(branch)
# return true
# end
# }
# }
# end
# }
# return false
# end
end
end
================================================
FILE: lib/m-git/command/pull.rb
================================================
#coding=utf-8
module MGit
# @!scope 类似 git pull
#
class Pull < BaseCommand
OPT_LIST = {
:no_check => '--no-check',
}.freeze
def options
[
ARGV::Opt.new(OPT_LIST[:no_check], info:'指定该参数意味着执行前跳过仓库的状态检查,直接对指定或所有仓库执行pull操作,有一定风险,请慎重执行。', type: :boolean),
].concat(super)
end
def __progress_type
OperationProgressManager::PROGRESS_TYPE[:pull]
end
def execute(argv)
if argv.opt(OPT_LIST[:no_check])
simple_pull(argv)
return
end
verbose_pull(argv)
end
def verbose_pull(argv)
return if do_abort(argv)
Output.puts_start_cmd
# 获取远程仓库当前分支信息
Workspace.pre_fetch
config_repo = generate_config_repo
if mgit_try_to_continue?
# 不处于中间态禁止执行
Foundation.help!("当前并不处于操作中间态,无法进行continue操作!") if !OperationProgressManager.is_in_progress?(Workspace.root, __progress_type)
# 读取指令缓存失败禁止执行
context, _ = OperationProgressManager.load_context(Workspace.root, __progress_type)
Foundation.help!("缓存指令读取失败,continue无法继续进行,请重新执行完整指令。") if context.nil? || !context.validate?
# 分支不匹配禁止执行
Foundation.help!("当前主仓库所在分支跟上次操作时所在分支(#{context.branch})不一致,请切换后重试。") if config_repo.status_checker.current_branch(use_cache:true) != context.branch
if !context.repos.nil?
Output.puts_processing_message("加载上次即将操作的子仓库...")
Workspace.update_all_repos(context.repos)
end
Output.puts_success_message("已跳过主仓库。")
else
# 处于中间态则提示
if OperationProgressManager.is_in_progress?(Workspace.root, __progress_type)
if Output.continue_with_user_remind?("当前处于操作中间态,建议取消操作并执行\"mgit pull --continue\"继续操作子仓库。\n 继续执行将清除中间态并重新操作所有仓库,是否取消?")
Output.puts_cancel_message
return
end
end
# 优先pull配置仓库
config_error = pull_config_repo(argv.cmd, argv.git_opts, config_repo)
if config_error
Output.puts_fail_block([config_repo.name], "主仓库操作失败:#{config_error}")
return
end
end
do_repos = []
diverged_repos = []
no_remote_repos = []
no_tracking_repos = []
dirty_repos = []
detached_repos = []
remote_inconsist_repos = []
do_nothing_repos = []
Output.puts_processing_message("检查各仓库状态...")
Workspace.serial_enumerate_with_progress(all_repos) { |repo|
next if !config_repo.nil? && repo.name == config_repo.name
Timer.start(repo.name)
status = repo.status_checker.status
branch_status = repo.status_checker.branch_status
if branch_status == Repo::Status::GIT_BRANCH_STATUS[:up_to_date] || branch_status == Repo::Status::GIT_BRANCH_STATUS[:ahead]
# 领先和最新的仓库均不操作
do_nothing_repos.push(repo)
else
url_consist = repo.url_consist?
is_dirty = status == Repo::Status::GIT_REPO_STATUS[:dirty]
dirty_repos.push(repo) if is_dirty
remote_inconsist_repos.push(repo) if !url_consist
# 仅有分叉或落后,且工作区干净的仓库直接加入到操作集
if branch_status == Repo::Status::GIT_BRANCH_STATUS[:diverged] && !is_dirty && url_consist
do_repos.push(repo)
diverged_repos.push(repo.name)
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:behind] && !is_dirty && url_consist
do_repos.push(repo)
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_remote]
no_remote_repos.push(repo)
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]
no_tracking_repos.push(repo)
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]
detached_repos.push(repo)
end
end
Timer.stop(repo.name)
}
Output.puts_success_message("检查完成!\n")
if Workspace.filter_config.auto_exec
do_repos += dirty_repos
do_repos.uniq! { |repo| repo.name }
elsif no_remote_repos.length > 0 ||
dirty_repos.length > 0 ||
detached_repos.length > 0 ||
no_tracking_repos.length > 0 ||
remote_inconsist_repos.length > 0
remind_repos = []
remind_repos.push(['远程分支不存在', no_remote_repos.map { |e| e.name }]) if no_remote_repos.length > 0
remind_repos.push(['未追踪远程分支(建议:mgit branch -u origin/<branch>)', no_tracking_repos.map { |e| e.name }]) if no_tracking_repos.length > 0
remind_repos.push(['有本地改动', dirty_repos.map { |e| e.name }]) if dirty_repos.length > 0
remind_repos.push(['HEAD游离,当前不在任何分支上', detached_repos.map { |e| e.name }]) if detached_repos.length > 0
remind_repos.push(['实际url与配置不一致', remote_inconsist_repos.map { |e| e.name }]) if remote_inconsist_repos.length > 0
Output.interact_with_multi_selection_combined_repos(remind_repos, "以上仓库状态异常", ['a: 跳过并继续', 'b: 强制执行', 'c: 终止']) { |input|
if input == 'b'
do_repos += dirty_repos
do_repos += detached_repos
do_repos += no_remote_repos
do_repos += no_tracking_repos
do_repos += remote_inconsist_repos
do_repos.uniq! { |repo| repo.name }
elsif input == 'c' || input != 'a'
Output.puts_cancel_message
return
end
}
end
if do_repos.length != 0
error_repos = []
# 如果不带任何参数,则将pull分解为fetch+merge执行, fetch已经执行,此处执行merge。带参数则透传。
if argv.git_opts.length == 0
# 排除HEAD游离,无远程分支,未追踪远程分支的仓库,这三种仓库是无法强制执行git pull的(但是可以执行如:git pull origin master,因此透传不做此校验)
skip_repos = do_repos.select { |repo|
branch_status = repo.status_checker.branch_status
branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_remote] ||
branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking] ||
branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]
}
if skip_repos.length > 0
Output.puts_remind_block(skip_repos.map { |e| e.name }, "以上仓库无法强制执行,已跳过。")
do_repos -= skip_repos
if do_repos.length == 0
Output.puts_success_message("仓库均为最新,无须执行!")
return
end
end
count_msg = ",另有#{do_nothing_repos.length}个仓库无须执行" if do_nothing_repos.length > 0
Output.puts_remind_block(do_repos.map { |repo| repo.name }, "开始为以上仓库合并远程分支#{count_msg}...")
_, error_repos = Workspace.execute_git_cmd_with_repos('', '', do_repos) { |repo|
msg = nil
branch = repo.status_checker.current_branch(strict_mode:false, use_cache:true)
tracking_branch = repo.status_checker.tracking_branch(branch, use_cache:true)
# 如果产生分叉,为生成的新节点提供log
msg = "-m \"【Merge】【0.0.0】【#{branch}】合并远程分支'#{tracking_branch}'。\"" if diverged_repos.include?(repo.name)
["merge", "#{tracking_branch} #{msg}"]
}
else
count_msg = ",另有#{do_nothing_repos.length}个仓库无须执行" if do_nothing_repos.length > 0
Output.puts_remind_block(do_repos.map { |repo| repo.name }, "开始pull以上仓库#{count_msg}...")
_, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, do_repos)
end
if config_error.nil? && error_repos.length == 0
Output.puts_succeed_cmd(argv.absolute_cmd)
Timer.show_time_consuming_repos
end
else
Output.puts_success_message("仓库均为最新,无须执行!")
end
# 清除中间态
OperationProgressManager.remove_progress(Workspace.root, __progress_type)
end
def simple_pull(argv)
Output.puts_start_cmd
# 优先pull配置仓库
config_repo = generate_config_repo
config_error = pull_config_repo(argv.cmd, argv.git_opts, config_repo)
Output.puts_processing_message("开始pull子仓库...")
_, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, all_repos)
if config_error.nil? && error_repos.length == 0
Output.puts_succeed_cmd(argv.absolute_cmd)
elsif !config_error.nil?
Output.puts_fail_block([config_repo.name], "主仓库操作失败:#{config_error}")
end
# 情况中间态
OperationProgressManager.remove_progress(Workspace.root, __progress_type)
end
def pull_config_repo(cmd, opts, repo)
if !repo.nil?
branch_status = repo.status_checker.branch_status
if branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]
remind_config_repo_fail("主仓库\"#{repo.name}\"HEAD游离,当前不在任何分支上,无法执行!")
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]
remind_config_repo_fail("主仓库\"#{repo.name}\"未跟踪对应远程分支,无法执行!(需要执行'mgit branch -u origin/<branch>')")
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_remote]
remind_config_repo_fail("主仓库\"#{repo.name}\"远程分支不存在,无法执行!")
# elsif repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]
# remind_config_repo_fail("主仓库\"#{repo.name}\"有改动,无法执行!")
else
Output.puts_processing_message("开始操作主仓库...")
# 现场信息
exec_subrepos = all_repos(except_config:true)
is_all = Workspace.is_all_exec_sub_repos?(exec_subrepos)
context = OperationProgressContext.new(__progress_type)
context.cmd = cmd
context.opts = opts
context.repos = is_all ? nil : exec_subrepos.map { |e| e.name } # nil表示操作所有子仓库
context.branch = repo.status_checker.current_branch(use_cache:true)
# 如果不带任何参数,则将pull分解为fetch+merge执行, fetch已经执行,此处执行merge。带参数则透传。
if opts.length == 0
branch = repo.status_checker.current_branch(strict_mode:false, use_cache:true)
tracking_branch = repo.status_checker.tracking_branch(branch, use_cache:true)
msg = "-m \"【Merge】【0.0.0】【#{branch}】合并远程分支'#{tracking_branch}'。\"" if branch_status == Repo::Status::GIT_BRANCH_STATUS[:diverged]
cmd, opts = "merge", "#{tracking_branch} #{msg}"
end
success, output = repo.execute_git_cmd(cmd, opts)
if success
Output.puts_success_message("主仓库操作成功!\n")
else
Output.puts_fail_message("主仓库操作失败!\n")
config_error = output
end
# 刷新配置表
begin
Workspace.update_config(strict_mode:false) { |missing_repos|
if missing_repos.length > 0
success_missing_repos = Workspace.guide_to_checkout_branch(missing_repos, all_repos, append_message:"拒绝该操作本次执行将忽略以上仓库")
all_repos.concat(success_missing_repos)
# success_missing_repos包含新下载的和当前分支已有的新仓库,其中已有仓库包含在@all_repos内,需要去重
all_repos.uniq! { |repo| repo.name }
end
}
refresh_context(context)
rescue Error => e
if e.type == MGIT_ERROR_TYPE[:config_generate_error]
OperationProgressManager.trap_into_progress(Workspace.root, context)
show_progress_error("配置表生成失败", "#{e.msg}")
end
end
return config_error
end
end
end
def remind_config_repo_fail(msg)
Output.puts_fail_message(msg)
if Workspace.filter_config.auto_exec || Output.continue_with_user_remind?("是否继续操作其余仓库?")
return
else
Output.puts_cancel_message
exit
end
end
def do_abort(argv)
if mgit_try_to_abort?
Output.puts_start_cmd
OperationProgressManager.remove_progress(Workspace.root, __progress_type)
do_repos = all_repos.select { |repo| repo.status_checker.is_in_merge_progress? }
if do_repos.length > 0
append_message = ",另有#{all_repos.length - do_repos.length}个仓库无须操作" if do_repos.length < all_repos.length
Output.puts_processing_block(do_repos.map { |e| e.name }, "开始操作以上仓库#{append_message}...")
_, error_repos = Workspace.execute_git_cmd_with_repos('merge', '--abort', do_repos)
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0
else
Output.puts_success_message("没有仓库需要操作!")
end
return true
else
return false
end
end
def refresh_context(context)
exec_subrepos = all_repos(except_config:true)
is_all = Workspace.is_all_exec_sub_repos?(exec_subrepos)
context.repos = is_all ? nil : exec_subrepos.map { |e| e.name } # nil表示操作所有子仓库
end
def show_progress_error(summary, detail)
error = "#{summary} 已进入操作中间态。
原因:
#{detail}
可选:
- 使用\"mgit pull --continue\"继续拉取。
- 使用\"mgit pull --abort\"取消拉取。"
Foundation.help!(error, title:'暂停')
end
def enable_repo_selection
return true
end
def enable_auto_execution
return true
end
def include_lock_by_default
return true
end
def enable_continue_operation
return true
end
def enable_abort_operation
return true
end
def self.description
return "从仓库或本地分支获取数据并合并。"
end
def self.usage
return "mgit pull [<git-pull-option>] [(--mrepo|--el-mrepo) <repo>...] [--auto-exec] [--no-check] [--include-lock] [--help]\nmgit pull --continue\nmgit pull --abort"
end
end
end
================================================
FILE: lib/m-git/command/push.rb
================================================
#coding=utf-8
module MGit
# @!scope 类似 git push
# 可自动生成 git gerrit 评审分支
# mgit push --gerrit
#
class Push < BaseCommand
# 默认是否开启gerrit相关功能
#
MGIT_PUSH_GERRIT_ENABLED = false
# 默认是否开启topic相关功能,开启后强制开启gerrit
MGIT_PUSH_TOPIC_ENABLED = false
OPT_LIST = {
:gerrit => '--gerrit',
:topic_id => '--topic'
}.freeze
def options
[
ARGV::Opt.new(OPT_LIST[:gerrit],
info:"开启gerrit功能,如果没有对应远程分支则推送新分支,否则推送到审查分支(refs/for/**),默认未开启",
type: :boolean),
ARGV::Opt.new(OPT_LIST[:topic_id],
info:"指定一组变更的分类topic,若未指定则自动生成,默认未开启,开启后强制开启Gerrit功能。mgit push --topic 12345 = git push origin HEAD:refs/for/<branch>%topic=12345",
type: :string),
].concat(super)
end
attr_reader :topic_id
attr_reader :gerrit_enabled
def __setup_option_value(argv)
group_id_opt = argv.opt(OPT_LIST[:topic_id])
if group_id_opt
@topic_id = group_id_opt.value
elsif MGIT_PUSH_TOPIC_ENABLED
@topic_id = SecureRandom.uuid
end
@gerrit_enabled = !@topic_id.nil? || argv.opt_list.did_set_opt?(OPT_LIST[:gerrit]) || MGIT_PUSH_GERRIT_ENABLED
end
def execute(argv)
__setup_option_value(argv)
if argv.git_opts&.length > 0
raws_string = ''
argv.raw_opts.each do |raws|
raws_string += ' '
raws_string += raws.join(' ')
end
Foundation.help!("禁止使用参数 #{argv.git_opts}\n" + Output.remind_message("建议直接使用mgit #{argv.cmd}#{raws_string}"))
end
Workspace.check_branch_consistency
Output.puts_start_cmd
# 获取远程仓库当前分支信息
Workspace.pre_fetch
do_repos = []
diverged_repos = []
do_nothing_repos = []
detached_repos = []
no_remote_repos = []
no_tracking_repos = []
remote_inconsist_repos = []
dirty_repos = []
Output.puts_processing_message("检查各仓库状态...")
Workspace.serial_enumerate_with_progress(all_repos) { |repo|
Timer.start(repo.name)
url_consist = repo.url_consist?
branch_status = repo.status_checker.branch_status
remote_inconsist_repos.push(repo) if !url_consist
if branch_status == Repo::Status::GIT_BRANCH_STATUS[:diverged]
diverged_repos.push(repo)
# 仅超前且url一致的仓库直接加入到操作集
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:ahead] && url_consist
do_repos.push(repo)
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_remote]
no_remote_repos.push(repo)
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]
no_tracking_repos.push(repo)
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]
detached_repos.push(repo)
else
do_nothing_repos.push(repo.name)
end
if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]
dirty_repos.push(repo)
end
Timer.stop(repo.name)
}
Output.puts_success_message("检查完成!\n")
# 将没有远程分支的仓库纳入到本次操作的仓库中
do_repos += no_remote_repos
no_remote_repos = []
if diverged_repos.length > 0 ||
detached_repos.length > 0 ||
no_remote_repos.length > 0 ||
no_tracking_repos.length > 0 ||
remote_inconsist_repos.length > 0 ||
dirty_repos.length > 0
remind_repos = []
remind_repos.push(['远程分支不存在', no_remote_repos.map { |e| e.name }]) if no_remote_repos.length > 0
remind_repos.push(['未追踪远程分支(建议:mgit branch -u origin/<branch>)', no_tracking_repos.map { |e| e.name }]) if no_tracking_repos.length > 0
remind_repos.push(['HEAD游离,当前不在任何分支上', detached_repos.map { |e| e.name }]) if detached_repos.length > 0
remind_repos.push(['当前分支与远程分支分叉,需先pull本地合并', diverged_repos.map { |e| e.name }]) if diverged_repos.length > 0
remind_repos.push(['实际url与配置不一致', remote_inconsist_repos.map { |e| e.name }]) if remote_inconsist_repos.length > 0
remind_repos.push(['有本地改动', dirty_repos.map { |e| e.name }]) if dirty_repos.length > 0
Output.interact_with_multi_selection_combined_repos(remind_repos, "以上仓库状态异常", ['a: 跳过并继续', 'b: 强制执行', 'c: 终止']) { |input|
if input == 'b'
do_repos += diverged_repos
do_repos += detached_repos
do_repos += no_remote_repos
do_repos += no_tracking_repos
do_repos += remote_inconsist_repos
do_repos.uniq! { |repo| repo.name }
elsif input == 'c' || input != 'a'
Output.puts_cancel_message
return
end
}
end
if do_repos.length == 0
Output.puts_remind_message("仓库均无新提交,无须执行!")
return
end
HooksManager.execute_mgit_pre_push_hook(argv.cmd, argv.pure_opts, do_repos.map { |e| e.config })
# 跳过无法处理的异常状态仓库
skip_repos = do_repos.select { |repo|
branch_status = repo.status_checker.branch_status
branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached] ||
branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]
}
Output.puts_remind_block(skip_repos.map { |e| e.name }, "以上仓库无法强制执行,已跳过。") if skip_repos.length > 0
do_repos -= skip_repos
if do_repos.length == 0
Output.puts_success_message("仓库均无新提交,无须执行!")
return
end
# 找到本次操作仓库中推新分支的仓库
no_remote_repo_names = do_repos.select { |repo|
branch_status = repo.status_checker.branch_status
branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_remote]
}.map { |repo| repo.name }
count_msg = ",另有#{do_nothing_repos.length}个仓库无须执行" if do_nothing_repos.length > 0
Output.puts_remind_block(do_repos.map { |repo| repo.name }, "开始push以上仓库#{count_msg}...")
# ------ 执行push ------
total_task = do_repos.length
Output.update_progress(total_task, 0)
config_repo_arr = do_repos.select { |repo| repo.config.is_config_repo }
do_repos_without_config_repo = do_repos - config_repo_arr
sub_error_repos, sub_cr_repos = __execute_push(argv, do_repos_without_config_repo) { |progress|
Output.update_progress(total_task, progress)
}
# 保证最后操作主仓库,便于流水线进行模块注册
sub_task_count = do_repos_without_config_repo.length
config_error_repos, config_cr_repos = __execute_push(argv, config_repo_arr) { |progress|
Output.update_progress(total_task, progress + sub_task_count)
}
puts "\n"
# ----------------------
# 显示错误仓库信息
error_repos = sub_error_repos.merge(config_error_repos)
Workspace.show_error(error_repos) if error_repos.length > 0
# 显示成功推了新分支的仓库
success_push_branch_repo_names = no_remote_repo_names - error_repos.keys
if success_push_branch_repo_names.length > 0
Output.puts_remind_block(success_push_branch_repo_names, "为以上仓库推送了新分支。")
puts "\n"
end
# 显示成功仓库的评审链接
if gerrit_enabled
success_output = ''
all_cr_repos = sub_cr_repos.keys + config_cr_repos.keys
all_cr_repos.uniq!
all_cr_repos.each do |repo_name|
cr_url = sub_cr_repos[repo_name] || config_cr_repos[repo_name]
success_output += Output.generate_title_block(repo_name, has_separator: false) { cr_url } + "\n\n"
end
if success_output.length > 0
Output.puts_remind_message("以下本地提交代码评审链接,请联系仓库负责人评审后合入:")
puts success_output
end
# 显示topic id
if topic_id
success_push_code_repo_names = do_repos.map { |e| e.name } - error_repos.keys - success_push_branch_repo_names
if success_push_code_repo_names.length > 0
Output.puts_remind_message("本次push的topic id:#{topic_id}\n") if !topic_id.nil? && topic_id.length > 0
end
end
end
# 显示成功信息
if error_repos.empty?
Output.puts_succeed_cmd(argv.absolute_cmd)
Timer.show_time_consuming_repos
elsif topic_id
# 显示失败后的操作提示,若全部成功则不显示
group_repo_names = error_repos.keys
group_repo_name_str = group_repo_names.join(' ')
is_all = Workspace.is_all_exec_sub_repos_by_name?(group_repo_names)
mrepo_str = is_all ? '' : " --mrepo #{group_repo_name_str}"
Output.puts_processing_block(group_repo_names, "以上仓库组推送失败,请处理后用以下指令再次推送:\n\n mgit push --topic #{topic_id}#{mrepo_str}\n")
end
end
private
def __execute_push(argv, do_repos)
mutex = Mutex.new
error_repos = {}
cr_repos = {}
task_count = 0
Workspace.concurrent_enumerate(do_repos) { |repo|
cmd, opt = __parse_cmd_and_opt(repo)
git_cmd = repo.git_cmd(cmd, opt)
Utils.execute_shell_cmd(git_cmd) { |stdout, stderr, status|
mutex.lock
error_msg, cr_url = __process_push_result(repo, stdout, stderr, status)
error_repos[repo.name] = error_msg if error_msg
cr_repos[repo.name] = cr_url if cr_url
task_count += 1
yield(task_count) if block_given?
mutex.unlock
}
}
[error_repos, cr_repos]
end
#
# @return [String, String] error_message, code_review_url
#
#
def __process_push_result(repo, stdout, stderr, status)
# 标记状态更新
repo.status_checker.refresh
# 本地成功但远程失败此时status.success? == true,解析以检测这个情况
repo_msg_parser = GitMessageParser.new(repo.config.url)
check_msg = repo_msg_parser.parse_push_msg(stderr)
# 本地和远程同时成功
if status.success? && check_msg.nil?
cr_url = repo_msg_parser.parse_code_review_url(stdout) || repo_msg_parser.parse_code_review_url(stderr) if gerrit_enabled
elsif status.success? && !check_msg.nil?
# 本地失败
# check_msg = error_msg
else
check_msg = stderr
check_msg += stdout if stdout.length > 0
end
[check_msg, cr_url]
end
def __parse_cmd_and_opt(repo)
cmd = 'push'
if repo.status_checker.branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_remote]
opt = "-u origin #{repo.status_checker.current_branch}"
else
opt = "origin HEAD:#{repo.status_checker.current_branch}"
if gerrit_enabled
opt = "origin HEAD:refs/for/#{repo.status_checker.current_branch}"
opt += "%topic=" + topic_id if topic_id
end
end
[cmd, opt]
end
def enable_repo_selection
return true
end
def self.description
return "更新远程分支和对应的数据对象。"
end
def self.usage
return "mgit push [<git-push-option>|--gerrit] [(--mrepo|--el-mrepo) <repo>...] [--topic] [--help]"
end
end
end
================================================
FILE: lib/m-git/command/rebase.rb
================================================
#coding=utf-8
module MGit
# @!scope 类似 git rebase
#
class Rebase < BaseCommand
PROGRESS_STAGE = {
:new_start => 0,
:did_pull_config => 1,
:did_refresh_config => 2,
:did_pull_sub => 3
}.freeze
PROGRESS_STAGE_KEY = 'progress_stage'
PROGRESS_AUTO = 'auto_exec'
OPT_LIST = {
:pull => '--pull'
}.freeze
def options
[
ARGV::Opt.new(OPT_LIST[:pull], info:'可选参数,指定后在合并仓库前会拉取远程分支更新代码,如:"mgit rabase --pull"。', type: :boolean)
].concat(super)
end
def execute(argv)
return if do_abort(argv)
check_master_rebase(argv)
Workspace.check_branch_consistency
Output.puts_start_cmd
config_repo = generate_config_repo
if mgit_try_to_continue?
# 不处于中间态禁止执行
Foundation.help!("当前并不处于操作中间态,无法进行continue操作!") if !OperationProgressManager.is_in_progress?(Workspace.root, __progress_type)
# 读取指令缓存失败禁止执行
context, _ = OperationProgressManager.load_context(Workspace.root, __progress_type)
Foundation.help!("缓存指令读取失败,continue无法继续进行,请重新执行完整指令。") if context.nil? || !context.validate?
# 分支不匹配禁止执行
Foundation.help!("当前主仓库所在分支跟上次操作时所在分支(#{context.branch})不一致,请切换后重试。") if config_repo.status_checker.current_branch(use_cache:true) != context.branch
if !context.repos.nil?
Output.puts_processing_message("加载上次即将操作的子仓库...")
Workspace.update_all_repos(context.repos)
end
cmd = context.cmd
opts = context.opts
config_error = continue_execute(cmd, opts, config_repo, context.other[PROGRESS_STAGE_KEY], context.other[PROGRESS_AUTO])
if config_error
Output.puts_fail_block([config_repo.name], "主仓库操作失败:#{config_error}")
return
end
else
# 处于中间态则提示
if OperationProgressManager.is_in_progress?(Workspace.root, __progress_type)
if Output.continue_with_user_remind?("当前处于操作中间态,建议取消操作并执行\"mgit merge --continue\"继续操作未完成仓库。\n 继续执行将清除中间态并重新操作所有仓库,是否取消?")
Output.puts_cancel_message
return
end
end
cmd = argv.cmd
opts = argv.git_opts
# 优先操作配置仓库
config_error = rebase_config_repo(cmd, opts, config_repo, argv.opt_list.did_set_opt?(OPT_LIST[:pull]))
if config_error
Output.puts_fail_block([config_repo.name], "主仓库操作失败:#{config_error}")
return
end
end
do_repos = []
dirty_repos = []
detached_repos = []
no_tracking_repos = []
Output.puts_processing_message("检查各仓库状态...")
Workspace.serial_enumerate_with_progress(all_repos) { |repo|
next if !config_repo.nil? && repo.name == config_repo.name
status = repo.status_checker.status
branch_status = repo.status_checker.branch_status
if status == Repo::Status::GIT_REPO_STATUS[:clean] &&
branch_status != Repo::Status::GIT_BRANCH_STATUS[:detached] &&
branch_status != Repo::Status::GIT_BRANCH_STATUS[:no_tracking]
do_repos.push(repo)
else
if status == Repo::Status::GIT_REPO_STATUS[:dirty]
dirty_repos.push(repo)
end
if branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]
detached_repos.push(repo)
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]
no_tracking_repos.push(repo)
end
end
}
Output.puts_success_message("检查完成!\n")
if dirty_repos.length > 0 || no_tracking_repos.length > 0 || detached_repos.length > 0
remind_repos = []
remind_repos.push(['有本地改动', dirty_repos.map { |e| e.name }])
remind_repos.push(['未追踪远程分支(建议:mgit branch -u origin/<branch>)', no_tracking_repos.map { |e| e.name }]) if no_tracking_repos.length > 0
remind_repos.push(['HEAD游离,当前不在任何分支上', detached_repos.map { |e| e.name }]) if detached_repos.length > 0
Output.interact_with_multi_selection_combined_repos(remind_repos, "以上仓库状态异常", ['a: 跳过并继续', 'b: 强制执行', 'c: 终止']) { |input|
if input == 'b'
do_repos += dirty_repos
do_repos += detached_repos
do_repos += no_tracking_repos
do_repos.uniq! { |repo| repo.name }
elsif input == 'c' || input != 'a'
Output.puts_cancel_message
return
end
}
end
if do_repos.length == 0
Output.puts_remind_message("没有仓库需要执行rebase指令!") if config_repo.nil?
else
Output.puts_processing_message("开始rebase子仓库...")
_, error_repos = Workspace.execute_git_cmd_with_repos(cmd, opts, do_repos)
end
Output.puts_succeed_cmd("#{cmd} #{opts}") if config_error.nil? || error_repos.empty?
# 清除中间态
OperationProgressManager.remove_progress(Workspace.root, __progress_type)
end
# 合并主仓库
#
# @param cmd [String] 合并指令
#
# @param opts [String] 合并参数
#
# @param repo [Repo] 配置仓库对象
#
# @param exec_repos [Array<Repo>] 本次操作的所有仓库(含配置仓库)
#
def rebase_config_repo(cmd, opts, repo, auto_update)
return if repo.nil?
branch_status = repo.status_checker.branch_status
if branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]
remind_config_repo_fail("主仓库\"#{repo.name}\"HEAD游离,当前不在任何分支上,无法执行!")
elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]
remind_config_repo_fail("主仓库\"#{repo.name}\"未跟踪对应远程分支,无法执行!(需要执行'mgit branch -u origin/<branch>')")
elsif repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]
remind_config_repo_fail("主仓库\"#{repo.name}\"有改动,无法执行\"#{cmd}\"!")
else
return continue_execute(cmd, opts, repo, PROGRESS_STAGE[:new_start], auto_update)
end
end
def continue_execute(cmd, opts, repo, check_point, auto_update)
# 现场信息
exec_subrepos = all_repos(except_config:true)
is_all = Workspace.is_all_exec_sub_repos?(exec_subrepos)
context = OperationProgressContext.new(__progress_type)
context.cmd = cmd
context.opts = opts
context.repos = is_all ? nil : exec_subrepos.map { |e| e.name } # nil表示操作所有子仓库
context.branch = repo.status_checker.current_branch(use_cache:true)
# 更新主仓库
update_config_repo(repo, context, auto_update) if check_point < PROGRESS_STAGE[:did_pull_config]
if check_point < PROGRESS_STAGE[:did_refresh_config]
# 操作主仓库
config_error = exec_config_repo(repo, cmd, opts)
return config_error if config_error
# 更新配置表
refresh_config(repo, context, auto_update)
end
if check_point < PROGRESS_STAGE[:did_pull_sub]
# 如果本次操作所有子仓库,则再次获取所有子仓库(因为配置表可能已经更新,子仓库列表也有更新,此处获取的仓库包含:已有的子仓库 + 合并后新下载仓库 + 从缓存弹出的仓库)
exec_subrepos = all_repos(except_config:true) if context.repos.nil?
# 更新子仓库
update_subrepos(exec_subrepos, context, auto_update)
end
config_error
end
def update_config_repo(repo, context, auto)
if auto || Output.continue_with_user_remind?("即将合并主仓库,是否先拉取远程代码更新?")
Output.puts_processing_message("正在更新主仓库...")
success, output = repo.execute_git_cmd('pull', '')
if !success
context.other = {
PROGRESS_STAGE_KEY => PROGRESS_STAGE[:did_pull_config],
PROGRESS_AUTO => auto
}
OperationProgressManager.trap_into_progress(Workspace.root, context)
show_progress_error("主仓库更新失败", "#{output}")
else
Output.puts_success_message("更新成功!\n")
end
end
end
def exec_config_repo(repo, cmd, opts)
error = nil
Output.puts_processing_message("开始操作主仓库...")
success, output = repo.execute_git_cmd(cmd, opts)
if success
Output.puts_success_message("操作成功!\n")
else
Output.puts_fail_message("操作失败!\n")
error = output
end
error
end
# 刷新配置表
def refresh_config(repo, context, auto)
begin
Workspace.update_config(strict_mode:false) { |missing_repos|
if missing_repos.length > 0
# 这里分支引导仅根据主仓库来进行,如果使用all_repos来作为引导
# 基准,可能不准确(因为all_repos可能包含merge分支已有的本地
# 仓库,而这些仓库所在分支可能五花八门,数量也可能多于处于正确
# 分支的仓库)。
success_missing_repos = Workspace.guide_to_checkout_branch(missing_repos, [repo], append_message:"拒绝该操作本次执行将忽略以上仓库")
all_repos.concat(success_missing_repos)
# success_missing_repos包含新下载的和当前分支已有的新仓库,其中已有仓库包含在@all_repos内,需要去重
all_repos.uniq! { |repo| repo.name }
end
}
refresh_context(context)
rescue Error => e
if e.type == MGIT_ERROR_TYPE[:config_generate_error]
context.other = {
PROGRESS_STAGE_KEY => PROGRESS_STAGE[:did_refresh_config],
PROGRESS_AUTO => auto
}
OperationProgressManager.trap_into_progress(Workspace.root, context)
show_progress_error("配置表生成失败", "#{e.msg}")
end
end
end
def update_subrepos(subrepos, context, auto)
if auto || Output.continue_with_user_remind?("即将合并子仓库,是否先拉取远程代码更新?")
Output.puts_processing_message("正在更新子仓库...")
_, error_repos = Workspace.execute_git_cmd_with_repos('pull', '', subrepos)
if error_repos.length > 0
context.other = {
PROGRESS_STAGE_KEY => PROGRESS_STAGE[:did_pull_sub],
PROGRESS_AUTO => auto
}
OperationProgressManager.trap_into_progress(Workspace.root, context)
show_progress_error("子仓库更新失败", "见上述输出")
else
Output.puts_success_message("更新成功!\n")
end
end
end
def refresh_context(context)
exec_subrepos = all_repos(except_config:true)
is_all = Workspace.is_all_exec_sub_repos?(exec_subrepos)
context.repos = is_all ? nil : exec_subrepos.map { |e| e.name } # nil表示操作所有子仓库
end
def remind_config_repo_fail(msg)
Output.puts_fail_message(msg)
return if Output.continue_with_user_remind?("是否继续操作其余仓库?")
Output.puts_cancel_message
exit
end
def check_master_rebase(argv)
opt_arr = argv.git_opts(raw:false)
opt_arr.each { |opts|
Foundation.help!("当前版本不支持\"-i\"或\"--interactive\"参数,请重试。") if ['-i','--interactive'].include?(opts.first)
}
end
def do_abort(argv)
return false unless argv.git_opts.include?('--abort')
Output.puts_start_cmd
OperationProgressManager.remove_progress(Workspace.root, __progress_type)
do_repos = all_repos.select { |repo| repo.status_checker.is_in_rebase_progress? }
if do_repos.length > 0
append_message = ",另有#{all_repos.length - do_repos.length}个仓库无须操作" if do_repos.length < all_repos.length
Output.puts_processing_block(do_repos.map { |e| e.name }, "开始操作以上仓库#{append_message}...")
_, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, do_repos)
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0
else
Output.puts_success_message("没有仓库需要操作!")
end
true
end
def show_progress_error(summary, detail)
error = "#{summary} 已进入操作中间态。
原因:
#{detail}
可选:
- 使用\"mgit rebase --continue\"继续变基。
- 使用\"mgit rebase --abort\"取消变基。"
Foundation.help!(error, title:'暂停')
end
def enable_repo_selection
true
end
def enable_continue_operation
true
end
def self.description
"重新将提交应用到其他基点,该命令不执行lock的仓库。"
end
def self.usage
"mgit rebase [<git-rebase-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]\nmgit rebase --continue\nmgit rebase --abort"
end
private
def __progress_type
OperationProgressManager::PROGRESS_TYPE[:rebase]
end
end
end
================================================
FILE: lib/m-git/command/reset.rb
================================================
#coding=utf-8
module MGit
# @!scope 类似 git reset
#
class Reset < BaseCommand
def execute(argv)
Workspace.check_branch_consistency
Output.puts_start_cmd
_, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, all_repos)
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0
end
def enable_repo_selection
true
end
def self.description
"将当前HEAD指针还原到指定状态。"
end
def self.usage
"mgit reset [<git-reset-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]"
end
end
end
================================================
FILE: lib/m-git/command/self.rb
================================================
#coding=utf-8
module MGit
# 该指令用于不带指令的输入:mgit --help,用于执行mgit的一级参数(如"mgit --help"的"--help")
#
class Self < BaseCommand
HELP_INTRODUCTION = <<INTRO
#{Output.info_title("Description:")}
mgit是多仓库管理工具,通过将git指令作用到多个仓库,实现批量的版本管理功能
更多介绍:https://github.com/baidu/mgit
INTRO
OPT_LIST = {
:all => '--all',
:all_s => '-a',
:list => '--list',
:list_s => '-l',
:size => '--size',
:size_s => '-s',
:version => '--version',
:version_s => '-v',
:workspace => '--workspace',
:workspace_s => '-w'
}.freeze
def options
return [
ARGV::Opt.new(OPT_LIST[:list], short_key:OPT_LIST[:list_s], info:"显示MGit管理的仓库。", type: :boolean),
ARGV::Opt.new(OPT_LIST[:size], short_key:OPT_LIST[:size_s], info:"显示MGit管理的仓库的磁盘占用量。", type: :boolean),
ARGV::Opt.new(OPT_LIST[:all], short_key:OPT_LIST[:all_s], info:"指定操作所有定义在manifest内的仓库,可配合-l合并使用: \"mgit -al\"。", type: :boolean),
ARGV::Opt.new(OPT_LIST[:version], short_key:OPT_LIST[:version_s], info:"显示当前MGit版本。", type: :boolean),
ARGV::Opt.new(OPT_LIST[:workspace], short_key:OPT_LIST[:workspace_s], info:"显示当前MGit工程管理根目录(.mgit所在目录)。", type: :boolean)
].concat(super)
end
def validate(argv)
Foundation.help!("输入非法参数:#{argv.git_opts}。请通过\"mgit --help\"查看用法。") if argv.git_opts.length > 0
end
def prepare
Workspace.setup_multi_repo_root
Workspace.setup_config
end
# --- 覆写,不需要预设操作 ---
def pre_exec
end
def post_exec
end
def usage(argv)
show_help(argv)
end
# -------------------------
def execute(argv)
# 如果存在多余(透传)指令则报错
extra_opt_str = argv.git_opts
if extra_opt_str.length > 0
Output.puts_fail_message("输入无效参数:#{extra_opt_str}\n")
show_help(argv)
exit
end
argv.enumerate_valid_opts { |opt|
if opt.key == OPT_LIST[:list] || opt.key == OPT_LIST[:list_s]
show_all_repos(argv)
return
elsif opt.key == OPT_LIST[:size] || opt.key == OPT_LIST[:size_s]
show_repo_size
return
elsif opt.key == OPT_LIST[:version] || opt.key == OPT_LIST[:version_s]
show_version
return
elsif opt.key == OPT_LIST[:workspace] || opt.key == OPT_LIST[:workspace_s]
show_workspace
return
end
}
# 无任何参数传入时显示帮助
show_help(argv)
end
def show_help(argv)
head_space = ' '
middle_space = ' '
output = HELP_INTRODUCTION # "#{Output.info_title("Description:")}\n\n#{head_space}mgit是多仓库管理工具,通过将git指令作用到多个仓库,实现批量的版本管理功能。"
output += "#{Output.info_title("Usage:")}\n\n#{head_space}$ #{Output.green_message("mgit <mgit_options>")}\n"
output += "#{head_space}$ #{Output.green_message("mgit <command> [<command_option>...] [<value>...]")}\n\n"
# mgit options
output += "#{Output.info_title("MGit Option:")}\n\n"
divider = ", "
longest_opt = argv.opt_list.opts.max_by { |e| "#{Output.blue_message("#{e.key}#{divider + e.short_key if !e.short_key.nil?}")}".length }
max_opt_length = "#{longest_opt.short_key + divider + longest_opt.key}".length
mgit_option_info = ''
argv.opt_list.opts.each { |opt|
key = "#{opt.short_key + divider + opt.key}"
mgit_option_info += "#{head_space}#{Output.blue_message(key)}#{' ' * (max_opt_length - key.length + middle_space.length)}#{argv.info(opt.key)}\n"
}
output += mgit_option_info + "\n"
# subcommand
output += "#{Output.info_title("Command:")}\n\n"
cmd_header = '+ '
cmd_info = ''
max_cmd_length = Output.blue_message(cmd_header + CommandManager.commands.keys.max_by { |e| e.length }.to_s).length
CommandManager.commands.keys.sort.each { |cmd_name|
next if cmd_name == self.class.cmd
cls_name = CommandManager.commands[cmd_name]
cmd_name = Output.green_message(cmd_header + cmd_name.downcase.to_s)
cmd_info += "#{head_space}#{cmd_name}#{' ' * (max_cmd_length - cmd_name.length + middle_space.length)}#{cls_name.description}\n"
}
output += cmd_info + "\n"
output += "#{Output.info_title("Command Option:")}\n\n#{head_space}请通过[ mgit <command> --help ]查看。\n\n"
output += "#{Output.info_title("Version:")}\n\n#{head_space}#{MGit::VERSION}\n"
puts output
end
def show_all_repos(argv)
prepare
list_all = argv.opt_list.did_set_opt?(OPT_LIST[:all])
existing_repos, missing_repos = prepare_repos(with_excluded:list_all)
list = {}
if existing_repos.length > 0
existing_repos.sort_by { |e| e.name }.each { |light_repo|
dir = File.join("<ROOT>", File.dirname(light_repo.path)).bold
list[dir] = [] if list[dir].nil?
list[dir].push(light_repo.name)
}
end
if missing_repos.length > 0
list['本地缺失'.bold] = missing_repos.sort_by { |e| e.name }.map { |e| e.name }
end
list_array = []
list.each { |dir, list|
list_array.push([dir.bold, list])
}
puts Output.generate_table_combination(list_array, separator:'|')
if list_all
message = "共统计#{existing_repos.length + missing_repos.length}个仓库。"
else
message = "mgit目前共管理#{existing_repos.length + missing_repos.length}个仓库。"
end
Output.puts_remind_message(message)
Output.puts_fail_message("有#{missing_repos.length}个仓库本地缺失!") if missing_repos.length > 0
end
def show_repo_size
prepare
Workspace.setup_all_repos
Output.puts_processing_message("开始计算...")
repo_size = {}
mutex = Mutex.new
task_count = 0
Output.update_progress(all_repos.length, task_count)
Workspace.concurrent_enumerate(all_repos) { |repo|
success, output = repo.execute("du -sh #{repo.path}")
mutex.lock
if success
repo_size[repo.name] = output&.strip
end
task_count += 1
Output.update_progress(all_repos.length, task_count)
mutex.unlock
}
Output.puts_success_message("计算完成。")
display_size = repo_size.sort_by { |k,v| k}.map { |e| e.last }
Output.puts_remind_block(display_size, "共统计#{repo_size.length}个仓库。")
end
def prepare_repos(with_excluded:false)
existing_repos = []
missing_repos = []
Workspace.config.light_repos.each { |light_repo|
if with_excluded || !light_repo.mgit_excluded
repo_exist = Repo.is_git_repo?(light_repo.abs_dest(Workspace.root))
if repo_exist
existing_repos.push(light_repo)
else
missing_repos.push(light_repo)
end
end
}
return existing_repos, missing_repos
end
def show_workspace
root = Workspace.multi_repo_root_path
if root.nil?
Output.puts_fail_message("当前不在任何多仓库目录下!")
else
puts root
end
end
def show_version
puts MGit::VERSION
end
def enable_short_basic_option
return true
end
end
end
================================================
FILE: lib/m-git/command/stash.rb
================================================
#coding=utf-8
module MGit
# @!scope 类似 git stash,但是强制标记名称
#
class Stash < BaseCommand
OPT_LIST = {
:push => '--push',
:pop => '--pop',
:apply => '--apply',
:list => '--list',
:clear => '--clear',
}.freeze
def options
return [
ARGV::Opt.new(OPT_LIST[:push], info:'添加储藏:mgit stash --push "stash_name"。', type: :string),
ARGV::Opt.new(OPT_LIST[:pop], info:'恢复储藏:mgit stash --pop "stash_name"。', type: :string),
ARGV::Opt.new(OPT_LIST[:apply], info:'恢复储藏:mgit stash --apply "stash_name"。', type: :string),
ARGV::Opt.new(OPT_LIST[:list], info:'显示储藏列表。', type: :boolean),
ARGV::Opt.new(OPT_LIST[:clear], info:'清空所有储藏。', type: :boolean)
].concat(super)
end
def validate(argv)
missing_msg = "缺失必要参数" if argv.raw_opts.length == 0
illegal_msg = "输入非法参数:#{argv.git_opts}" if argv.git_opts.length > 0
conjunction = ",同时" if !missing_msg.nil? && !illegal_msg.nil?
Foundation.help!("#{missing_msg}#{conjunction}#{illegal_msg},请通过\"mgit #{argv.cmd} --help\"查看用法。") if !missing_msg.nil? || !illegal_msg.nil?
end
# 注意:git stash相关命令不支持指定“working tree”和“git dir”
# 如:git --git-dir=/path/to/.git --work-tree=/path/to/working-tree stash list,若当前不在working tree目录下,则将无法执行。
def execute(argv)
argv.enumerate_valid_opts { |opt|
if opt.key == OPT_LIST[:push]
do_stash_push(argv, opt.value)
break
elsif opt.key == OPT_LIST[:pop] || opt.key == OPT_LIST[:apply]
action = opt.key.gsub('--', '')
do_stash_pop_apply(argv, opt.value, action)
break
elsif opt.key == OPT_LIST[:list]
do_stash_list(argv)
elsif opt.key == OPT_LIST[:clear]
do_clear(argv)
end
}
end
def do_clear(argv)
if Output.continue_with_user_remind?("该操作会丢失所有的stash,确定要执行吗?")
Output.puts_start_cmd
abs_cmd = "git stash clear"
_, error_repos = Workspace.execute_common_cmd_with_repos(abs_cmd, all_repos)
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0
else
Output.puts_cancel_message
end
end
def do_stash_list(argv)
Output.puts_start_cmd
error_repos = {}
all_repos.each { |repo|
cmd = "git -C \"#{repo.path}\" stash list"
success, output = repo.execute(cmd)
if success
puts Output.generate_title_block(repo.name) {
output
} + "\n" if output.length > 0
else
error_repos[repo.name] = output
end
}
if error_repos.length > 0
Workspace.show_error(error_repos)
else
Output.puts_succeed_cmd(argv.absolute_cmd)
end
end
def do_stash_pop_apply(argv, stash_name, action)
do_repos = []
mutex = Mutex.new
Output.puts_processing_message("检查仓库状态...")
all_repos.each { |repo|
stash_list = repo_stash_list_msg(repo)
next if stash_list.nil?
stash_id = stash_include_name(stash_list, stash_name)
next if stash_id.nil?
mutex.lock
do_repos.push(repo)
mutex.unlock
}
Output.puts_success_message("检查完成!\n")
Output.puts_start_cmd
if do_repos.length == 0
Output.puts_nothing_to_do_cmd
else
_, error_repos = Workspace.execute_common_cmd_with_repos('', do_repos) { |repo|
stash_list = repo_stash_list_msg(repo)
stash_id = stash_include_name(stash_list, stash_name)
"git stash #{action} #{stash_id}"
}
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0
end
end
def do_stash_push(argv, stash_name)
do_repos = []
remind_repos = []
mutex = Mutex.new
Output.puts_processing_message("检查仓库状态...")
all_repos.each { |repo|
next if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:clean]
next if repo.status_checker.dirty_zone == Repo::Status::GIT_REPO_STATUS_DIRTY_ZONE[:special]
stash_list = repo_stash_list_msg(repo)
stash_id = stash_include_name(stash_list, stash_name)
mutex.lock
if stash_id.nil?
do_repos.push(repo)
else
remind_repos.push(repo.name)
end
mutex.unlock
}
Output.puts_success_message("检查完成!\n")
if remind_repos.length > 0
Output.puts_remind_block(remind_repos, "以上仓库当前分支已经存在stash名称:#{stash_name},请换一个名称或者使用\"mgit stash --list\"查看详情。")
Output.puts_fail_cmd(argv.absolute_cmd)
elsif do_repos.empty?
Output.puts_remind_message("所有仓库均是clean状态或者文件未跟踪,无需执行")
else
Output.puts_start_cmd
abs_cmd = "git stash save -u #{stash_name}"
_, error_repos = Workspace.execute_common_cmd_with_repos(abs_cmd, do_repos)
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0
end
end
# 获取当前的 stash list 字符串,nil,标识当前没有stash
def repo_stash_list_msg(repo)
success, output = repo.execute("git -C \"#{repo.path}\" stash list")
return output if success && output.length > 0
end
# 查询stash_list 是否包含某一个保存的stash
# 不做分支判断,因为在保存的stashlist中,分支只保留了/之后的内容
#
def stash_include_name(stash_list, stash_name)
return if stash_list.nil?
stash_list_array = stash_list.split("\n")
find_stash_id = nil
stash_list_array.each do |line|
regex = /(stash@{\d+}):.*:\s(.*)$/
next unless line.match(regex)
match_stash_name = $2
next unless match_stash_name == stash_name
find_stash_id = $1
break
end
find_stash_id
end
def enable_repo_selection
true
end
def self.description
"使用git stash将当前工作区改动暂时存放起来。"
end
def self.usage
"mgit stash [<option> <value>...] [(--mrepo|--el-mrepo) <repo>...] [--help]"
end
end
end
================================================
FILE: lib/m-git/command/status.rb
================================================
#coding=utf-8
module MGit
# @!scope 类似 git status
#
class Status < BaseCommand
def execute(argv)
Output.puts_processing_message("正在检查各仓库状态...")
status_info = {}
mutex = Mutex.new
mutex_branch = Mutex.new
mutex_modification = Mutex.new
mutex_other = Mutex.new
branch_notice = []
modification_notice = []
other_notice = []
in_progress_notice = []
task_count = 0
repo_combo = all_repos + locked_repos
Output.update_progress(repo_combo.length, task_count)
Workspace.concurrent_enumerate(repo_combo) { |repo|
status_msg = ''
info = []
# 非锁定仓库进行常规分支检查
if !locked_repos.include?(repo) &&
repo.status_checker.branch_status != Repo::Status::GIT_BRANCH_STATUS[:up_to_date] &&
repo.status_checker.branch_status != Repo::Status::GIT_BRANCH_STATUS[:no_remote]
info.push(['分支', [repo.status_checker.branch_message]])
mutex_branch.lock
branch_notice.push(repo.name)
mutex_branch.unlock
end
# 检查工作区状态
if repo.status_checker.status != Repo::Status::GIT_REPO_STATUS[:clean]
info += repo.status_checker.message
mutex_modification.lock
modification_notice.push(repo.name)
mutex_modification.unlock
end
# 检查url是否一致
if !repo.url_consist?
info.push(['其他', ['仓库实际url与当前配置不一致']])
mutex_other.lock
other_notice.push(repo.name)
mutex_other.unlock
end
# 生成表格
status_msg = Output.generate_table_combination(info) + "\n\n" if info.length > 0
# 压缩状态信息
mutex.lock
if status_msg.length > 0
status_info[status_msg] = {'repo_names' => [], 'info' => info} if status_info[status_msg].nil?
status_info[status_msg]['repo_names'].push(repo.name)
end
task_count += 1
Output.update_progress(repo_combo.length, task_count)
mutex.unlock
}
status_info.each_with_index { |(status_msg, item), index|
info = item['info']
repo_names = item['repo_names']
Output.puts_remind_block(repo_names, "以上仓库状态:")
MGit::Loger.info(info)
status_msg += "\n" if index != status_info.length - 1
puts status_msg
}
OperationProgressManager::PROGRESS_TYPE.each { |type, type_str|
if OperationProgressManager.is_in_progress?(Workspace.root, type_str)
in_progress_notice.push(type.to_s)
end
}
summary = []
if branch_notice.length > 0
summary.push(["分支提醒(#{branch_notice.length})",branch_notice])
end
if modification_notice.length > 0
summary.push(["改动提醒(#{modification_notice.length})",modification_notice])
end
if other_notice.length > 0
summary.push(["其他警告(#{other_notice.length})", other_notice])
end
if in_progress_notice.length > 0
summary.push(["处于中间态的操作",in_progress_notice])
end
if summary.length > 0
puts "\n"
puts Output.generate_table_combination(summary, title: "状态小结", separator: "|")
MGit::Loger.info('状态小结')
MGit::Loger.info(summary)
end
Output.puts_success_message("所查询仓库均无改动!") if status_info.keys.length == 0
Output.puts_succeed_cmd(argv.absolute_cmd)
end
def validate(argv)
Foundation.help!("输入非法参数:#{argv.git_opts}。请通过\"mgit #{argv.cmd} --help\"查看用法。") if argv.git_opts.length > 0
end
def enable_repo_selection
true
end
def enable_short_basic_option
true
end
def self.description
"输出所有仓库的状态。包括:\"分支\",\"暂存区\",\"工作区\",\"特殊(未跟踪和被忽略)\",\"冲突\"。"
end
def self.usage
"mgit status [(-m|-e) <repo>...] [-h]"
end
end
end
================================================
FILE: lib/m-git/command/sync.rb
================================================
#coding=utf-8
module MGit
# @!scope 同步所有管理的仓库到工作区,可能从远端拉取,可能从本地dump,所以sync后的工作区不一定是分支的最新节点
# 可通过 mgit sync --pull 进行同步后pull到最新节点
#
class Sync < BaseCommand
OPT_LIST = {
:new_repo => '--new-repo',
:new_repo_s => '-n',
:all => '--all',
:all_s => '-a',
:clone => '--clone',
:clone_s => '-c',
:pull => '--pull',
:pull_s => '-p',
:url => '--url',
:url_s => '-u',
}.freeze
# --- 覆写前后hook,不需要预设操作 ---
def pre_exec
MGit::DurationRecorder.start
Workspace.setup_multi_repo_root
# 配置log
MGit::Loger.config(Workspace.root)
MGit::Loger.info("~~~ #{@argv.absolute_cmd} ~~~")
Workspace.setup_config
end
def post_exec
# 打点结束
duration = MGit::DurationRecorder.end
MGit::Loger.info("~~~ #{@argv.absolute_cmd}, 耗时:#{duration} s ~~~")
end
# --------------------------------
def options
return [
ARGV::Opt.new(OPT_LIST[:new_repo],
short_key:OPT_LIST[:new_repo_s],
info:"下载配置表中指定被mgit管理,但本地不存在的仓库,已有仓库不做任何处理,使用:mgit sync -n。",
type: :boolean),
ARGV::Opt.new(OPT_LIST[:all],
short_key:OPT_LIST[:all_s],
info:'对所有(包含不被mgit管理的)仓库操作:1.如果本地缺失则下载。2.如果本地存在且被锁定则同步到锁定状态。注意,如果需要下载代码,需要配置仓库URL,否则跳过,使用:mgit sync -a。',
type: :boolean),
ARGV::Opt.new(OPT_LIST[:clone], short_key:OPT_LIST[:clone_s], info:'下载一组仓库(包含不被mgit管理的仓库),如: mgit sync -c repo1 repo2。'),
ARGV::Opt.new(OPT_LIST[:pull],
short_key:OPT_LIST[:pull_s],
info:'同步本地仓库后执行pull操作更新,配合其他指令使用,如: mgit sync -a -p。',
type: :boolean),
ARGV::Opt.new(OPT_LIST[:url],
short_key:OPT_LIST[:url_s],
info:'校验并同步URL与配置不一致的仓库,如: mgit sync -u。',
type: :boolean)
].concat(super)
end
def validate(argv)
opt_all = argv.opt(OPT_LIST[:all])
opt_clone = argv.opt(OPT_LIST[:clone])
opt_new = argv.opt(OPT_LIST[:new_repo])
invalid = [opt_all, opt_clone, opt_new].select { |e| !e.nil? }.length > 1
Foundation.help!("请勿同时指定[#{OPT_LIST[:all]}|#{OPT_LIST[:all_s]}],[#{OPT_LIST[:clone]}|#{OPT_LIST[:clone_s]}]和[#{OPT_LIST[:new_repo]}|#{OPT_LIST[:new_repo_s]}]。") if invalid
end
def execute(argv)
Output.puts_start_cmd
if argv.opt_list.did_set_opt?(OPT_LIST[:all])
setup_all_sync_reops(argv)
elsif argv.opt_list.did_set_opt?(OPT_LIST[:new_repo])
setup_new_reops
elsif argv.opt_list.did_set_opt?(OPT_LIST[:url])
setup_config_url_repos
elsif argv.opt_list.did_set_opt?(OPT_LIST[:clone])
return if !setup_download_reops(argv.opt(OPT_LIST[:clone]).value)
elsif argv.git_opts.length > 0
return if !setup_specified_repos(argv)
else
setup_normal_reops(argv)
end
if (@sync_repos.length + @update_repos.length + @download_repos.length) == 0
Output.puts_success_message("没有仓库需要同步!")
return
end
error_repos = {}
if @sync_repos.length > 0
Workspace.concurrent_enumerate_with_progress_bar(@sync_repos, "正在同步(锁定)以上仓库...") { |light_repo|
repo = Repo.generate_strictly(Workspace.root, light_repo)
error_message = Repo::SyncHelper.sync_exist_repo(repo, light_repo)
if !error_message.nil?
Lock.mutex_exec { error_repos[light_repo.name] = error_message }
end
}
end
if @update_repos.length > 0
Workspace.concurrent_enumerate_with_progress_bar(@update_repos, "正在更新以上仓库...") { |light_repo|
repo = Repo.generate_strictly(Workspace.root, light_repo)
success, output = repo.execute_git_cmd('pull', '')
if !success
Lock.mutex_exec { error_repos[light_repo.name] = output }
end
}
end
if @download_repos.length > 0
Workspace.sync_new_repos(@download_repos)
end
if error_repos.length > 0
Workspace.show_error(error_repos)
else
Output.puts_succeed_cmd(argv.absolute_cmd)
end
end
def prepare_repo_category
@sync_repos = []
@update_repos = []
@download_repos = []
end
# M/NM: 被/不被mgit管理
# E/NE: 仓库本地存在/不存在
# L/NL: 仓库被/不被锁定
# U/NU: 仓库有/没有远程地址
# mgit sync repo1 repo2 ... [--pull]
# 针对一组指定仓库进行:下载仓库:NE|U,同步仓库:E|L,更新仓库: E|U(可选)
def setup_specified_repos(argv)
prepare_repo_category
should_pull = argv.opt_list.did_set_opt?(OPT_LIST[:pull])
valid_repos, error_repo_names = check_valid_repos(parse_repo_name(argv))
valid_repos.each { |light_repo|
repo_exist = Dir.exist?(light_repo.abs_dest(Workspace.root))
if !repo_exist
if !light_repo.url.nil?
@download_repos.push(light_repo)
else
error_repo_names.push(light_repo.name)
end
else
if light_repo.lock
@sync_repos.push(light_repo)
end
if should_pull
if !light_repo.url.nil?
@update_repos.push(light_repo)
else
error_repo_names.push(light_repo.name)
end
end
end
}
should_continue = true
error_repos = []
error_repos.push(['配置表中未定义(或未指定"remote-path")', error_repo_names]) if error_repo_names.length > 0
Output.interact_with_multi_selection_combined_repos(error_repos, "以上仓库状态异常", ['a: 跳过并继续', 'b: 终止']) { |input|
if input == 'b'
Output.puts_cancel_message
should_continue = false
end
} if error_repos.length > 0
return should_continue
end
# mgit sync [--pull]
# 下载仓库:M|NE|U, 同步仓库:M|E|L, 更新仓库:M|E|U(可选)
def setup_normal_reops(argv)
prepare_repo_category
should_pull = argv.opt_list.did_set_opt?(OPT_LIST[:pull])
Workspace.config.light_repos.each { |light_repo|
if !light_repo.mgit_excluded
repo_exist = Dir.exist?(light_repo.abs_dest(Workspace.root))
if !repo_exist && !light_repo.url.nil?
@download_repos.push(light_repo)
elsif repo_exist && light_repo.lock
@sync_repos.push(light_repo)
end
if repo_exist && should_pull && !light_repo.url.nil?
@update_repos.push(light_repo)
end
end
}
end
# mgit sync --new
# 下载仓库:M|NE|U
def setup_new_reops
prepare_repo_category
Workspace.config.light_repos.each { |light_repo|
if !light_repo.mgit_excluded
repo_exist = Dir.exist?(light_repo.abs_dest(Workspace.root))
@download_repos.push(light_repo) if !repo_exist && !light_repo.url.nil?
end
}
end
# mgit sync --all [--pull]
# 下载仓库:NE|U,同步仓库:E|L,更新仓库: E|NL|U(可选)
def setup_all_sync_reops(argv)
prepare_repo_category
Workspace.config.light_repos.each { |light_repo|
repo_exist = Dir.exist?(light_repo.abs_dest(Workspace.root))
if !repo_exist && !light_repo.url.nil?
@download_repos.push(light_repo)
elsif repo_exist && light_repo.lock
@sync_repos.push(light_repo)
elsif repo_exist && !light_repo.url.nil? && argv.opt_list.did_set_opt?(OPT_LIST[:pull])
@update_repos.push(light_repo)
end
}
end
# mgit sync --clone repo1 repo2 ...
# 下载仓库:NE|U(指定)
def setup_download_reops(repo_names)
prepare_repo_category
existing_repos = []
valid_repos, error_repo_names = check_valid_repos(repo_names)
valid_repos.each { |light_repo|
repo_exist = Dir.exist?(light_repo.abs_dest(Workspace.root))
if !repo_exist
if !light_repo.url.nil?
@download_repos.push(light_repo)
else
error_repo_names.push(light_repo.name)
end
else
error_repo_names.push(light_repo.name) if light_repo.url.nil?
existing_repos.push(light_repo.name)
end
}
should_continue = true
error_repos = []
error_repos.push(['配置表中未定义(或未指定"remote-path")', error_repo_names]) if error_repo_names.length > 0
error_repos.push(['本地已经存在', existing_repos]) if existing_repos.length > 0
Output.interact_with_multi_selection_combined_repos(error_repos, "以上仓库状态异常", ['a: 跳过并继续', 'b: 终止']) { |input|
if input == 'b'
Output.puts_cancel_message
should_continue = false
end
} if error_repos.length > 0
return should_continue
end
# mgit sync -u
# 同步仓库:E | url不一致
def setup_config_url_repos()
prepare_repo_category
warning_repos = []
missing_repos = []
Workspace.config.light_repos.each { |light_repo|
if !light_repo.mgit_excluded
repo, _ = Repo.generate_softly(Workspace.root, light_repo)
if !repo.nil?
original_url = repo.status_checker.default_url
target_url = light_repo.url
warning_repos.push(light_repo) if !Utils.url_consist?(original_url, target_url)
else
missing_repos.push(light_repo)
end
end
}
Output.puts_remind_block(missing_repos.map { |repo| repo.name }, "以上仓库本地缺失,无法校验URL,请执行\"mgit sync -n\"重新下载后重试!") if missing_repos.length > 0
if warning_repos.length > 0
if Output.continue_with_interact_repos?(warning_repos.map { |repo| repo.name }, "以上仓库的当前URL(origin)和配置表指定URL不一致,建议\n 1. 执行\"mgit delete repo1 repo2...\"删除仓库.\n 2. 执行\"mgit sync -n\"重新下载。\n 继续强制设置可能导致仓库出错,是否继续?")
@sync_repos = warning_repos
else
Output.puts_cancel_message
exit
end
end
end
def check_valid_repos(repo_names)
specified_repos = repo_names.map { |name| name.downcase }
all_valid_repos = Workspace.config.light_repos.map { |light_repo| light_repo.name.downcase }
error_repo_names = specified_repos - all_valid_repos
valid_repo_names = specified_repos - error_repo_names
valid_repos = Workspace.config.light_repos.select { |light_repo|
valid_repo_names.include?(light_repo.name.downcase)
}
[valid_repos, error_repo_names]
end
def parse_repo_name(argv)
return if argv.git_opts.nil?
repos = argv.git_opts.split(' ')
extra_opts = repos.select { |e| argv.is_option?(e) }
Foundation.help!("输入非法参数:#{extra_opts.join(',')}。请通过\"mgit #{argv.cmd} --help\"查看用法。") if extra_opts.length > 0
Foundation.help!("未输入查询仓库名!请使用这种形式查询:mgit info repo1 repo2 ...") if repos.length == 0
repos.map { |e| e.downcase }
end
def enable_short_basic_option
true
end
def self.description
"根据配置表(从远端或本地)同步仓库到工作区,包括被锁定仓库,已经在工作的不作处理(默认不执行pull)。"
end
def self.usage
"mgit sync [-a|-n|-c] [<repo>...] [-p] [-o] [-h]"
end
end
end
================================================
FILE: lib/m-git/command/tag.rb
================================================
#coding=utf-8
module MGit
# @!scope 类似 git tag
#
class Tag < BaseCommand
def execute(argv)
Workspace.check_branch_consistency
Output.puts_start_cmd
exec_repos = all_repos + locked_repos
if argv.git_opts.empty?
_, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, exec_repos)
Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0
return
end
error_repos = {}
no_tag_repos = []
exec_repos.each { |repo|
success, output = repo.execute_git_cmd(argv.cmd, argv.git_opts)
if success
tags = output.split("\n")
if tags.length > 0
puts Output.generate_title_block(repo.name) {
Output.generate_table(tags, separator:"")
}
else
no_tag_repos.push(repo.name)
end
else
error_repos[repo.name] = output
end
}
if no_tag_repos.length > 0
puts "\n"
Output.puts_remind_block(no_tag_repos, "以上仓库尚未创建tag!")
end
if error_repos.length > 0
Workspace.show_error(error_repos)
else
Output.puts_succeed_cmd(argv.absolute_cmd)
end
end
def enable_repo_selection
true
end
def self.description
"增删查或验证标签。增加标签示例:mgit tag -a 'v0.0.1' -m 'Tag description message'"
end
def self.usage
"mgit tag [<git-tag-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]"
end
end
end
================================================
FILE: lib/m-git/command_manager.rb
================================================
module MGit
class CommandManager
class << self
# cmd generate
#
def commands
@commands ||= {}
end
def register_command(cmd, cls)
commands[cmd] = cls
end
def [](cmd)
class_with_command(cmd)
end
def class_with_command(cmd)
commands[cmd]
end
end
end
end
================================================
FILE: lib/m-git/error.rb
================================================
#coding=utf-8
module MGit
MGIT_ERROR_TYPE = {
:config_name_error => 'config_name_error',
:config_generate_error => 'config_generate_error'
}.freeze
class Error < StandardError
attr_reader :msg
attr_reader :type
def initialize(msg, type:nil)
@msg = msg
@type = type
end
end
end
================================================
FILE: lib/m-git/foundation/constants.rb
================================================
#coding=utf-8
module MGit
module Constants
PROJECT_DIR = {
:hooks => '.mgit/hooks',
:source_config => '.mgit/source-config',
:source_git => '.mgit/source-git',
:snapshot => '.mgit/snapshot',
:log_dir => '.mgit/logs'
}.freeze
HOOK_NAME = {
:pre_hook => 'pre_hook.rb',
:post_hook => 'post_hook.rb',
:manifest_hook => 'manifest_hook.rb',
:post_download_hook => 'post_download_hook.rb',
:pre_push_hook => 'pre_push_hook.rb',
:pre_exec_hook => 'pre_exec_hook.rb'
}.freeze
MGIT_CONFIG_PATH = ".mgit/config.yml"
CONFIG_FILE_NAME = {
:manifest => 'manifest.json',
:manifest_cache => '.manifest_cache.json',
:local_manifest => 'local_manifest.json'
}.freeze
# 全局配置
CONFIG_KEY = {
# [String] 包含仓库的文件夹相对.mgit目录路径,完整路径如:<.mgit所在路径>/dest/<repo_name>,与abs-dest同时指定时无效
:dest => 'dest',
# [Boolean] 是否将所有仓库排除mgit管理
:mgit_excluded => 'mgit-excluded',
# [String] 远程仓库根目录,完整URL:remote/remote_path
:remote => 'remote',
# [String] 远程仓库相对目录,完整URL:remote/remote_path
:repositories => 'repositories',
# [String] mgit版本
:version => 'version',
}.freeze
# 仓库配置
REPO_CONFIG_KEY = {
# [String] 仓库完整路径
:abs_dest => 'abs-dest',
# [Boolean] 是否是配置仓库
:config_repo => 'config-repo',
# [String] 包含仓库的文件夹相对.mgit目录路径,完整路径如:<.mgit所在路径>/dest/<repo_name>,与abs-dest同时指定时无效
:dest => 'dest',
# [Boolean] 是否是占位仓库,占位操作不会让mgit进行常规操作(隐含指定mgit_excluded为true),标记为占位的仓库组装器组装时不需要使用
:dummy => 'dummy',
# [Json] 锁定状态,见REPO_CONFIG_LOCK_KEY
:lock => 'lock',
# [Boolean] 是否排除mgit管理,被排除的仓库不会让mgit进行常规操作,若未标记dummy,则为不被mgit管理,但组装器需要的仓库
:mgit_excluded => 'mgit-excluded',
# [String] 远程仓库根目录,完整URL:remote/remote_path
:remote => 'remote',
# [String] 远程仓库相对目录,完整URL:remote/remote_path
:remote_path => 'remote-path'
}.freeze
# 必须全局字段
REQUIRED_CONFIG_KEY = [
CONFIG_KEY[:remote],
CONFIG_KEY[:version],
CONFIG_KEY[:dest],
CONFIG_KEY[:repositories]
].freeze
# 必须仓库配置字段
REQUIRED_REPO_CONFIG_KEY = [
REPO_CONFIG_KEY[:remote_path],
].freeze
REPO_CONFIG_LOCK_KEY = {
:branch => 'branch',
:commit_id => 'commit-id',
:tag => 'tag'
}.freeze
SNAPSHOT_KEY = {
:time_stamp => 'time-stamp',
:message => 'message',
:snapshot => 'snapshot'
}.freeze
CONFIG_CACHE_KEY = {
:hash => 'hash',
:cache => 'cache'
}.freeze
# 定义字段用于调用git(shell)指令前export,便于仓库git hook区分来进行其余操作
MGIT_EXPORT_INFO = {
# 是否是MGit操作的git指令
:MGIT_TRIGGERRED => 1,
# 本次MGit操作的唯一标示
:MGIT_OPT_ID => SecureRandom.uuid
}.freeze
# 临时中央仓库名
CENTRAL_REPO = "Central".freeze
INIT_CACHE_DIR_NAME = "pmet_tini_tigm".freeze
end
end
================================================
FILE: lib/m-git/foundation/dir.rb
================================================
class Dir
# 检查传入路径是不是git仓库
#
# @param path [String] 仓库路径
#
def self.is_git_repo?(path)
return false unless File.directory?(path)
git_dir = File.join(path, '.git')
File.directory?(git_dir)
end
def self.is_in_git_repo?(path)
check_path = path
result = is_git_repo?(check_path)
while !result
check_path = File.dirname(check_path)
break if check_path == '/'
result = is_git_repo?(check_path)
end
result
end
end
================================================
FILE: lib/m-git/foundation/duration_recorder.rb
================================================
#coding=utf-8
module MGit
# 计时器
#
class DurationRecorder
DEFAULT_DURATION_KEY = 'default_duration_key'
module Status
# 停止态
IDLE = 0
# 暂停态
PAUSE = 1
# 计时中
RUNNING = 2
end
include Status
@@status = {}
@@duration = {}
@@time_stamp = {}
@@lock = Mutex.new
def self.start(duration_key:DEFAULT_DURATION_KEY, use_lock:false)
mutex_exec(use_lock) {
status = IDLE if @@status[duration_key].nil?
if status == IDLE
@@duration[duration_key] = 0.0
@@time_stamp[duration_key] = Time.new.to_f
@@status[duration_key] = RUNNING
else
puts '需要停止计时后重新开始计时'
end
}
end
def self.pause(duration_key:DEFAULT_DURATION_KEY, use_lock:false)
mutex_exec(use_lock) {
if @@status[duration_key] == RUNNING
current = Time.new.to_f
@@duration[duration_key] += (current - @@time_stamp[duration_key])
@@time_stamp.delete(duration_key)
@@status[duration_key] = PAUSE
end
}
end
def self.resume(duration_key:DEFAULT_DURATION_KEY, use_lock:false)
mutex_exec(use_lock) {
if @@status[duration_key] == PAUSE
@@time_stamp[duration_key] = Time.new.to_f
@@status[duration_key] = RUNNING
end
}
end
def self.end(duration_key:DEFAULT_DURATION_KEY, use_lock:false)
mutex_exec(use_lock) {
if @@status[duration_key] != IDLE
current = Time.new.to_f
@@duration[duration_key] += (current - @@time_stamp[duration_key])
@@time_stamp.delete(duration_key)
@@status[duration_key] = IDLE
return @@duration[duration_key]
else
puts '没有需要停止的计时'
end
}
end
private
# 多线程执行保护
#
# @param use_lock [Boolean] 执行是否加锁
#
def self.mutex_exec(use_lock)
if use_lock
@@lock.lock
yield if block_given?
@@lock.unlock
else
yield if block_given?
end
end
end
end
================================================
FILE: lib/m-git/foundation/git_message_parser.rb
================================================
module MGit
# Git interact message parse with git-server
#
class GitMessageParser
def initialize(url)
@url = url
end
# @return [String] error message
# @return [nil] none error
#
def parse_fetch_msg(input)
parse_pull_msg(input)
end
# @return [String] error message
# @return [nil] none error
#
def parse_pull_msg(input)
__default_parse_msg(input)
end
# @return [String] error message
# @return [nil] none error
#
def parse_push_msg(input)
__default_parse_msg(input)
end
# @return [String] codereview的url地址
# 默认无解析
def parse_code_review_url(input)
nil
end
def __default_parse_msg(msg)
return if msg.nil? || msg.empty?
key_word = 'error:'
error_line = msg.split("\n").find {|line|
# 解析 "error: failed to push some refs..."
line.include?(key_word)
}
msg if error_line
end
end
end
================================================
FILE: lib/m-git/foundation/lock.rb
================================================
module MGit
class Lock
class << self
# @!scope 互斥执行锁
# @example
# mutex_exec do
# exec..
# end
def mutex_exec
@mutex = Mutex.new if @mutex.nil?
@mutex.lock
yield if block_given?
@mutex.unlock
end
# @!scope 互斥显示锁
# @example
# mutex_puts do
# exec..
# end
def mutex_puts
@mutex_puts = Mutex.new if @mutex_puts.nil?
@mutex_puts.lock
yield if block_given?
@mutex_puts.unlock
end
end
end
end
================================================
FILE: lib/m-git/foundation/loger.rb
================================================
#coding=utf-8
require 'logger'
require 'fileutils'
module MGit
# 日志模块配置
class Loger
MGIT_LOG_FILE_NAME = 'mgit.log'
#
# 配置Log模块
#
# @param root [String] 工程根目录
#
def self.config(root)
# 是否开启log
begin
log_enable = MGit::MGitConfig.query_with_key(root, :logenable)
rescue MGitClass::Error => e
log_enable = TRUE
end
MGit::Loger.set_log_enable(log_enable)
# 配置log路径
log_dir = File.join(root, MGit::Constants::PROJECT_DIR[:log_dir])
FileUtils.mkdir_p(log_dir) if !Dir.exist?(log_dir)
file_path = File.join(log_dir, MGIT_LOG_FILE_NAME)
MGit::Loger.set_log_file(file_path)
# 配置log的level
begin
log_level = MGit::MGitConfig.query_with_key(root, :loglevel)
rescue MGitClass::Error => e
log_level = 1
end
MGit::Loger.set_log_level(log_level)
end
end
# 日志模块
class Loger
DEFAULT_SHIFT_SIZE = 1048576 #每个文件1M
DEFAULT_SHIFT_AGE = 10 # 保留10个文件
DEFAULT_LOG_FILE = "./mgit.log"
#
# 设置mgit log是否开启 默认开启
#
# @param log_enable [Boolean] 是否开启log日志
#
def self.set_log_enable(log_enable)
@log_enable = log_enable
end
#
# 设置mgit log的打印等级
#
# @param level [Enumerable]: Logger::DEBUG | Logger::INFO | Logger::ERROR | Logger::FATAL
#
def self.set_log_level(level)
self.logger.level = level
end
#
# 设置mgit log的文件名, 默认路径在工作区的 .mgit/logs/mgit.log
#
def self.set_log_file(file_path)
@log_file = file_path
end
#
# 打印 DEBUG类型的log
#
def self.debug(message)
self.logger.debug(message) if @log_enable
end
#
# 打印 DEBUG类型的log
#
def self.info(message)
self.logger.info(message) if @log_enable
end
#
# 打印 WARN类型的log
#
def self.warn(message)
self.logger.warn(message) if @log_enable
end
#
# 打印 ERROR类型的log
#
def self.error(message)
self.logger.error(message) if @log_enable
end
#
# 打印 FATAL类型的log
#
def self.fatal(message)
self.logger.fatal(message) if @log_enable
end
private
def self.logger
unless @logger
@log_enable ||= TRUE
@log_level ||= Logger::INFO
@log_file ||= DEFAULT_LOG_FILE
@logger = Logger.new(@log_file, shift_age = DEFAULT_SHIFT_AGE, shift_size = DEFAULT_SHIFT_SIZE)
@logger.level = @log_level
@logger.datetime_format = '%Y-%m-%d %H:%M:%S'
@logger.formatter = proc do | severity, datetime, progname, msg|
"#{datetime} - #{severity} - : #{msg}\n"
end
end
@logger
end
end
end
================================================
FILE: lib/m-git/foundation/mgit_config.rb
================================================
#coding=utf-8
require_relative 'utils'
module MGit
class MGitConfig
CONFIG_KEY = {
:managegit => {
:info => '是否将.git实体托管给MGit,托管后仓库内的.git目录是软链 (true/false)',
:type => 'Boolean',
:default => true
},
:maxconcurrentcount => {
:info => 'MGit操作的最大并发数? (Integer)',
:type => 'Integer',
:default => MGit::Utils.logical_cpu_num
},
:syncworkspace => {
:info => '是否按照配置表同步工作区? (true/false)',
:type => 'Boolean',
:default => true
},
:savecache => {
:info => '同步工作区时回收的仓库是否保留到缓存中? (true/false)',
:type => 'Boolean',
:default => false
},
:logenable => {
:info => '是否开启日志打印,操作日志会收集到.mgit/log目录下? (true/false)',
:type => 'Boolean',
:default => true
},
:loglevel => {
:info => '日志的打印级别, DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, FATAL = 4',
:type => 'Integer',
:default => 1
}
}
class << self
# 查询配置
#
# @param root [String] 工程根目录
#
# @raise [MGit::Error] 异常错误
#
def query(root)
config, error = __load_file(root)
if !error.nil?
raise Error.new(error)
return
elsif block_given?
# 如果文件存在但无内容,此时读取到的数据类型是FalseClass,此处统一规范化
config = {} if !config.is_a?(Hash)
yield(config)
end
end
# Description of #query_with_key
#
# @param root [String] 工程根目录
#
# @param key_symbol [Symbol] 符号key值
#
# @return [Object] 配置值
#
# @raise [MGit::Error] 异常错误
def query_with_key(root, key_symbol)
query(root) { |config|
if !config[key_symbol.to_s].nil?
return config[key_symbol.to_s]
elsif !CONFIG_KEY[key_symbol].nil?
return CONFIG_KEY[key_symbol][:default]
else
return nil
end
}
end
# 更新配置
#
# @param root [String] 工程根目录
#
# @raise [MGit::Error] 异常错误
#
def update(root)
# 加载配置
config, error = __load_file(root)
if !error.nil?
raise Error.new(error)
return
end
# 如果文件存在但无内容,此时读取到的数据类型是FalseClass,此处统一规范化
config = {} if !config.is_a?(Hash)
# 更新
yield(config) if block_given?
# 更新完后校验格式
if !config.is_a?(Hash)
raise Error.new("工具配置更新数据格式错误,更新失败!")
return
end
# 写回配置
error = write_to_file(root, config)
if !error.nil?
raise Error.new(error)
return
end
end
# 列出所有配置
#
# @param root [String] 工程根目录
#
def dump_config(root)
query(root) { |config|
CONFIG_KEY.each_with_index { |(key, value_dict), index|
line = "#{Output.blue_message("[#{value_dict[:info]}]")}\n#{key.to_s}: "
set_value = config[key.to_s]
if !set_value.nil?
line += "#{set_value}\n\n"
else
line += "#{value_dict[:default]}\n\n"
end
puts line
}
}
end
# 验证一个value的值是否符合key的类型
#
# @param root [String] 工程根目录
#
# @param key [String] 字符串key值
#
# @param value [String] 字符串格式的值
#
# @return [Object] key值对应的合法类型value值
#
def to_suitable_value_for_key(root, key, value)
return unless CONFIG_KEY.keys.include?(key.to_sym)
return nil if !value.is_a?(String)
key_dict = CONFIG_KEY[key.to_sym]
set_value = nil
# 是否是数字
if key_dict[:type] == 'Integer' && value.to_i.to_s == value
set_value = value.to_i
# 是否是true
elsif key_dict[:type] == 'Boolean' && value.to_s.downcase == 'true'
set_value = true
# 是否是false
elsif key_dict[:type] == 'Boolean' && value.to_s.downcase == 'false'
set_value = false
# 是否是其他字符串
elsif key_dict[:type] == 'String' && value.is_a?(String)
set_value = value
end
set_value
end
private
# 加载mgit配置文件
#
# @param root [String] 工程根目录
#
# @return [(Hash, Boolean)] (配置内容,错误信息)
#
def __load_file(root)
config_path = File.join(root, Constants::MGIT_CONFIG_PATH)
if File.exists?(config_path)
begin
return YAML.load_file(config_path), nil
rescue => e
return nil, "工具配置文件\"#{File.basename(config_path)}\"读取失败,原因:\n#{e.message}"
end
end
[nil, nil]
end
# 将配置写回文件
#
# @param root [String] 工程根目录
#
# @param content [Hash] 新配置
#
# @return [String] 错误信息
#
def write_to_file(root, content)
config_path = File.join(root, Constants::MGIT_CONFIG_PATH)
begin
FileUtils.rm_f(config_path) if File.exist?(config_path)
dir_name = File.dirname(config_path)
FileUtils.mkdir_p(dir_name) if !Dir.exist?(dir_name)
file = File.new(config_path, 'w')
if !file.nil?
file.write(content.to_yaml)
file.close
end
return nil
rescue => e
return "工具配置文件\"#{File.basename(config_path)}\"写入失败,原因:\n#{e.message}"
end
end
end
end
end
================================================
FILE: lib/m-git/foundation/operation_progress_manager.rb
================================================
#coding=utf-8
module MGit
# 本类用于缓存/加载操作中间态
class OperationProgressContext
# 现场信息关键字段
CONTEXT_CMD = 'cmd'
CONTEXT_OPTS = 'opts'
CONTEXT_BRANCH = 'branch'
CONTEXT_REPOS = 'repos'
CONTEXT_OTHER = 'other'
attr_accessor :type # String: 本次进入中间态的操作类型,可自定义,如'merge_in_progress','rebase_in_progress'等。该字段用于索引缓存的中间态信息,需要唯一。
attr_accessor :cmd # String: 本次执行指令,如'merge','checkout'等
attr_accessor :opts # String: 本次执行参数,如'--no-ff','--ff'等
attr_accessor :branch # String: 仓库当前分支
attr_accessor :repos # [String]: 本次执行哪些仓库的名称数组
attr_accessor :other # Object: 其他信息,可自定义
def initialize(type)
self.type = type
self.other = {}
end
# 将中间态对象序列化为Hash
def serialize
return {
CONTEXT_CMD => self.cmd,
CONTEXT_OPTS => self.opts,
CONTEXT_BRANCH => self.branch,
CONTEXT_REPOS => self.repos,
CONTEXT_OTHER => self.other
}
end
# 反序列化
#
# @param dict [Hash] 中间态Hash
#
def deserialize(dict)
self.cmd = dict[CONTEXT_CMD]
self.opts = dict[CONTEXT_OPTS]
self.branch = dict[CONTEXT_BRANCH]
self.repos = dict[CONTEXT_REPOS]
self.other = dict[CONTEXT_OTHER]
end
# 校验中间态是否合法,仓库可缺省,若缺省则表示所有仓库
#
# @return [Boolean] 是否合法
#
def validate?
return !self.cmd.nil? && !self.opts.nil? && !self.branch.nil?
end
end
class OperationProgressManager
PROGRESS_TYPE = {
:merge => 'merge_in_progress',
:rebase => 'rebase_in_progress',
:pull => 'pull_in_progress'
}.freeze
class << self
# 进入中间态
#
# @param root [String] 多仓库根目录
#
# @param context [OperationProgressContext] 中间态对象
#
def trap_into_progress(root, context)
begin
MGitConfig.update(root) { |config|
config[context.type] = context.serialize
}
rescue Error => e
Output.puts_fail_message(e.msg)
end
end
# 删除中间态
#
# @param root [String] 多仓库根目录
#
# @param type [String] 自定义Key值,用于索引中间态信息
#
def remove_progress(root, type)
begin
MGitConfig.update(root) { |config|
config.delete(type)
}
rescue Error => e
Output.puts_fail_message(e.msg)
end
end
# 是否处于中间态中
def is_in_progress?(root, type)
is_in_progress = false
begin
MGitConfig.query(root) { |config|
is_in_progress = !config[type].nil?
gitextract_p54qz427/
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── docs/
│ ├── commands/
│ │ └── commands.md
│ ├── config/
│ │ ├── config-env.md
│ │ └── manifest.md
│ ├── design/
│ │ ├── design.md
│ │ ├── principle.md
│ │ └── workspace.md
│ ├── extension/
│ │ ├── cmd_ext.md
│ │ ├── hooks.md
│ │ ├── plugins.md
│ │ └── submodule.md
│ ├── faq/
│ │ └── faq.md
│ ├── references.md
│ └── use/
│ ├── common-commands.md
│ └── how-to-start.md
├── lib/
│ ├── m-git/
│ │ ├── argv/
│ │ │ ├── opt.rb
│ │ │ ├── opt_list.rb
│ │ │ └── parser.rb
│ │ ├── argv.rb
│ │ ├── base_command.rb
│ │ ├── command/
│ │ │ ├── add.rb
│ │ │ ├── branch.rb
│ │ │ ├── checkout.rb
│ │ │ ├── clean.rb
│ │ │ ├── commit.rb
│ │ │ ├── config.rb
│ │ │ ├── delete.rb
│ │ │ ├── fetch.rb
│ │ │ ├── forall.rb
│ │ │ ├── info.rb
│ │ │ ├── init.rb
│ │ │ ├── log.rb
│ │ │ ├── merge.rb
│ │ │ ├── pull.rb
│ │ │ ├── push.rb
│ │ │ ├── rebase.rb
│ │ │ ├── reset.rb
│ │ │ ├── self.rb
│ │ │ ├── stash.rb
│ │ │ ├── status.rb
│ │ │ ├── sync.rb
│ │ │ └── tag.rb
│ │ ├── command_manager.rb
│ │ ├── error.rb
│ │ ├── foundation/
│ │ │ ├── constants.rb
│ │ │ ├── dir.rb
│ │ │ ├── duration_recorder.rb
│ │ │ ├── git_message_parser.rb
│ │ │ ├── lock.rb
│ │ │ ├── loger.rb
│ │ │ ├── mgit_config.rb
│ │ │ ├── operation_progress_manager.rb
│ │ │ ├── timer.rb
│ │ │ └── utils.rb
│ │ ├── foundation.rb
│ │ ├── hooks_manager.rb
│ │ ├── manifest/
│ │ │ ├── cache_manager.rb
│ │ │ ├── internal.rb
│ │ │ ├── light_repo.rb
│ │ │ ├── light_repo_generator.rb
│ │ │ ├── linter.rb
│ │ │ └── manifest.sample
│ │ ├── manifest.rb
│ │ ├── open_api/
│ │ │ └── script_download_info.rb
│ │ ├── open_api.rb
│ │ ├── output/
│ │ │ └── output.rb
│ │ ├── plugin/
│ │ │ ├── completion.bash
│ │ │ ├── completion.zsh
│ │ │ └── init.sh
│ │ ├── plugin_manager.rb
│ │ ├── repo/
│ │ │ ├── status.rb
│ │ │ └── sync_helper.rb
│ │ ├── repo.rb
│ │ ├── template/
│ │ │ ├── local_manifest.rb
│ │ │ ├── manifest_hook.rb
│ │ │ ├── post_download_hook.rb
│ │ │ ├── post_hook.rb
│ │ │ ├── pre_exec_hook.rb
│ │ │ ├── pre_hook.rb
│ │ │ └── pre_push_hook.rb
│ │ ├── template.rb
│ │ ├── version.rb
│ │ ├── workspace/
│ │ │ ├── path_helper.rb
│ │ │ └── workspace_helper.rb
│ │ └── workspace.rb
│ └── m-git.rb
├── m-git.gemspec
├── manifest.json
├── mgit
└── test/
├── argv/
│ ├── test_opt.rb
│ ├── test_opt_list.rb
│ └── test_parser.rb
├── foundation/
│ ├── test_dir.rb
│ ├── test_mgit_config.rb
│ └── test_utils.rb
├── manifest/
│ └── test_light_repo.rb
├── test_argv.rb
├── test_foundation.rb
├── test_helper.rb
├── test_manifest.rb
├── test_open_api.rb
├── test_plugin_manager.rb
└── test_workspace.rb
SYMBOL INDEX (674 symbols across 68 files)
FILE: lib/m-git.rb
type MGit (line 19) | module MGit
function run (line 47) | def run(raw_argv)
type MGit (line 41) | module MGit
function run (line 47) | def run(raw_argv)
FILE: lib/m-git/argv.rb
type MGit (line 3) | module MGit
class ARGV (line 6) | class ARGV
method initialize (line 30) | def initialize(cmd, pure_opts, absolute_cmd, raw_opts)
method register_opts (line 38) | def register_opts(opts)
method resolve! (line 44) | def resolve!
method update_opt (line 59) | def update_opt(key, value, priority:nil, info:nil)
method opt (line 76) | def opt(key)
method info (line 81) | def info(key)
method git_opts (line 92) | def git_opts(raw: true)
method enumerate_valid_opts (line 102) | def enumerate_valid_opts
method is_option? (line 110) | def is_option?(opt_str)
method show_detail (line 115) | def show_detail
method show_info (line 126) | def show_info
method __resolve_git_opts (line 137) | def __resolve_git_opts
method __resolve_raw_opts (line 150) | def __resolve_raw_opts
FILE: lib/m-git/argv/opt.rb
type MGit (line 3) | module MGit
class ARGV (line 5) | class ARGV
class Opt (line 10) | class Opt
method initialize (line 21) | def initialize(key, default:nil, short_key:nil, priority:-1, info:...
method empty? (line 27) | def empty?
method validate? (line 31) | def validate?
FILE: lib/m-git/argv/opt_list.rb
type MGit (line 3) | module MGit
class ARGV (line 4) | class ARGV
class OptList (line 6) | class OptList
method initialize (line 13) | def initialize(opts)
method valid_opts (line 17) | def valid_opts
method opt (line 27) | def opt(key)
method did_set_opt? (line 36) | def did_set_opt?(key)
method registered_opt (line 48) | def registered_opt(key)
method did_register_opt? (line 56) | def did_register_opt?(key)
method opts_ordered_by_priority (line 64) | def opts_ordered_by_priority
FILE: lib/m-git/argv/parser.rb
type MGit (line 2) | module MGit
class ARGV (line 3) | class ARGV
type Parser (line 4) | module Parser
function parse (line 15) | def self.parse(argv)
FILE: lib/m-git/base_command.rb
type MGit (line 6) | module MGit
class BaseCommand (line 7) | class BaseCommand
method inherited (line 9) | def self.inherited(sub_klass)
method cmd (line 14) | def self.cmd
method initialize (line 45) | def initialize(argv)
method run (line 55) | def run
method all_repos (line 67) | def all_repos(except_config:false)
method locked_repos (line 71) | def locked_repos
method exec_light_repos (line 75) | def exec_light_repos
method generate_config_repo (line 79) | def generate_config_repo
method __config_repo_filter (line 84) | def __config_repo_filter
method setup_argv (line 93) | def setup_argv(argv)
method process_highest_priority_option (line 104) | def process_highest_priority_option(argv)
method pre_exec (line 115) | def pre_exec
method post_exec (line 136) | def post_exec
method enable_auto_execution (line 145) | def enable_auto_execution
method enable_repo_selection (line 150) | def enable_repo_selection
method enable_short_basic_option (line 155) | def enable_short_basic_option
method enable_lock_operation (line 160) | def enable_lock_operation
method include_lock_by_default (line 165) | def include_lock_by_default
method enable_continue_operation (line 170) | def enable_continue_operation
method enable_abort_operation (line 175) | def enable_abort_operation
method did_interrupt (line 180) | def did_interrupt
method description (line 184) | def self.description
method usage (line 188) | def self.usage
method execute (line 194) | def execute(argv)
method options (line 202) | def options
method revise_option_value (line 246) | def revise_option_value(opt)
method validate (line 251) | def validate(argv)
method mgit_try_to_continue? (line 255) | def mgit_try_to_continue?
method mgit_try_to_abort? (line 259) | def mgit_try_to_abort?
method usage (line 264) | def usage(argv)
FILE: lib/m-git/command/add.rb
type MGit (line 3) | module MGit
class Add (line 9) | class Add < BaseCommand
method execute (line 13) | def execute(argv)
method enable_repo_selection (line 23) | def enable_repo_selection
method description (line 29) | def self.description
method usage (line 35) | def self.usage
FILE: lib/m-git/command/branch.rb
type MGit (line 3) | module MGit
class Branch (line 9) | class Branch < BaseCommand
method options (line 15) | def options
method execute (line 21) | def execute(argv)
method extcute_as_common (line 34) | def extcute_as_common(argv)
method show_compact_branches (line 54) | def show_compact_branches(argv)
method show_branches_for_repos (line 61) | def show_branches_for_repos(repos, locked)
method enable_repo_selection (line 77) | def enable_repo_selection
method description (line 81) | def self.description
method usage (line 85) | def self.usage
FILE: lib/m-git/command/checkout.rb
type MGit (line 3) | module MGit
class Checkout (line 9) | class Checkout < BaseCommand
method execute (line 11) | def execute(argv)
method checkout_config_repo (line 60) | def checkout_config_repo(cmd, opts, repo)
method remind_config_repo_fail (line 85) | def remind_config_repo_fail(msg)
method enable_repo_selection (line 92) | def enable_repo_selection
method description (line 96) | def self.description
method usage (line 100) | def self.usage
FILE: lib/m-git/command/clean.rb
type MGit (line 3) | module MGit
class Clean (line 9) | class Clean < BaseCommand
method execute (line 11) | def execute(argv)
method validate (line 42) | def validate(argv)
method enable_repo_selection (line 46) | def enable_repo_selection
method enable_short_basic_option (line 50) | def enable_short_basic_option
method description (line 54) | def self.description
method usage (line 58) | def self.usage
FILE: lib/m-git/command/commit.rb
type MGit (line 3) | module MGit
class Commit (line 9) | class Commit < BaseCommand
method validate (line 11) | def validate(argv)
method execute (line 21) | def execute(argv)
method enable_repo_selection (line 70) | def enable_repo_selection
method description (line 74) | def self.description
method usage (line 78) | def self.usage
FILE: lib/m-git/command/config.rb
type MGit (line 3) | module MGit
class Config (line 9) | class Config < BaseCommand
method options (line 24) | def options
method validate (line 48) | def validate(argv)
method pre_exec (line 57) | def pre_exec
method post_exec (line 66) | def post_exec
method execute (line 73) | def execute(argv)
method update_mgit_config (line 102) | def update_mgit_config(config_path)
method update_local_config (line 124) | def update_local_config(config_path)
method create_local_config (line 138) | def create_local_config(dir)
method dump_config (line 157) | def dump_config
method set_config (line 166) | def set_config(key_value_arr)
method enable_short_basic_option (line 190) | def enable_short_basic_option
method description (line 194) | def self.description
method usage (line 198) | def self.usage
FILE: lib/m-git/command/delete.rb
type MGit (line 3) | module MGit
class Delete (line 9) | class Delete < BaseCommand
method pre_exec (line 12) | def pre_exec
method post_exec (line 21) | def post_exec
method execute (line 28) | def execute(argv)
method parse_repo_name (line 75) | def parse_repo_name(argv)
method enable_short_basic_option (line 85) | def enable_short_basic_option
method description (line 89) | def self.description
method usage (line 93) | def self.usage
FILE: lib/m-git/command/fetch.rb
type MGit (line 3) | module MGit
class Fetch (line 9) | class Fetch < BaseCommand
method execute (line 10) | def execute(argv)
method enable_repo_selection (line 19) | def enable_repo_selection
method description (line 23) | def self.description
method usage (line 27) | def self.usage
FILE: lib/m-git/command/forall.rb
type MGit (line 3) | module MGit
class Forall (line 9) | class Forall < BaseCommand
method options (line 18) | def options
method validate (line 31) | def validate(argv)
method execute (line 36) | def execute(argv)
method enable_repo_selection (line 63) | def enable_repo_selection
method enable_short_basic_option (line 67) | def enable_short_basic_option
method description (line 71) | def self.description
method usage (line 75) | def self.usage
FILE: lib/m-git/command/info.rb
type MGit (line 3) | module MGit
class Info (line 7) | class Info < BaseCommand
method execute (line 9) | def execute(argv)
method parse_repo_name (line 39) | def parse_repo_name(argv)
method calculate_size (line 48) | def calculate_size(repo)
method check_stash (line 54) | def check_stash(repo)
method enable_short_basic_option (line 60) | def enable_short_basic_option
method description (line 64) | def self.description
method usage (line 68) | def self.usage
FILE: lib/m-git/command/init.rb
type MGit (line 3) | module MGit
class Init (line 7) | class Init < BaseCommand
method options (line 22) | def options
method validate (line 47) | def validate(argv)
method pre_exec (line 60) | def pre_exec
method post_exec (line 72) | def post_exec
method execute (line 81) | def execute(argv)
method init_dir (line 101) | def init_dir
method write_content (line 114) | def write_content(path, content)
method setup_hooks (line 122) | def setup_hooks(pre_hook_path, post_hook_path, manifest_hook_path, p...
method initial_multi_repo_root (line 129) | def initial_multi_repo_root
method setup_local_config (line 141) | def setup_local_config(path, config_repo, use_local)
method clone_with_git_url (line 152) | def clone_with_git_url(git_url, branch, use_local, clone_all)
method clone_with_local_config (line 203) | def clone_with_local_config(config_path, clone_all)
method clone_sub_repos (line 222) | def clone_sub_repos(repo_list, default_branch)
method link_config (line 278) | def link_config(config_path, config_cache_path, root)
method terminate! (line 289) | def terminate!(msg)
method move_project_to_root (line 297) | def move_project_to_root
method finish_init (line 306) | def finish_init(config)
method enable_short_basic_option (line 312) | def enable_short_basic_option
method description (line 316) | def self.description
method usage (line 320) | def self.usage
FILE: lib/m-git/command/log.rb
type MGit (line 3) | module MGit
class Log (line 7) | class Log < BaseCommand
method options (line 14) | def options
method revise_option_value (line 20) | def revise_option_value(opt)
method execute (line 24) | def execute(argv)
method parse_repo_name (line 46) | def parse_repo_name(argv)
method is_integer? (line 56) | def is_integer?(string)
method enable_short_basic_option (line 60) | def enable_short_basic_option
method description (line 64) | def self.description
method usage (line 68) | def self.usage
FILE: lib/m-git/command/merge.rb
type MGit (line 3) | module MGit
class Merge (line 7) | class Merge < BaseCommand
method options (line 23) | def options
method __progress_type (line 29) | def __progress_type
method execute (line 33) | def execute(argv)
method merge_config_repo (line 175) | def merge_config_repo(cmd, opts, repo, auto_update)
method continue_execute (line 190) | def continue_execute(cmd, opts, repo, check_point, auto_update)
method update_config_repo (line 222) | def update_config_repo(repo, context, auto)
method exec_config_repo (line 239) | def exec_config_repo(repo, cmd, opts)
method refresh_config (line 253) | def refresh_config(repo, context, auto)
method update_subrepos (line 280) | def update_subrepos(subrepos, context, auto)
method refresh_context (line 297) | def refresh_context(context)
method remind_config_repo_fail (line 303) | def remind_config_repo_fail(msg)
method do_abort (line 313) | def do_abort(argv)
method show_progress_error (line 334) | def show_progress_error(summary, detail)
method enable_repo_selection (line 346) | def enable_repo_selection
method enable_continue_operation (line 350) | def enable_continue_operation
method description (line 354) | def self.description
method usage (line 358) | def self.usage
FILE: lib/m-git/command/pull.rb
type MGit (line 3) | module MGit
class Pull (line 7) | class Pull < BaseCommand
method options (line 13) | def options
method __progress_type (line 19) | def __progress_type
method execute (line 23) | def execute(argv)
method verbose_pull (line 32) | def verbose_pull(argv)
method simple_pull (line 202) | def simple_pull(argv)
method pull_config_repo (line 222) | def pull_config_repo(cmd, opts, repo)
method remind_config_repo_fail (line 285) | def remind_config_repo_fail(msg)
method do_abort (line 295) | def do_abort(argv)
method refresh_context (line 316) | def refresh_context(context)
method show_progress_error (line 322) | def show_progress_error(summary, detail)
method enable_repo_selection (line 334) | def enable_repo_selection
method enable_auto_execution (line 338) | def enable_auto_execution
method include_lock_by_default (line 342) | def include_lock_by_default
method enable_continue_operation (line 346) | def enable_continue_operation
method enable_abort_operation (line 350) | def enable_abort_operation
method description (line 354) | def self.description
method usage (line 358) | def self.usage
FILE: lib/m-git/command/push.rb
type MGit (line 3) | module MGit
class Push (line 9) | class Push < BaseCommand
method options (line 23) | def options
method __setup_option_value (line 36) | def __setup_option_value(argv)
method execute (line 47) | def execute(argv)
method __execute_push (line 233) | def __execute_push(argv, do_repos)
method __process_push_result (line 262) | def __process_push_result(repo, stdout, stderr, status)
method __parse_cmd_and_opt (line 283) | def __parse_cmd_and_opt(repo)
method enable_repo_selection (line 297) | def enable_repo_selection
method description (line 301) | def self.description
method usage (line 305) | def self.usage
FILE: lib/m-git/command/rebase.rb
type MGit (line 3) | module MGit
class Rebase (line 7) | class Rebase < BaseCommand
method options (line 23) | def options
method execute (line 29) | def execute(argv)
method rebase_config_repo (line 152) | def rebase_config_repo(cmd, opts, repo, auto_update)
method continue_execute (line 166) | def continue_execute(cmd, opts, repo, check_point, auto_update)
method update_config_repo (line 198) | def update_config_repo(repo, context, auto)
method exec_config_repo (line 215) | def exec_config_repo(repo, cmd, opts)
method refresh_config (line 229) | def refresh_config(repo, context, auto)
method update_subrepos (line 257) | def update_subrepos(subrepos, context, auto)
method refresh_context (line 274) | def refresh_context(context)
method remind_config_repo_fail (line 280) | def remind_config_repo_fail(msg)
method check_master_rebase (line 287) | def check_master_rebase(argv)
method do_abort (line 294) | def do_abort(argv)
method show_progress_error (line 312) | def show_progress_error(summary, detail)
method enable_repo_selection (line 324) | def enable_repo_selection
method enable_continue_operation (line 328) | def enable_continue_operation
method description (line 332) | def self.description
method usage (line 336) | def self.usage
method __progress_type (line 342) | def __progress_type
FILE: lib/m-git/command/reset.rb
type MGit (line 3) | module MGit
class Reset (line 7) | class Reset < BaseCommand
method execute (line 9) | def execute(argv)
method enable_repo_selection (line 17) | def enable_repo_selection
method description (line 21) | def self.description
method usage (line 25) | def self.usage
FILE: lib/m-git/command/self.rb
type MGit (line 3) | module MGit
class Self (line 7) | class Self < BaseCommand
method options (line 31) | def options
method validate (line 41) | def validate(argv)
method prepare (line 45) | def prepare
method pre_exec (line 51) | def pre_exec
method post_exec (line 54) | def post_exec
method usage (line 57) | def usage(argv)
method execute (line 62) | def execute(argv)
method show_help (line 91) | def show_help(argv)
method show_all_repos (line 130) | def show_all_repos(argv)
method show_repo_size (line 164) | def show_repo_size
method prepare_repos (line 188) | def prepare_repos(with_excluded:false)
method show_workspace (line 204) | def show_workspace
method show_version (line 213) | def show_version
method enable_short_basic_option (line 217) | def enable_short_basic_option
FILE: lib/m-git/command/stash.rb
type MGit (line 3) | module MGit
class Stash (line 7) | class Stash < BaseCommand
method options (line 17) | def options
method validate (line 27) | def validate(argv)
method execute (line 36) | def execute(argv)
method do_clear (line 53) | def do_clear(argv)
method do_stash_list (line 64) | def do_stash_list(argv)
method do_stash_pop_apply (line 86) | def do_stash_pop_apply(argv, stash_name, action)
method do_stash_push (line 115) | def do_stash_push(argv, stash_name)
method repo_stash_list_msg (line 150) | def repo_stash_list_msg(repo)
method stash_include_name (line 158) | def stash_include_name(stash_list, stash_name)
method enable_repo_selection (line 175) | def enable_repo_selection
method description (line 179) | def self.description
method usage (line 183) | def self.usage
FILE: lib/m-git/command/status.rb
type MGit (line 3) | module MGit
class Status (line 7) | class Status < BaseCommand
method execute (line 8) | def execute(argv)
method validate (line 114) | def validate(argv)
method enable_repo_selection (line 118) | def enable_repo_selection
method enable_short_basic_option (line 122) | def enable_short_basic_option
method description (line 126) | def self.description
method usage (line 130) | def self.usage
FILE: lib/m-git/command/sync.rb
type MGit (line 3) | module MGit
class Sync (line 8) | class Sync < BaseCommand
method pre_exec (line 24) | def pre_exec
method post_exec (line 33) | def post_exec
method options (line 40) | def options
method validate (line 62) | def validate(argv)
method execute (line 70) | def execute(argv)
method prepare_repo_category (line 124) | def prepare_repo_category
method setup_specified_repos (line 137) | def setup_specified_repos(argv)
method setup_normal_reops (line 178) | def setup_normal_reops(argv)
method setup_new_reops (line 199) | def setup_new_reops
method setup_all_sync_reops (line 211) | def setup_all_sync_reops(argv)
method setup_download_reops (line 227) | def setup_download_reops(repo_names)
method setup_config_url_repos (line 261) | def setup_config_url_repos()
method check_valid_repos (line 291) | def check_valid_repos(repo_names)
method parse_repo_name (line 303) | def parse_repo_name(argv)
method enable_short_basic_option (line 313) | def enable_short_basic_option
method description (line 317) | def self.description
method usage (line 321) | def self.usage
FILE: lib/m-git/command/tag.rb
type MGit (line 3) | module MGit
class Tag (line 7) | class Tag < BaseCommand
method execute (line 9) | def execute(argv)
method enable_repo_selection (line 53) | def enable_repo_selection
method description (line 57) | def self.description
method usage (line 61) | def self.usage
FILE: lib/m-git/command_manager.rb
type MGit (line 2) | module MGit
class CommandManager (line 3) | class CommandManager
method commands (line 7) | def commands
method register_command (line 11) | def register_command(cmd, cls)
method [] (line 15) | def [](cmd)
method class_with_command (line 19) | def class_with_command(cmd)
FILE: lib/m-git/error.rb
type MGit (line 3) | module MGit
class Error (line 10) | class Error < StandardError
method initialize (line 14) | def initialize(msg, type:nil)
FILE: lib/m-git/foundation.rb
type MGit (line 14) | module MGit
type Foundation (line 15) | module Foundation
function help! (line 19) | def help!(msg, title:nil)
FILE: lib/m-git/foundation/constants.rb
type MGit (line 3) | module MGit
type Constants (line 4) | module Constants
FILE: lib/m-git/foundation/dir.rb
class Dir (line 2) | class Dir
method is_git_repo? (line 8) | def self.is_git_repo?(path)
method is_in_git_repo? (line 14) | def self.is_in_git_repo?(path)
FILE: lib/m-git/foundation/duration_recorder.rb
type MGit (line 3) | module MGit
class DurationRecorder (line 7) | class DurationRecorder
type Status (line 11) | module Status
method start (line 27) | def self.start(duration_key:DEFAULT_DURATION_KEY, use_lock:false)
method pause (line 40) | def self.pause(duration_key:DEFAULT_DURATION_KEY, use_lock:false)
method resume (line 51) | def self.resume(duration_key:DEFAULT_DURATION_KEY, use_lock:false)
method end (line 60) | def self.end(duration_key:DEFAULT_DURATION_KEY, use_lock:false)
method mutex_exec (line 80) | def self.mutex_exec(use_lock)
FILE: lib/m-git/foundation/git_message_parser.rb
type MGit (line 2) | module MGit
class GitMessageParser (line 5) | class GitMessageParser
method initialize (line 7) | def initialize(url)
method parse_fetch_msg (line 14) | def parse_fetch_msg(input)
method parse_pull_msg (line 21) | def parse_pull_msg(input)
method parse_push_msg (line 28) | def parse_push_msg(input)
method parse_code_review_url (line 34) | def parse_code_review_url(input)
method __default_parse_msg (line 38) | def __default_parse_msg(msg)
FILE: lib/m-git/foundation/lock.rb
type MGit (line 2) | module MGit
class Lock (line 3) | class Lock
method mutex_exec (line 11) | def mutex_exec
method mutex_puts (line 23) | def mutex_puts
FILE: lib/m-git/foundation/loger.rb
type MGit (line 5) | module MGit
class Loger (line 8) | class Loger
method config (line 17) | def self.config(root)
method set_log_enable (line 56) | def self.set_log_enable(log_enable)
method set_log_level (line 65) | def self.set_log_level(level)
method set_log_file (line 72) | def self.set_log_file(file_path)
method debug (line 79) | def self.debug(message)
method info (line 86) | def self.info(message)
method warn (line 93) | def self.warn(message)
method error (line 100) | def self.error(message)
method fatal (line 107) | def self.fatal(message)
method logger (line 112) | def self.logger
class Loger (line 46) | class Loger
method config (line 17) | def self.config(root)
method set_log_enable (line 56) | def self.set_log_enable(log_enable)
method set_log_level (line 65) | def self.set_log_level(level)
method set_log_file (line 72) | def self.set_log_file(file_path)
method debug (line 79) | def self.debug(message)
method info (line 86) | def self.info(message)
method warn (line 93) | def self.warn(message)
method error (line 100) | def self.error(message)
method fatal (line 107) | def self.fatal(message)
method logger (line 112) | def self.logger
FILE: lib/m-git/foundation/mgit_config.rb
type MGit (line 5) | module MGit
class MGitConfig (line 6) | class MGitConfig
method query (line 49) | def query(root)
method query_with_key (line 70) | def query_with_key(root, key_symbol)
method update (line 88) | def update(root)
method dump_config (line 120) | def dump_config(root)
method to_suitable_value_for_key (line 145) | def to_suitable_value_for_key(root, key, value)
method __load_file (line 180) | def __load_file(root)
method write_to_file (line 201) | def write_to_file(root, content)
FILE: lib/m-git/foundation/operation_progress_manager.rb
type MGit (line 3) | module MGit
class OperationProgressContext (line 5) | class OperationProgressContext
method initialize (line 21) | def initialize(type)
method serialize (line 27) | def serialize
method deserialize (line 41) | def deserialize(dict)
method validate? (line 53) | def validate?
class OperationProgressManager (line 58) | class OperationProgressManager
method trap_into_progress (line 74) | def trap_into_progress(root, context)
method remove_progress (line 90) | def remove_progress(root, type)
method is_in_progress? (line 101) | def is_in_progress?(root, type)
method load_context (line 121) | def load_context(root, type)
FILE: lib/m-git/foundation/timer.rb
type MGit (line 3) | module MGit
class Timer (line 5) | class Timer
method start (line 19) | def start(repo_name, use_lock:false)
method stop (line 33) | def stop(repo_name, use_lock:false)
method show_time_consuming_repos (line 47) | def show_time_consuming_repos(threshold:5)
method mutex_exec (line 61) | def mutex_exec(use_lock)
FILE: lib/m-git/foundation/utils.rb
type MGit (line 3) | module MGit
type Utils (line 4) | module Utils
function logical_cpu_num (line 7) | def logical_cpu_num
function execute_shell_cmd (line 23) | def execute_shell_cmd(cmd)
function change_dir (line 37) | def change_dir(dir)
function execute_under_dir (line 47) | def execute_under_dir(dir)
function relative_dir (line 62) | def relative_dir(dir_a, dir_b, realpath: true)
function expand_path (line 78) | def expand_path(path, base:nil)
function generate_init_cache_path (line 92) | def generate_init_cache_path(root)
function link (line 103) | def link(target_path, link_path)
function show_clone_info (line 112) | def show_clone_info(root, missing_light_repos)
function branch_exist_on_remote? (line 137) | def branch_exist_on_remote?(branch, git_url)
function has_permission_of_remote? (line 149) | def has_permission_of_remote?(git_url)
function link_git (line 168) | def link_git(source_dir, source_git_dir)
function generate_git_store (line 191) | def generate_git_store(root, url)
function sync_workspace (line 218) | def sync_workspace(root, config, recover_cache_if_cancelled:true)
function pop_git_entity (line 285) | def pop_git_entity(root, config)
function push_git_entity (line 292) | def push_git_entity(root, config)
function url_consist? (line 307) | def url_consist?(url_a, url_b)
function normalize_url (line 331) | def normalize_url(url)
function safe_join (line 350) | def safe_join(path_a, path_b)
FILE: lib/m-git/hooks_manager.rb
type MGit (line 2) | module MGit
type HooksManager (line 3) | module HooksManager
function execute_manifest_hook (line 14) | def execute_manifest_hook(strict_mode:true)
function execute_mgit_pre_hook (line 29) | def execute_mgit_pre_hook(cmd, pure_opts)
function execute_mgit_post_hook (line 36) | def execute_mgit_post_hook(cmd, pure_opts, light_repos)
function execute_mgit_pre_exec_hook (line 45) | def execute_mgit_pre_exec_hook(cmd, pure_opts, light_repos)
function execute_mgit_pre_push_hook (line 52) | def execute_mgit_pre_push_hook(cmd, pure_opts, light_repos)
function execute_post_download_hook (line 70) | def execute_post_download_hook(repo_name, repo_path)
function __execute_hook_file (line 84) | def __execute_hook_file(file_name, hook_class)
FILE: lib/m-git/manifest.rb
type MGit (line 7) | module MGit
class Manifest (line 11) | class Manifest
method generate_light_repos (line 54) | def self.generate_light_repos(root, mgit_managed_only: false, only_e...
method parse (line 89) | def self.parse(config_path, strict_mode:true)
method simple_parse (line 103) | def self.simple_parse(config_content, strict_mode:true)
method repo_list (line 120) | def repo_list(selection: nil, exclusion: nil, all:false)
method global_config (line 141) | def global_config(key)
method update_cache_with_content (line 154) | def update_cache_with_content(root, config_content)
method update_previous_extra_light_repos (line 165) | def update_previous_extra_light_repos(root)
method terminate! (line 170) | def terminate!(msg, type:nil)
FILE: lib/m-git/manifest/cache_manager.rb
type MGit (line 2) | module MGit
class Manifest (line 3) | class Manifest
class CacheManager (line 4) | class CacheManager
method load_path (line 10) | def load_path(cache_path)
method save_to_cache (line 31) | def self.save_to_cache(cache_path, hash_sha1, hash_data)
FILE: lib/m-git/manifest/internal.rb
type MGit (line 4) | module MGit
class Manifest (line 5) | class Manifest
type Internal (line 6) | module Internal
function __setup (line 15) | def __setup(config_path, strict_mode)
function __simple_setup (line 37) | def __simple_setup(config_content, strict_mode)
function cache_manager (line 57) | def cache_manager
function __load_config (line 64) | def __load_config(config_path, config_content: nil, local_config_p...
function __load_cache (line 102) | def __load_cache(cache_path)
function __parse_manifest_json (line 112) | def __parse_manifest_json(raw_string)
function __parse_manifest (line 122) | def __parse_manifest(path)
function __generate_light_repos (line 131) | def __generate_light_repos(config_hash)
function __generate_extra_light_repos (line 140) | def __generate_extra_light_repos(current_hash, previous_hash)
function __generate_hash_sha1 (line 154) | def __generate_hash_sha1(json_string)
function __merge_manifest_hash (line 162) | def __merge_manifest_hash(base_hash, attach_hash)
FILE: lib/m-git/manifest/light_repo.rb
type MGit (line 5) | module MGit
class Manifest (line 6) | class Manifest
method clone_url (line 51) | def clone_url(root, local_url:nil, clone_branch:nil)
method abs_dest (line 71) | def abs_dest(root)
method git_store_dir (line 81) | def git_store_dir(root)
method cache_store_dir (line 103) | def cache_store_dir(root)
FILE: lib/m-git/manifest/light_repo_generator.rb
type MGit (line 2) | module MGit
class Manifest (line 3) | class Manifest
class LightRepoGenerator (line 6) | class LightRepoGenerator
method simple_init (line 18) | def self.simple_init(name, path, url)
method light_repo_with (line 22) | def self.light_repo_with(name, config_json, parent_json)
method __parse_path (line 56) | def __parse_path(repo_name, config_json, parent_json)
method __parse_url (line 72) | def __parse_url(config_json, parent_json)
FILE: lib/m-git/manifest/linter.rb
type MGit (line 2) | module MGit
class Manifest (line 3) | class Manifest
type Linter (line 4) | module Linter
function lint_manifest_path (line 12) | def lint_manifest_path(path)
function lint_local_manifest_path (line 32) | def lint_local_manifest_path(path)
function lint_light_repos! (line 40) | def lint_light_repos!
function lint_raw_json! (line 62) | def lint_raw_json!(dict)
FILE: lib/m-git/open_api.rb
type MGit (line 5) | module MGit
class OpenApi (line 8) | class OpenApi
method sync_repos (line 28) | def sync_repos(config_content, download_root)
method download_repos (line 72) | def download_repos(config_content, download_root, manage_git:true)
method checkout (line 119) | def checkout(repo_name, repo_path, create_branch:false, branch:nil, ...
method fetch (line 222) | def fetch(repo_name, repo_path)
method pull (line 244) | def pull(repo_name, repo_path)
method check_extra_repos (line 267) | def check_extra_repos(path_dict)
method concurrent_enumerate (line 298) | def concurrent_enumerate(array, max_concurrent_count:5)
method check_permission_batch (line 320) | def check_permission_batch(root, url_list)
method check_permission (line 345) | def check_permission(url, root:nil)
method validate_argv (line 365) | def validate_argv(method_name, args)
method download (line 386) | def download(config, download_root, manage_git:true, sync_exist: false)
FILE: lib/m-git/open_api/script_download_info.rb
type MGit (line 2) | module MGit
class OpenApi (line 3) | class OpenApi
type DownloadResult (line 5) | module DownloadResult
class ScriptDownloadInfo (line 11) | class ScriptDownloadInfo
method initialize (line 28) | def initialize(repo_name, repo_path, result, output, progress)
FILE: lib/m-git/output/output.rb
type MGit (line 5) | module MGit
type Output (line 6) | module Output
function puts_cancel_message (line 11) | def puts_cancel_message
function puts_succeed_cmd (line 15) | def puts_succeed_cmd(cmd)
function puts_start_cmd (line 19) | def puts_start_cmd
function puts_fail_cmd (line 23) | def puts_fail_cmd(cmd)
function puts_nothing_to_do_cmd (line 27) | def puts_nothing_to_do_cmd
function puts_success_message (line 31) | def puts_success_message(str)
function puts_remind_message (line 36) | def puts_remind_message(str)
function puts_terminate_message (line 41) | def puts_terminate_message(str, title:nil)
function puts_fail_message (line 46) | def puts_fail_message(str)
function puts_processing_message (line 51) | def puts_processing_message(str)
function puts_fail_block (line 56) | def puts_fail_block(list, bottom_summary)
function puts_remind_block (line 60) | def puts_remind_block(list, bottom_summary)
function puts_processing_block (line 64) | def puts_processing_block(list, bottom_summary)
function puts_fail_combined_block (line 68) | def puts_fail_combined_block(list_array, bottom_summary, title:nil)
function puts_in_pager (line 72) | def puts_in_pager(output)
function continue_with_user_remind? (line 82) | def continue_with_user_remind?(msg)
function continue_with_interact_repos? (line 97) | def continue_with_interact_repos?(repos, msg)
function continue_with_combined_interact_repos? (line 114) | def continue_with_combined_interact_repos?(repos_array, msg, title:nil)
function interact_with_multi_selection_combined_repos (line 139) | def interact_with_multi_selection_combined_repos(list_array, msg, se...
function processing_message (line 150) | def processing_message(str)
function remind_message (line 154) | def remind_message(str)
function success_message (line 158) | def success_message(str)
function fail_message (line 162) | def fail_message(str)
function terminate_message (line 166) | def terminate_message(str, title:nil)
function green_message (line 175) | def green_message(str)
function blue_message (line 180) | def blue_message(str)
function red_message (line 185) | def red_message(str)
function yellow_message (line 190) | def yellow_message(str)
function info_title (line 194) | def info_title(str)
function generate_fail_combined_block (line 200) | def generate_fail_combined_block(list_array, bottom_summary, title:nil)
function generate_remind_block (line 207) | def generate_remind_block(list, bottom_summary)
function generate_fail_block (line 213) | def generate_fail_block(list, bottom_summary)
function generate_processing_block (line 219) | def generate_processing_block(list, bottom_summary)
function generate_block (line 225) | def generate_block(list, bottom_summary)
function generate_title_block (line 233) | def generate_title_block(title, has_separator:true)
function generate_table_combination (line 252) | def generate_table_combination(list_array, title:nil, separator:'')
function generate_table (line 300) | def generate_table(list, title:nil, separator:'', fixed_width:-1, hi...
function calculate_table_info (line 368) | def calculate_table_info(list, head_separator, middle_separator, tai...
function string_length_by_ascii (line 429) | def string_length_by_ascii(str)
function max_meta_display_length_by_char (line 436) | def max_meta_display_length_by_char(str, max_meta_display_length_by_...
function secure (line 442) | def secure(num)
function update_progress (line 448) | def update_progress(totaltasks, finishtasks)
FILE: lib/m-git/plugin_manager.rb
type MGit (line 2) | module MGit
class PluginManager (line 3) | class PluginManager
method setup (line 10) | def self.setup
method loaded_plugins (line 41) | def self.loaded_plugins
method load_local_plugin_dir (line 47) | def self.load_local_plugin_dir(plugin_prefix, plugins_dir)
method load_local_plugins (line 58) | def self.load_local_plugins(plugin_prefix, plugin_root, with_name = ...
method load_gem_plugins (line 70) | def self.load_gem_plugins(plugin_prefix)
method safe_activate_gem (line 84) | def self.safe_activate_gem(spec, paths)
method safe_activate_plugin_files (line 98) | def self.safe_activate_plugin_files(plugin_name, paths)
FILE: lib/m-git/repo.rb
type MGit (line 6) | module MGit
class Repo (line 8) | class Repo
method initialize (line 22) | def initialize(name, path, config:nil)
method generate_softly (line 31) | def self.generate_softly(root, config)
method generate_strictly (line 42) | def self.generate_strictly(root, config)
method check_git_dest (line 54) | def self.check_git_dest(root, config)
method is_git_repo? (line 69) | def self.is_git_repo?(path)
method execute (line 79) | def execute(abs_cmd)
method execute_git_cmd (line 97) | def execute_git_cmd(cmd, opts)
method git_cmd (line 102) | def git_cmd(cmd, opts)
method url_consist? (line 123) | def url_consist?
FILE: lib/m-git/repo/status.rb
type MGit (line 3) | module MGit
class Repo (line 4) | class Repo
class Status (line 5) | class Status
method initialize (line 119) | def initialize(path)
method status (line 125) | def status
method message (line 130) | def message
method dirty_zone (line 135) | def dirty_zone
method branch_status (line 140) | def branch_status
method branch_message (line 145) | def branch_message
method is_in_merge_progress? (line 151) | def is_in_merge_progress?
method is_in_rebase_progress? (line 159) | def is_in_rebase_progress?
method is_ancestor_of_branch? (line 167) | def is_ancestor_of_branch?(branch)
method tracking_branch (line 203) | def tracking_branch(branch, use_cache:false)
method current_branch (line 224) | def current_branch(strict_mode:true, use_cache:false)
method current_head (line 246) | def current_head(strict_mode:true)
method local_branch_exist? (line 260) | def local_branch_exist?(branch)
method remote_branch_exist? (line 265) | def remote_branch_exist?(branch)
method commit_exist? (line 270) | def commit_exist?(commit)
method default_url (line 278) | def default_url
method refresh (line 290) | def refresh
method git_dir (line 299) | def git_dir
method work_tree (line 304) | def work_tree
method has_branch? (line 314) | def has_branch?(branch, is_remote)
method check_repo_status (line 328) | def check_repo_status
method check_branch_status (line 345) | def check_branch_status
method parse_change (line 406) | def parse_change(list)
method convert_file_status (line 469) | def convert_file_status(type, status)
FILE: lib/m-git/repo/sync_helper.rb
type MGit (line 3) | module MGit
class Repo (line 5) | class Repo
class SyncHelper (line 6) | class SyncHelper
method sync_new_repo (line 20) | def sync_new_repo(light_repo, root, link_git:true)
method sync_exist_repo (line 102) | def sync_exist_repo(repo, light_repo)
method sync_lock_point (line 126) | def sync_lock_point(repo, light_repo)
method sync_tag (line 147) | def sync_tag(repo, light_repo)
method sync_commit_id (line 163) | def sync_commit_id(repo, light_repo)
method sync_branch (line 180) | def sync_branch(repo, light_repo)
method sync_remote_url (line 221) | def sync_remote_url(repo, light_repo)
method local_bare_git_url (line 234) | def local_bare_git_url(path)
method delete_legacy_repo (line 246) | def delete_legacy_repo(repo_abs_path)
FILE: lib/m-git/template/local_manifest.rb
type MGit (line 3) | module MGit
type Template (line 4) | module Template
function default_template (line 6) | def default_template
function local_config_template (line 15) | def local_config_template(config_repo_name)
FILE: lib/m-git/template/manifest_hook.rb
type MGit (line 3) | module MGit
type Template (line 4) | module Template
FILE: lib/m-git/template/post_download_hook.rb
type MGit (line 3) | module MGit
type Template (line 4) | module Template
FILE: lib/m-git/template/post_hook.rb
type MGit (line 3) | module MGit
type Template (line 4) | module Template
FILE: lib/m-git/template/pre_exec_hook.rb
type MGit (line 3) | module MGit
type Template (line 4) | module Template
FILE: lib/m-git/template/pre_hook.rb
type MGit (line 3) | module MGit
type Template (line 4) | module Template
FILE: lib/m-git/template/pre_push_hook.rb
type MGit (line 3) | module MGit
type Template (line 4) | module Template
FILE: lib/m-git/version.rb
type MGit (line 2) | module MGit
FILE: lib/m-git/workspace.rb
type MGit (line 5) | module MGit
class Workspace (line 6) | class Workspace
method filter_config (line 20) | def filter_config
method setup_multi_repo_root (line 25) | def setup_multi_repo_root(initial_root=nil)
method setup_config (line 40) | def setup_config(strict_mode:true)
method update_config (line 93) | def update_config(strict_mode:true)
method setup_all_repos (line 147) | def setup_all_repos(strict_mode: true)
method all_repos (line 192) | def all_repos(except_config:false)
method locked_repos (line 202) | def locked_repos
method update_all_repos (line 208) | def update_all_repos(update_repos_names)
method exec_light_repos (line 221) | def exec_light_repos
method multi_repo_root_path (line 246) | def multi_repo_root_path
method generate_config_repo (line 262) | def generate_config_repo
method concurrent_enumerate_with_progress_bar (line 273) | def concurrent_enumerate_with_progress_bar(light_repos, message, &ex...
method concurrent_enumerate_with_progress_bar_pure (line 278) | def concurrent_enumerate_with_progress_bar_pure(light_repos, &exec_h...
method sync_new_repos (line 290) | def sync_new_repos(repos)
method sync_locked_repos (line 343) | def sync_locked_repos(repos)
method check_branch_consistency (line 363) | def check_branch_consistency
method has_diff_branch? (line 378) | def has_diff_branch?(repos)
method guide_to_checkout_branch (line 402) | def guide_to_checkout_branch(new_repos, exist_repos, append_message:...
method concurrent_enumerate (line 470) | def concurrent_enumerate(array)
method serial_enumerate_with_progress (line 483) | def serial_enumerate_with_progress(array)
method execute_git_cmd_with_repos (line 494) | def execute_git_cmd_with_repos(cmd, git_opts, repos)
method execute_common_cmd_with_repos (line 520) | def execute_common_cmd_with_repos(abs_cmd, repos)
method execute_common_cmd_with_repos_concurrent (line 543) | def execute_common_cmd_with_repos_concurrent(abs_cmd, repos)
method show_error (line 570) | def show_error(error_repos, action:nil)
method pre_fetch (line 588) | def pre_fetch
method is_all_exec_sub_repos? (line 626) | def is_all_exec_sub_repos?(subrepos)
method is_all_exec_sub_repos_by_name? (line 639) | def is_all_exec_sub_repos_by_name?(subrepo_names)
FILE: lib/m-git/workspace/path_helper.rb
type MGit (line 2) | module MGit
class Workspace (line 3) | class Workspace
type PathHelper (line 7) | module PathHelper
function config_file (line 11) | def config_file
function hooks_dir (line 17) | def hooks_dir
function snapshot_dir (line 23) | def snapshot_dir
function source_config_dir (line 29) | def source_config_dir
function source_git_dir (line 34) | def source_git_dir
function manifest_path (line 39) | def manifest_path
function local_manifest_path (line 44) | def local_manifest_path
function cache_manifest_path (line 49) | def cache_manifest_path
FILE: lib/m-git/workspace/workspace_helper.rb
type MGit (line 3) | module MGit
class Workspace (line 4) | class Workspace
type WorkspaceHelper (line 5) | module WorkspaceHelper
function pop_git_entity (line 12) | def pop_git_entity(root, light_repo)
function push_git_entity (line 40) | def push_git_entity(root, light_repo)
function pop (line 64) | def pop(root, light_repo)
function push (line 89) | def push(root, light_repo)
function replace (line 117) | def replace(root, light_repo_a, light_repo_b)
function sync_workspace (line 128) | def sync_workspace(root, light_repo)
function invalid_move? (line 153) | def invalid_move?(from_path, to_path)
FILE: test/foundation/test_mgit_config.rb
function __stub_config_env (line 14) | def __stub_config_env
function __stub_write_config (line 20) | def __stub_write_config
FILE: test/test_helper.rb
type MGitTest (line 14) | module MGitTest
type FileProvider (line 15) | module FileProvider
Condensed preview — 109 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (420K chars).
[
{
"path": ".gitignore",
"chars": 203,
"preview": "/.bundle/\n/.yardoc\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n\n.bundle\nvendor/\n*.gem\n\n## IDE\n*.xccheckout\n.id"
},
{
"path": "CHANGELOG.md",
"chars": 149,
"preview": "## Release Note:\n\n### 2.5.5\n\n-【bug修复】修复 box config -m 命令 不可用问题,修改参数类型为String\n\n### 2.5.4\n\n- mgit增加inject注入文件功能\n- 增加manife"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3213,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "Gemfile",
"chars": 168,
"preview": "source \"https://gems.ruby-china.com\"\n\n# git_source(:github) {|repo_name| \"https://github.com/#{repo_name}\" }\n\n# Specify "
},
{
"path": "LICENSE",
"chars": 1099,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2020 Baidu, Inc. All Rights Reserved.\n\nPermission is hereby granted, free of charge"
},
{
"path": "README.md",
"chars": 2092,
"preview": "# MGit\n\nMGit 是一款 Ruby 封装的基于 Git 的多仓库管理工具,可以高效的、同时的对多个 Git 仓库执行 Git 命令。\n适合于在多个仓库中进行关联开发的项目,提高 Git 操作的效率,避免逐个执行 Git 命令带来的误"
},
{
"path": "Rakefile",
"chars": 187,
"preview": "\nrequire \"bundler/gem_tasks\"\ntask :default => :test\n\n\nrequire 'rake/testtask'\n\nRake::TestTask.new(:test) do |t|\n t.libs"
},
{
"path": "docs/commands/commands.md",
"chars": 1022,
"preview": "\n## Command-line Reference\n参考:https://guides.cocoapods.org/terminal/commands.html#pod_spec_lint\n\n\n# Brief Description Of"
},
{
"path": "docs/config/config-env.md",
"chars": 2179,
"preview": "### config.yml 介绍\n\n config.yml 是 MGit 的配置文件;\n\n MGit的配置内容保存在多仓库目录下的`.mgit/config.yml`文件中,配置仅针对当前的多仓库生效;\n\n\n\n- 通过 `$ mgit c"
},
{
"path": "docs/config/manifest.md",
"chars": 6859,
"preview": "### 1. manifest 配置介绍\n\nmanifest.json 配置表定义了多仓库管理所需的仓库信息,采用json格式,示例如下:\n\n```ruby\n{\n \"remote\": \"https://github.com/baidu\","
},
{
"path": "docs/design/design.md",
"chars": 18,
"preview": "## 设计篇(略)\n### 基本架构"
},
{
"path": "docs/design/principle.md",
"chars": 7,
"preview": "## 基本原则"
},
{
"path": "docs/design/workspace.md",
"chars": 8,
"preview": "### 工作区\n"
},
{
"path": "docs/extension/cmd_ext.md",
"chars": 934,
"preview": "### 一、扩展指令\n\n\nMGit 加载插件有**2种方式**:\n\n- **ruby gem 包加载**\n当运行 MGit 命令时,从所有的`ruby-gems`包中查找并加载 MGit 的插件,查找判定条件 gem 包中存在如下文件:\n`"
},
{
"path": "docs/extension/hooks.md",
"chars": 1324,
"preview": "### hook\n\n#### 一、什么是hook\n\nhook是在MGit 仓库中特定事件触发后被调用的脚本。hook最常见的使用场景包括根据仓库状态改变项目环境、接入持续集成工作流等。 由于脚本是可以完全定制,所以你可以用hook来自动化或"
},
{
"path": "docs/extension/plugins.md",
"chars": 39,
"preview": "### 插件\n- 插件介绍,可以做什么\n- 如何扩展插件\n- 如何引用其他插件"
},
{
"path": "docs/extension/submodule.md",
"chars": 30,
"preview": "### 用做子工具\n- 介绍其他工具如何调用mgit作为模块"
},
{
"path": "docs/faq/faq.md",
"chars": 8,
"preview": "## F.A.Q"
},
{
"path": "docs/references.md",
"chars": 406,
"preview": "<center style=\"font-family:verdana\">MGit相关文档</center>\n\n* [介绍](../README.md)\n\n* 使用篇\n * [快速开始](use/how-to-start.md)\n "
},
{
"path": "docs/use/common-commands.md",
"chars": 17379,
"preview": "## 常用命令\n\n> 指令描述符说明: \n>\n> < > : 占位参数\n>\n> [ ] : 可选组合\n>\n> ( ) :必选组合 \n>\n> | : 互斥参数 \n>\n> ··· :可重复指定前一个参数\n\n\n### mg"
},
{
"path": "docs/use/how-to-start.md",
"chars": 2432,
"preview": "## 快速开始\n\n\n### 1、安装 MGit 工具\n\n环境要求:\n\n- 系统:支持 macOS、Ubuntu,暂时不支持 window-\n- Ruby版本: >= 2.3.7\n\n```ruby\n$ gem install m-git\n``"
},
{
"path": "lib/m-git/argv/opt.rb",
"chars": 1022,
"preview": "#coding=utf-8\n\nmodule MGit\n\n class ARGV\n # 单个选项, 如‘--k1=v1 --k2 v2 v3 --flag’\n # '--k1=v1' key: '--k1', value"
},
{
"path": "lib/m-git/argv/opt_list.rb",
"chars": 1324,
"preview": "#coding=utf-8\n\nmodule MGit\n class ARGV\n # 参数对象列表\n class OptList\n\n # [Array<ARGV::Opt>] 参数对象数组\n attr_acc"
},
{
"path": "lib/m-git/argv/parser.rb",
"chars": 2068,
"preview": "\nmodule MGit\n class ARGV\n module Parser\n\n # @param [Array] argv\n #\n # @return [String, Array, Array]\n"
},
{
"path": "lib/m-git/argv.rb",
"chars": 4323,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # 参数处理类\n class ARGV\n\n require 'm-git/argv/opt'\n require 'm-git/argv/opt_list'\n r"
},
{
"path": "lib/m-git/base_command.rb",
"chars": 6948,
"preview": "#coding=utf-8\n#\n\nrequire 'm-git/command_manager'\n\nmodule MGit\n class BaseCommand\n\n def self.inherited(sub_klass)\n "
},
{
"path": "lib/m-git/command/add.rb",
"chars": 728,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope [command] add\n # follow git add\n # eg: mgit add .\n #\n class Add < BaseComman"
},
{
"path": "lib/m-git/command/branch.rb",
"chars": 2111,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope [command] branch\n # follow git branch\n # eg: mgit branch --compact\n #\n class"
},
{
"path": "lib/m-git/command/checkout.rb",
"chars": 3027,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope [command] checkout\n # follow git checkout\n # eg: mgit checkout master\n #\n cl"
},
{
"path": "lib/m-git/command/clean.rb",
"chars": 1743,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope [command] clean 清除所有仓库中工作区的变更\n # follow git combinatorial command\n # eg: git a"
},
{
"path": "lib/m-git/command/commit.rb",
"chars": 2456,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope [command] commit\n # follow git commit\n # eg: mgit commit -m 'Just for fun'\n #"
},
{
"path": "lib/m-git/command/config.rb",
"chars": 7053,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope [command] config 配置 .mgit/config.yml 文件信息\n #\n # eg: mgit config -s key 'value'"
},
{
"path": "lib/m-git/command/delete.rb",
"chars": 2991,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope [command] delete 删除某个仓库的`所有`文件,包括工作区、暂存区和版本库\n #\n # eg: mgit delete subA\n #\n "
},
{
"path": "lib/m-git/command/fetch.rb",
"chars": 647,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope [command] fetch\n # follow git fetch\n # eg: mgit fetch\n #\n class Fetch < Base"
},
{
"path": "lib/m-git/command/forall.rb",
"chars": 2250,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope [command] forall 对管理的仓库依次执行shell命令\n #\n # eg: mgit forall -c 'git status'\n #\n "
},
{
"path": "lib/m-git/command/info.rb",
"chars": 2314,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope 指定仓库的信息\n #\n class Info < BaseCommand\n\n def execute(argv)\n Output.puts_"
},
{
"path": "lib/m-git/command/init.rb",
"chars": 11804,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope 初始化多仓库命令\n #\n class Init < BaseCommand\n\n OPT_LIST = {\n :git_source "
},
{
"path": "lib/m-git/command/log.rb",
"chars": 1964,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope 查询多仓库的日志\n #\n class Log < BaseCommand\n\n OPT_LIST = {\n :number => '-"
},
{
"path": "lib/m-git/command/merge.rb",
"chars": 12924,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope 类似 git merge\n #\n class Merge < BaseCommand\n\n PROGRESS_STAGE = {\n :new_"
},
{
"path": "lib/m-git/command/pull.rb",
"chars": 13281,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope 类似 git pull\n #\n class Pull < BaseCommand\n\n OPT_LIST = {\n :no_check "
},
{
"path": "lib/m-git/command/push.rb",
"chars": 10750,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope 类似 git push\n # 可自动生成 git gerrit 评审分支\n # mgit push --gerrit\n #\n class Push < "
},
{
"path": "lib/m-git/command/rebase.rb",
"chars": 11859,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope 类似 git rebase\n #\n class Rebase < BaseCommand\n\n PROGRESS_STAGE = {\n :ne"
},
{
"path": "lib/m-git/command/reset.rb",
"chars": 590,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope 类似 git reset\n #\n class Reset < BaseCommand\n\n def execute(argv)\n Worksp"
},
{
"path": "lib/m-git/command/self.rb",
"chars": 7202,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # 该指令用于不带指令的输入:mgit --help,用于执行mgit的一级参数(如\"mgit --help\"的\"--help\")\n #\n class Self < BaseC"
},
{
"path": "lib/m-git/command/stash.rb",
"chars": 5963,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope 类似 git stash,但是强制标记名称\n #\n class Stash < BaseCommand\n\n OPT_LIST = {\n :p"
},
{
"path": "lib/m-git/command/status.rb",
"chars": 3832,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope 类似 git status\n #\n class Status < BaseCommand\n def execute(argv)\n Outpu"
},
{
"path": "lib/m-git/command/sync.rb",
"chars": 11102,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope 同步所有管理的仓库到工作区,可能从远端拉取,可能从本地dump,所以sync后的工作区不一定是分支的最新节点\n # 可通过 mgit sync --pull "
},
{
"path": "lib/m-git/command/tag.rb",
"chars": 1517,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # @!scope 类似 git tag\n #\n class Tag < BaseCommand\n\n def execute(argv)\n Workspace."
},
{
"path": "lib/m-git/command_manager.rb",
"chars": 359,
"preview": "\nmodule MGit\n class CommandManager\n class << self\n # cmd generate\n #\n def commands\n @commands "
},
{
"path": "lib/m-git/error.rb",
"chars": 326,
"preview": "#coding=utf-8\n\nmodule MGit\n\n MGIT_ERROR_TYPE = {\n :config_name_error => 'config_name_error',\n :config_generate_er"
},
{
"path": "lib/m-git/foundation/constants.rb",
"chars": 3219,
"preview": "#coding=utf-8\n\nmodule MGit\n module Constants\n PROJECT_DIR = {\n :hooks => '.mgit/hooks',\n :sou"
},
{
"path": "lib/m-git/foundation/dir.rb",
"chars": 476,
"preview": "\nclass Dir\n\n # 检查传入路径是不是git仓库\n #\n # @param path [String] 仓库路径\n #\n def self.is_git_repo?(path)\n return false unle"
},
{
"path": "lib/m-git/foundation/duration_recorder.rb",
"chars": 2076,
"preview": "#coding=utf-8\n\nmodule MGit\n\n # 计时器\n #\n class DurationRecorder\n\n DEFAULT_DURATION_KEY = 'default_duration_key'\n\n "
},
{
"path": "lib/m-git/foundation/git_message_parser.rb",
"chars": 962,
"preview": "\nmodule MGit\n # Git interact message parse with git-server\n #\n class GitMessageParser\n\n def initialize(url)\n "
},
{
"path": "lib/m-git/foundation/lock.rb",
"chars": 555,
"preview": "\nmodule MGit\n class Lock\n class << self\n\n # @!scope 互斥执行锁\n # @example\n # mutex_exec do\n # exec"
},
{
"path": "lib/m-git/foundation/loger.rb",
"chars": 2678,
"preview": "#coding=utf-8\n\nrequire 'logger'\nrequire 'fileutils'\nmodule MGit\n\n # 日志模块配置\n class Loger\n\n MGIT_LOG_FILE_NAME = 'mgi"
},
{
"path": "lib/m-git/foundation/mgit_config.rb",
"chars": 5579,
"preview": "#coding=utf-8\n\nrequire_relative 'utils'\n\nmodule MGit\n class MGitConfig\n\n CONFIG_KEY = {\n :managegit => {\n "
},
{
"path": "lib/m-git/foundation/operation_progress_manager.rb",
"chars": 3368,
"preview": "#coding=utf-8\n\nmodule MGit\n # 本类用于缓存/加载操作中间态\n class OperationProgressContext\n\n # 现场信息关键字段\n CONTEXT_CMD = 'cmd'\n "
},
{
"path": "lib/m-git/foundation/timer.rb",
"chars": 1832,
"preview": "#coding=utf-8\n\nmodule MGit\n # 计时器,用于统计指令执行耗时\n class Timer\n\n @@time_stamp = {}\n @@duration = {}\n @@lock = Mute"
},
{
"path": "lib/m-git/foundation/utils.rb",
"chars": 10428,
"preview": "#coding=utf-8\n\nmodule MGit\n module Utils\n\n class << self\n def logical_cpu_num\n if @logical_cpu_num.nil?\n"
},
{
"path": "lib/m-git/foundation.rb",
"chars": 591,
"preview": "#coding=utf-8\n\nrequire 'm-git/foundation/constants'\nrequire 'm-git/foundation/dir'\nrequire 'm-git/foundation/git_message"
},
{
"path": "lib/m-git/hooks_manager.rb",
"chars": 3005,
"preview": "\nmodule MGit\n module HooksManager\n\n# --- 执行hook ---\n\n class << self\n # 获取配置表前执行的hook\n #\n # @param str"
},
{
"path": "lib/m-git/manifest/cache_manager.rb",
"chars": 1120,
"preview": "\nmodule MGit\n class Manifest\n class CacheManager\n\n attr_reader :path\n attr_reader :hash_sha1\n attr_re"
},
{
"path": "lib/m-git/manifest/internal.rb",
"chars": 5609,
"preview": "\nrequire 'm-git/manifest/cache_manager'\n\nmodule MGit\n class Manifest\n module Internal\n\n\n # 配置对象\n #\n #"
},
{
"path": "lib/m-git/manifest/light_repo.rb",
"chars": 2519,
"preview": "#coding=utf-8\n\nrequire_relative 'light_repo_generator'\n\nmodule MGit\n class Manifest\n # @!scope manifest.json 配置的repo"
},
{
"path": "lib/m-git/manifest/light_repo_generator.rb",
"chars": 2931,
"preview": "\nmodule MGit\n class Manifest\n # @!scope lightrepo生成器\n #\n class LightRepoGenerator\n\n # 简单初始化,有写字段缺失,仅包含名字,"
},
{
"path": "lib/m-git/manifest/linter.rb",
"chars": 6006,
"preview": "\nmodule MGit\n class Manifest\n module Linter\n\n # 校验配置文件路径\n #\n # @param path [Stirng] 配置文件路径或包含配置文件的目录\n"
},
{
"path": "lib/m-git/manifest/manifest.sample",
"chars": 1832,
"preview": "--- manifest.json ---\n{\n \"remote\": \"https://github.com/baidu\",//远程仓库地址\n \"version\":1,//配置文件版本\n \"branch\": \"master\",//配置"
},
{
"path": "lib/m-git/manifest.rb",
"chars": 5057,
"preview": "#coding=utf-8\n\nrequire 'm-git/manifest/light_repo'\nrequire 'm-git/manifest/linter'\nrequire 'm-git/manifest/internal'\n\nmo"
},
{
"path": "lib/m-git/open_api/script_download_info.rb",
"chars": 745,
"preview": "#coding=utf-8\nmodule MGit\n class OpenApi\n\n module DownloadResult\n SUCCESS = 0\n FAIL = 1\n EXIST = 2\n "
},
{
"path": "lib/m-git/open_api.rb",
"chars": 14301,
"preview": "#coding=utf-8\n\nrequire 'm-git/open_api/script_download_info'\n\nmodule MGit\n\n # --- Ruby环境调用 ---\n class OpenApi\n clas"
},
{
"path": "lib/m-git/output/output.rb",
"chars": 14908,
"preview": "#coding=utf-8\n\nrequire 'm-git/foundation'\n\nmodule MGit\n module Output\n class << self\n\n# --- 简单输出 ---\n\n def puts"
},
{
"path": "lib/m-git/plugin/completion.bash",
"chars": 442,
"preview": "_mgit_completion() {\n COMPREPLY=()\n\n local completions word argc\n word=\"${COMP_WORDS[COMP_CWORD]}\"\n argc=${#COMP_WOR"
},
{
"path": "lib/m-git/plugin/completion.zsh",
"chars": 450,
"preview": "_mgit_completion() {\n\n if [[ ${#words[@]} == 2 ]]; then\n # Complete command\n completions=\"$(mgit script --command"
},
{
"path": "lib/m-git/plugin/init.sh",
"chars": 227,
"preview": "if [ -n \"$BASH_VERSION\" ]; then\n plugin_root=\"$(dirname \"${BASH_SOURCE[0]}\")\"\n source \"$plugin_root/completion.bash\"\n\n"
},
{
"path": "lib/m-git/plugin_manager.rb",
"chars": 3719,
"preview": "\nmodule MGit\n class PluginManager\n\n # @!scope 加载插件\n # 1. 先加载本地源码插件\n # 2. 搜索加载gem插件\n # 3. 处理加载注入的插件\n #\n "
},
{
"path": "lib/m-git/repo/status.rb",
"chars": 16058,
"preview": "#coding=utf-8\n\nmodule MGit\n class Repo\n class Status\n\n # https://git-scm.com/docs/git-status\n # status格式:X"
},
{
"path": "lib/m-git/repo/sync_helper.rb",
"chars": 8353,
"preview": "#coding=utf-8\n\nmodule MGit\n\n class Repo\n class SyncHelper\n\n class << self\n\n # 同步新仓库\n #\n # "
},
{
"path": "lib/m-git/repo.rb",
"chars": 3336,
"preview": "#coding=utf-8\n\nrequire 'm-git/repo/status'\nrequire 'm-git/repo/sync_helper'\n\nmodule MGit\n\n class Repo\n\n # 仓库名\n at"
},
{
"path": "lib/m-git/template/local_manifest.rb",
"chars": 466,
"preview": "#coding=utf-8\n\nmodule MGit\n module Template\n module_function\n def default_template\n return \"{\n \\\"#{Constant"
},
{
"path": "lib/m-git/template/manifest_hook.rb",
"chars": 427,
"preview": "#coding=utf-8\n\nmodule MGit\n module Template\n MANIFEST_HOOK_TEMPLATE = '\n#coding=utf-8\n\nmodule MGitTemplate\n\n class "
},
{
"path": "lib/m-git/template/post_download_hook.rb",
"chars": 363,
"preview": "#coding=utf-8\n\nmodule MGit\n module Template\n POST_DOWNLOAD_HOOK_TEMPLATE = '\n#coding=utf-8\n\nmodule MGitTemplate\n\n c"
},
{
"path": "lib/m-git/template/post_hook.rb",
"chars": 462,
"preview": "#coding=utf-8\n\nmodule MGit\n module Template\n POST_CUSTOMIZED_HOOK_TEMPLATE = '\n#coding=utf-8\n\nmodule MGitTemplate\n\n "
},
{
"path": "lib/m-git/template/pre_exec_hook.rb",
"chars": 469,
"preview": "#coding=utf-8\n\nmodule MGit\n module Template\n PRE_CUSTOMIZED_EXEC_HOOK_TEMPLATE = '\n#coding=utf-8\n\nmodule MGitTemplat"
},
{
"path": "lib/m-git/template/pre_hook.rb",
"chars": 370,
"preview": "#coding=utf-8\n\nmodule MGit\n module Template\n PRE_CUSTOMIZED_HOOK_TEMPLATE = '\n#coding=utf-8\n\nmodule MGitTemplate\n\n "
},
{
"path": "lib/m-git/template/pre_push_hook.rb",
"chars": 472,
"preview": "#coding=utf-8\n\nmodule MGit\n module Template\n\n PRE_CUSTOMIZED_PUSH_HOOK_TEMPLATE = '\n#coding=utf-8\n\nmodule MGitTempla"
},
{
"path": "lib/m-git/template.rb",
"chars": 284,
"preview": "#coding=utf-8\n\nrequire 'm-git/template/local_manifest'\nrequire 'm-git/template/manifest_hook'\nrequire 'm-git/template/po"
},
{
"path": "lib/m-git/version.rb",
"chars": 46,
"preview": "\nmodule MGit\n VERSION = \"2.5.5\".freeze\nend\n\n\n"
},
{
"path": "lib/m-git/workspace/path_helper.rb",
"chars": 1255,
"preview": "\nmodule MGit\n class Workspace\n\n # .mgit 目录下的文件路径\n #\n module PathHelper\n\n # .mgit/config.yml\n #\n "
},
{
"path": "lib/m-git/workspace/workspace_helper.rb",
"chars": 4727,
"preview": "#coding=utf-8\n\nmodule MGit\n class Workspace\n module WorkspaceHelper\n # 弹出托管的.git实体\n #\n # @param root "
},
{
"path": "lib/m-git/workspace.rb",
"chars": 20197,
"preview": "\nrequire 'm-git/workspace/workspace_helper'\nrequire 'm-git/workspace/path_helper'\n\nmodule MGit\n class Workspace\n # @"
},
{
"path": "lib/m-git.rb",
"chars": 1409,
"preview": "\n$:.unshift __dir__\n\nrequire 'open3'\nrequire 'fileutils'\nrequire 'json'\nrequire 'digest'\nrequire 'pathname'\nrequire 'uri"
},
{
"path": "m-git.gemspec",
"chars": 2090,
"preview": "\nlib = File.expand_path(\"../lib\", __FILE__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequire \"m-git/versi"
},
{
"path": "manifest.json",
"chars": 338,
"preview": "{\n \"remote\": \"https://gitee.com/baidu\",\n \"version\":1,\n \"mgit-excluded\": false,\n \"dest\": \"Sources\",\n \"repositories\":"
},
{
"path": "mgit",
"chars": 275,
"preview": "#!/usr/bin/env ruby -W0\n#coding=utf-8\n\n#\n# if $PROGRAM_NAME == __FILE__\n# ENV['BUNDLE_GEMFILE'] = File.expand_path('.."
},
{
"path": "test/argv/test_opt.rb",
"chars": 623,
"preview": "\n\nrequire_relative '../test_helper'\n\n\ndescribe MGit::ARGV::Opt do\n\n it \"opt key cant be nil\" do\n _{MGit::ARGV::Opt.n"
},
{
"path": "test/argv/test_opt_list.rb",
"chars": 1291,
"preview": "\n\n\nrequire_relative '../test_helper'\n\n\ndescribe MGit::ARGV::OptList do\n\n let(:list) {\n arr = []\n opt = MGit::ARGV"
},
{
"path": "test/argv/test_parser.rb",
"chars": 1651,
"preview": "\n\nrequire_relative '../test_helper'\n\n\ndescribe MGit::ARGV::Parser do\n\n let(:input_argv) {\n cmd = \"checkout -b branch"
},
{
"path": "test/foundation/test_dir.rb",
"chars": 832,
"preview": "\n\nrequire_relative '../test_helper'\n\n\ndescribe Dir do\n\n # before do\n # @git_dir = MiniTest::Mock.new\n # end\n\n it "
},
{
"path": "test/foundation/test_mgit_config.rb",
"chars": 1932,
"preview": "\n\nrequire_relative '../test_helper'\n\n\ndescribe MGit::MGitConfig do\n\n STUB_CONFIG = {\n 'managegit' => true,\n '"
},
{
"path": "test/foundation/test_utils.rb",
"chars": 2398,
"preview": "require_relative '../test_helper'\n\ndescribe MGit::Utils do\n\n it \"#change_dir(dir)\" do\n check_dir = File.expand_path("
},
{
"path": "test/manifest/test_light_repo.rb",
"chars": 80,
"preview": "\n\nrequire_relative '../test_helper'\n\ndescribe MGit::Manifest::LightRepo do\n\n\nend"
},
{
"path": "test/test_argv.rb",
"chars": 2742,
"preview": "require_relative 'test_helper'\n\ndescribe MGit::ARGV do\n\n let(:input_argv) {\n cmd = \"checkout -b branch_name --mrepo "
},
{
"path": "test/test_foundation.rb",
"chars": 67,
"preview": "\nrequire_relative 'test_helper'\n\ndescribe MGit::Foundation do\n\nend\n"
},
{
"path": "test/test_helper.rb",
"chars": 686,
"preview": "\nrequire 'minitest/autorun'\nrequire 'minitest/reporters'\nMiniTest::Reporters.use! Minitest::Reporters::SpecReporter.new\n"
},
{
"path": "test/test_manifest.rb",
"chars": 1,
"preview": "\n"
},
{
"path": "test/test_open_api.rb",
"chars": 68,
"preview": "\n\n\nrequire_relative './test_helper'\n\n\ndescribe MGit::OpenApi do\n\nend"
},
{
"path": "test/test_plugin_manager.rb",
"chars": 76,
"preview": "\n\n\nrequire_relative './test_helper'\n\n\ndescribe MGit::PluginManager do\n\n\n\nend"
},
{
"path": "test/test_workspace.rb",
"chars": 70,
"preview": "\n\n\nrequire_relative './test_helper'\n\n\ndescribe MGit::Workspace do\n\nend"
}
]
About this extraction
This page contains the full source code of the baidu/m-git GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 109 files (334.7 KB), approximately 103.2k tokens, and a symbol index with 674 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.