[
  {
    "path": ".gitignore",
    "content": "/.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.idea\n\n## File\n*.svn/\n*.DS_Store\n.DS_Store?\n\n\n## other\n*.pyc\n\n# log指令缓存\nlog_cache.json"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## Release Note:\n\n### 2.5.5\n\n-【bug修复】修复 box config -m 命令 不可用问题，修改参数类型为String\n\n### 2.5.4\n\n- mgit增加inject注入文件功能\n- 增加manifest.json文件，当前仓库可用于mgit-demo\n\n\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\nnationality, personal appearance, race, religion, or sexual identity and\norientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\nadvances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at baidu. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "Gemfile",
    "content": "source \"https://gems.ruby-china.com\"\n\n# git_source(:github) {|repo_name| \"https://github.com/#{repo_name}\" }\n\n# Specify your gem's dependencies in mgit.gemspec\ngemspec\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020 Baidu, Inc. All Rights Reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# MGit\n\nMGit 是一款 Ruby 封装的基于 Git 的多仓库管理工具，可以高效的、同时的对多个 Git 仓库执行 Git 命令。\n适合于在多个仓库中进行关联开发的项目，提高 Git 操作的效率，避免逐个执行 Git 命令带来的误操作风险。\n\n- **易用的命令**\n封装 Git 命令，命令和参数均由 Git 衍生而来，会使用 Git 就可以成本低上手 MGit。\n\n- **直观高效的执行命令**\n提供图表化的结果展示，开发者可以快速查看命令在多个仓库的执行结果；\n多线程并发执行多仓库命令，通过仓库缓存机制提高仓库的拉取效率；\n\n- **安全的执行命令**\n在执行命令前对多仓库状态进行安全检查：分支是否异常，工作区是否未提交代码等；\n对 .git 进行托管与 Git 工作区分类，避免误删丢失改动或提交；\n执行存在风险的操作时，会给与风险操作提示，避免误操作；\n\n- **方便扩展**\n支持加载 ruby-gem 包作为插件，gem 包名格式 `m-git-${suffix}`和`mgit-${suffix}`\n快速的扩展 MGit 的命令，增加自定义命令，扩展已有命令的功能；\n提供类似`git hook`的 hook 点，方便开发者实现自定义逻辑；\n\n## 快速开始\n  #### 1、安装 MGit 工具\n\n环境要求：\n\n- 系统：支持 macOS、Ubuntu，暂时不支持 Windows\n- Ruby版本: >= 2.3.7\n\n```ruby\n$ gem install m-git\n```\n\n#### 2、初始化多仓库 \n\n初始化多仓库使用 `mgit init` 命令;\n\n类似于 Git 从远程 clone 新仓库, 会将多个仓库 clone 到本地;\n\n下面通过一个 demo 体验一下 MGit 命令：\n\n```ruby\n# 2.1 建议在一个新文件夹中拉取demo\n$ mgit init -g https://github.com/baidu/m-git.git\n\n# 2.2 体验一下mgit命令\n$ mgit -l                 显示所有mgit管理的仓库\n$ mgit branch --compact   查看多仓库的分支\n$ mgit status             产看仓库分支超前/落后情况\n```\n\n\n#### 3、已有多仓库如何迁移到 MGit 管理\n\n- 根据文档[配置 manifest.json](docs/config/manifest.md)\n\n  将要管理的仓库都配置到 manifest.json 中\n  \n- 将 manifest.json 放到一个仓库中管理\n\n  这个仓库同样会在 manifest.json 中描述，并且需要配置 \"config-repo\": true \n  \n  这个仓库称为配置仓库，也叫做**主仓库**，其他仓库叫做**子仓库**\n     \n- 使用 `mgit init -f manifest文件路径` 初始化多仓库，命令测试 manifest.json 配置是否正常\n\n  注意这个命令不会重复拉取主仓库，只会拉取所有的子仓库到当前目录，并在当前目录创建一个.mgit\n  \n  你可以在当前目录中看到每个仓库的源码，他们的路径可以通过  manifest.json 的 dest字段配置\n  \n  你也可以在 .mgit/source-git/ 下看到所有仓库的.git, 这是 MGit 对所有仓库的托管\n  \n- 本地测试成功后，你可以提交主仓库中的 manifest.json，推送主仓库的变更到远端\n  \n- 通过 `mgit init -g 主仓库地址` 命令初始化多仓库\n  \n\n#### 4、进一步了解 MGit\n\n[常用命令](docs/use/common-commands.md)\n\n[manifest文件介绍](docs/config/manifest.md)\n\n[配置多仓库](docs/config/config-env.md) \n\n[使用 MGit 管理多仓库的案例](docs/use/how-to-start.md#4-mgit-) \n\n[了解更多](docs/references.md)\n\n\n## 测试\n\n单测在MGit仓库内的test文件夹下\n新建单测文件，必须以‘test_’开头\n执行单测：rake （如果报错尝试执行 bundle install）\n\n\n## 如何贡献\n\n欢迎开发者向 MGit 贡献代码。如果您开发了新功能或发现了 bug，欢迎给我们提交PR。\n\n代码贡献要求：\n1. 功能和实现应该具有通用性, 不是为了解决某个具体业务而定制的代码逻辑\n2. 代码质量高，符合 Ruby 编码规范\n3. 需要补充对应的单测 case\n\nissues贡献： 如在使用中遇到问题，请在 https://github.com/baidu/m-git/issues 新建 issues 反馈问题。\n\n\n\n"
  },
  {
    "path": "Rakefile",
    "content": "\nrequire \"bundler/gem_tasks\"\ntask :default => :test\n\n\nrequire 'rake/testtask'\n\nRake::TestTask.new(:test) do |t|\n  t.libs << \"test\"\n  t.pattern = 'test/**/test_*.rb'\n  t.verbose = true\nend"
  },
  {
    "path": "docs/commands/commands.md",
    "content": "\n## Command-line Reference\n参考：https://guides.cocoapods.org/terminal/commands.html#pod_spec_lint\n\n\n# Brief Description Of Commands\n\n## Initalizing\n+ mgit\n+ config     用于更新多仓库配置信息。\n\n## Creating && Workspace\n+ init       初始化多仓库目录。\n+ sync       根据配置表(从远端或本地)同步仓库到工作区，包括被锁定仓库，已经在工作的不作处理（默认不执行pull）。\n+ info       输出指定仓库的信息。\n+ clean      对指定或所有仓库执行\"git add . && git reset --hard\"操作，强制清空暂存区和工作区。\n+ delete     删除指定单个或多个仓库（包含被管理的.git文件和工程文件以及跟该.git关联的所有缓存）。\n\n## Snapshotting\n+ add        将文件改动加入暂存区。\n+ status     输出所有仓库的状态。包括：\"分支\"，\"暂存区\"，\"工作区\"，\"特殊（未跟踪和被忽略）\"，\"冲突\"。\n+ commit     将修改记录到版本库。\n+ reset      将当前HEAD指针还原到指定状态。\n\n## Branching\n+ branch     列出，创建和删除分支。\n+ checkout   切换分支或恢复工作区改动。\n+ merge      合并两个或多个开发历史。\n+ stash      使用git stash将当前工作区改动暂时存放起来。\n+ tag        增删查或验证标签。增加标签示例：mgit tag -a 'v0.0.1' -m 'Tag description message'\n+ log        输出指定仓库的提交历史。\n\n## Trunk\n+ fetch      从远程仓库下载数据对象和引用。\n+ pull       从仓库或本地分支获取数据并合并。\n+ rebase     重新将提交应用到其他基点，该命令不执行lock的仓库。\n+ push       更新远程分支和对应的数据对象。\n\n## Addition\n+ forall     对多仓库批量执行指令。\n"
  },
  {
    "path": "docs/config/config-env.md",
    "content": "### config.yml 介绍\n\n config.yml 是 MGit 的配置文件；\n\n MGit的配置内容保存在多仓库目录下的`.mgit/config.yml`文件中，配置仅针对当前的多仓库生效；\n\n\n\n- 通过 `$ mgit config --list`  可以查看当前config.yml中的配置\n\n  ```ruby\n  [是否将.git实体托管给MGit，托管后仓库内的.git目录是软链 (true/false)]\n  managegit: true\n  \n  [MGit操作的最大并发数? (Integer)]\n  maxconcurrentcount: (默认当前机器的逻辑 CPU 数)\n  \n  [是否按照配置表同步工作区? (true/false)]\n  syncworkspace: true\n  \n  [同步工作区时回收的仓库是否保留到缓存中? (true/false)]\n  savecache: false\n  \n  [是否开启日志打印，操作日志会收集到.mgit/log目录下? (true/false)]\n  logenable: true\n  \n  [日志的打印级别, DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, FATAL = 4]\n  loglevel: 0\n  ```\n\n- 通过 `$ mgit config --set` 可以对config.yml进行配置，如：\"mgit config -s maxconcurrentcount 20\"\n\n- 当前支持的几个配置项：\n\n  | key                | 类型    | 描述                                                         | 默认值 |\n  | ------------------ | :------ | ------------------------------------------------------------ | ------ |\n  | managegit          | Boolean | 如果该配置为`true`, 那么在通过MGit下载新仓库时（`mgit sync -n`)，会将工作区仓库内的`.git`托管给MGit（将`.git`移动到`.mgit/source-git/`下）。若配置为`false`，则任何情况下都不操作`.git`, 如有`.git`已经被托管，则弹出到工作区。 | true  |\n  | maxconcurrentcount | Integer | MGit操作的最大并发数.                                        | 当前机器的逻辑 CPU 数      |\n  | syncworkspace      | Boolean | 当配置表发生改变的时候，工作区内的仓库可能和配置表不匹配，若配置为`true`，会将上次操作和本地操作依据的配置表做对比，将多余的仓库（如有的话）缓存到`/.mgit/source-git/.../cache`目录下。若配置为`false`，则不操作。 | true   |\n  | savecache          | Boolean | 同步工作区时回收的仓库被放到缓存中后，若配置为`true`，则一直保留该工作区目录，若配置为`false`，则直接删除(`.git`被托管的仓库可以删除工作区，需要时导出即可)。该配置在`syncworkspace`为`false`时不生效。 | false  |\n  | logenable          | Boolean | 是否开启日志打印，操作日志会收集到.mgit/log目录下            | true   |\n  | loglevel           | Integer | 日志的打印级别, DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, FATAL = 4 | 1      |\n\n  \n  \n - 关于 managegit 与 syncworkspace 配置的详细说明\n \n   建议配置：\n      - managegit ：true\n      - syncworkspace ： true\n     \n   在使用MGit管理多仓库时，当前分支dev-1分支 有 a、b、c 三个仓库；\n   \n   1、现在需要checkout到一个旧分支feature-1，feature-1只配置了 a、b仓库，此时c仓库会被MGit回收，不会展示在当前工作区（syncworkspace ： true 按照配置表同步工作区）\n   \n   2、但c仓库还有未推到远端的开发分支和stash的代码，MGit会托管暂时不用的c仓库，保证本地c仓库中的的代码不会丢失（ managegit: true 托管工作区）\n   \n   3、当再checkout到dev-1分支时，会将被托管的c仓库同步回工作区（syncworkspace ： true 按照配置表同步工作区）"
  },
  {
    "path": "docs/config/manifest.md",
    "content": "### 1. manifest 配置介绍\n\nmanifest.json 配置表定义了多仓库管理所需的仓库信息，采用json格式，示例如下：\n\n```ruby\n{\n  \"remote\": \"https://github.com/baidu\",//远程仓库地址\n  \"version\":1, //配置文件版本\n  \"mgit-excluded\": false,\n  \"dest\": \"Sources\", //本地仓库相对mgit根目录存放路径\n  \"repositories\": { //源码列表，包含git仓库和非git仓库\n\t\"MainApp\": {\n      \"remote-path\": \"mainapp\" //远程仓库名，对于需要mgit管理的仓库是必需字段。此时git地址为：https://github.com/baidu/mainapp\n      \"config-repo\": true //指定该仓库为配置仓库，即包含本配置表的仓库，仅允许指定一个仓库\n    },\n    \"Watch\": {\n      \"remote-path\": \"watch\"\n      \"dest\":\"temp/test\" //可选，覆盖最外层定义，在mgit根目录下仓库的父目录。该仓库本地路径为\"<mgit根目录>/temp/test/Watch\"\n    },\n    \"BBAAccount\": {\n      \"remote-path\": \"bbaaccount\",\n      \"abs-dest\":\"/Users/someone/baidu/temp/my_account\" //仓库本地完整路径（指定后dest无效）\n    },\n    \"Script\": {\n      \"remote-path\": \"script\",\n      \"remote\": \"https://github.com/some_script\",//可选，覆盖最外层定义\n      \"lock\": {\n\t    \"branch\":\"my_branch\"//当前分支\n\t    或\"tag\":\"tag1\"//tag\n\t    或\"commit_id\":\"123456\"//HEAD指向的commit id\n\t  }//锁定某仓库状态，每次执行指令时会保持该仓库为指定的状态\n    },\n    \"Some_Repo\": {\n      \"remote-path\": \"some_repo\",\n      \"mgit-excluded\": true//指定不进行多仓库管理，可选\n    },\n    //本地git仓库或非git仓库（mgit不操作）\n    \"New_Repo\": {\n      \"dest\": \"Some/Dir\",\n      \"mgit-excluded\": true\n    }，\n    \"Test_Repo\": {\n      \"dest\": \"Some/Dir2\",\n      \"dummy\": true //（2.3.0已废弃）指定该仓库为占位仓库（mgit不操作，EasyBox组装器也不使用）\n    }\n  }\n}\n```\n\n\n\n### 2. manifest 配置中的具体字段介绍\n\n#### 2.1 一级字段\n\n| 字段名          | 说明                                                         | 必要性 | 值类型               |\n| :-------------- | :----------------------------------------------------------- | :----- | :------------------- |\n| `remote`        | 远程仓库git地址的根目录。注意，完整地址为`<remote>/<remote-path>`。 | 必需   | `String`             |\n| `version`       | 配置文件版本。                                               | 必需   | `Number`             |\n| `dest`          | 在mgit根目录下仓库的父目录，此时完整本地目录为：`<mgit根目录>/<dest>/<repo_name>`。 | 必需   | `String`             |\n| `mgit-excluded` | 指定为`true`则不被mgit纳入多仓库管理，即mgit不会操作该仓库。 | 可选   | `Bool`               |\n| `repositories`  | 源码列表。                                                   | 必需   | `JSON<String, JSON>` |\n\n\n\n####  2.2 `repositories`内的Json数据\n\n| 字段名        | 说明                                                         | 值类型 |\n| :------------ | :----------------------------------------------------------- | :----- |\n| `<repo_name>` | 仓库唯一标识，值为该仓库的配置。在`dest`生效时，为本地仓库目录名，此时完整本地目录为：`<mgit根目录>/<dest>/<repo_name>`。 | `JSON` |\n\n\n\n#### 2.3. 仓库配置字段\n\n| 字段名                | 说明                                                         | 必要性                                                       | 值类型   |\n| :-------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- | :------- |\n| `remote-path`         | 远程仓库git地址相对路径。注意，完整的远程地址为`<remote>/<remote-path>`。 | 有条件可选：在不显式指定`mgit-excluded`为`true`的情况下（即希望该仓库纳入mgit管理的话），是必须字段，否则可选。 | `String` |\n| `remote`              | 远程仓库地址根目录，如果指定则覆盖一级字段的`remote`。注意，完整的远程地址为`<remote>/<remote-path>`。 | 可选                                                         | `String` |\n| `lock`                | 指定后会锁定仓库状态，每次执行指令时会保持该仓库为锁定状态。状态可以指定`branch`, `tag`, `commit_id`中的一个，如`lock: { \"branch\": \"master\" }`。 | 可选                                                         | `Json`   |\n| `dest`                | 本地仓库相对mgit根目录存放的父目录，如果指定则覆盖一级字段的`dest`。此时完整本地目录为：`<mgit根目录>/<dest>/<repo_name>`。 | 可选                                                         | `String` |\n| `abs-dest`            | 本地仓库的完整存放路径，如果指定则`dest`失效，此时仓库完整的本地路径为`<abs-dest>`。 | 可选                                                         | `String` |\n| `config-repo`         | 如果指定为`true`则表明该仓库为包含该配置文件的配置仓库，最多只能指定一个。注意，指定了该仓库后，某些mgit操作会优先处理该仓库，如`checkout`,`merge`，`pull`。 | 可选                                                         | `Bool`   |\n| `mgit-excluded`       | 指定为`true`则mgit不操作，此处指定会覆盖一级字段的`mgit-excluded`。 | 可选                                                         | `Bool`   |\n| `[2.3.0已废弃] dummy` | 指定为`true`则表明该仓库是占位仓库，不受mgit操作，同时EasyBox组装器也不使用它的**源码**，单纯用于记录仓库信息，如一些完全二进制的三方库。<br><br>**提示**：指定`dummy:true`后，则默认指定`mgit-excluded:true`（如果显示指定了`mgit-excluded:false`，依然将其置为`true`）。该字段用于配合EasyBox进行工程组装，日常使用无需添加。 | 可选                                                         | `Bool`   |\n\n\n\n- **mgit根目录**是指mgit初始化的目录，在该目录下存在`.mgit`隐藏文件。\n- 配置表文件名必须是`manifest.json`\n- 配置表中同名字段覆盖优先级：仓库配置 > 全局配置。\n- 对于值为`Bool`类型的字段，缺省意味着该字段值为`false`。\n- 采用包含配置表的中央仓库的方式管理多仓库时，需要将配置仓库的配置也描述在配置表中。\n\n\n\n### 3. 本地配置表 local_manifest\n\n当你想在本地调试时，可以通过创建`local_manifest.json`临时修改多仓库配置；可以在不修改`manifest.json`的情况下对配置进行调整；\n\n可以选择以下方式创建 local_manifest.json：\n\n\n- 通过 `migt init` 命令自动创建 \n\n    - 执行命令时不带参数`-l`参数\n    \n      将自动在配置仓库创建不生效的`local_manifest.json`，内容为：\n    \n      ```ruby\n      {\n        \"repositories\": {\n         // 可自行根据需要添加/修改仓库配置\n        }\n      }\n      ```\n\n    - 执行命令时添加参数`-l`\n    \n      则在配置仓库创建`local_manifest.json`，此时该文件只包含配置仓库信息：\n    \n      ```ruby\n      // 此配置的含义：让mgit只管理配置仓库，其余仓库均不管理，主要用于简化壳工程初始化。\n      {\n        \"mgit-excluded\": true,\n        \"repositories\": {\n          \"config_repo_name\": {\n            \"mgit-excluded\": false\n           }\n        }\n      }\n      ```\n  \n\n\n-  通过 `mgit config` 命令创建\n\n    指定一个目录，在该目录下创建`local_manifest.json`，若目录不存在则自动创建。如：\n\n    ```ruby\n    $ mgit config -c /a/b/c\n    \n    ```\n\n    **注意**：\n\n    * `/a/b/c`为包含配置文件的文件夹目录，此时生成配置文件`/a/b/c/local_manifest.json`\n    * 如果未传入值，如：`mgit config -c`，那么若配置仓库存在的话，会在配置仓库中创建空的本地配置文件。\n\n\n-  手动创建 local_manifest.json\n\n    * 手动新建配置文件`local_manifest.json`，放到任意位置，执行以下指令将其托管给mgit：\n\n      ```\n      //注意，此后如果local_manifest.json位置发生了移动，需要重新执行该指令使其生效。\n      $ mgit config -u <path_to>/local_manifest.json\n      ```\n\n    * 在不执行指令的情况下，可手动创建`local_manifest.json`并将其放置于下列目录即可自动生效:\n\n      将创建的local_manifest.json 放到`manifest.json`所在目录 或 `.mgit/source-config`文件夹内 即可生效\n\n  \n\n### 4、 对local_manifest.json 校验与合并\n\n- `local_manifest.json`文件的字段与`manifest.json`完全一致，唯一区别是不会对它做字段合法性校验，对于不需要覆盖的字段可以缺省。\n- 执行mgit指令时，会将`manifest.json`和`local_manifest.json`合并，配置文件中的字段会被本地配置文件中定义的对应字段覆盖（`repositories`字段除外，而是仓库配置内的对应字段被覆盖），如：\n\n\n\n```ruby\n// manifest.json:\n{\n  \"remote\":\"https://github.com/baidu\",\n  \"version\":1,\n  \"dest\":\"Sources\",\n  \"repositories\": {\n    \"TestRepo1\": {\n      \"remote-path\":\"test1.git\",\n       \"mgit-excluded\": false\n    }\n  }\n}\n\n\n// local_manifest.json:\n{\n    \"remote\":\"https://github.com/baidu\", //覆盖原定义\n   \"repositories\": {\n      //将manifest.json内定义的TestRepo1排除管理\n       \"TestRepo1\": {\n           \"mgit-excluded\": true\n       }，\n       //添加一个新仓库，配置完成后执行mgit sync -n可下载该仓库\n       \"TestRepo2\": {\n          \"remote-path\":\"test.git\"\n       }\n   }\n}\n\n// 将合并为：\n{\n  \"remote\":\"https://github.com/baidu\", //被覆盖\n  \"version\":1,\n  \"dest\":\"Sources\",\n  \"repositories\": {\n    \"TestRepo1\": {\n      \"remote-path\":\"test1.git\",\n       \"mgit-excluded\": true //被覆盖\n    },\n    //被添加\n    \"TestRepo2\": {\n      \"remote-path\":\"test.git\"\n    }\n  }\n}\n```\n\n"
  },
  {
    "path": "docs/design/design.md",
    "content": "## 设计篇(略)\n### 基本架构"
  },
  {
    "path": "docs/design/principle.md",
    "content": "## 基本原则"
  },
  {
    "path": "docs/design/workspace.md",
    "content": "### 工作区\n"
  },
  {
    "path": "docs/extension/cmd_ext.md",
    "content": "### 一、扩展指令\n\n\nMGit 加载插件有**2种方式**：\n\n- **ruby gem 包加载**\n当运行 MGit 命令时，从所有的`ruby-gems`包中查找并加载 MGit 的插件，查找判定条件 gem 包中存在如下文件：\n```\n- m-git_plugin.rb\n- mgit_plugin.rb\n```\n> Tips：插件功能开发完成后，打包为 gem 包后发布到 gem 源，MGit 执行命令时将自动被加载\n\n- **inject 参数加载**\n```\n# 执行命令时加载插件\n# path_to_plugin：可以是待加载的插件文件或文件夹，\n# 如果是文件夹，则从中查找插件文件(m-git_plugin.rb、mgit_plugin.rb)并加载\n$ mgit add --inject=${path_to_plugin}\n```\n\n### 1分钟极简扩展（以Demo多仓库示例，待修改demo url）\n**需求：**假设我们要提供一个命令用于查看 MGit 管理的所有 Git 仓库，创建一个新的指令：hi\n```\n$ mgit hi //输出当前mgit管理的仓库列表\n```\n**Step0：**建议在一个新文件夹中拉取demo\n```\nmgit init -g https://github.com/baidu/m-git.git\n```\n**Step1：**新建一个文件：hi.rb （无所谓名称）\n**Step2：**增加一个继承自 BaseCommand 类的子类`Hi`，指令默认使用子类的名称，可使用`self.cmd`方法重写\n```\nmodule MGit\n  class Hi < BaseCommand\n    def execute(argv)\n\t  # Do something for each repo.\n      puts all_repos.map(&:name).join(',')\n    end\n  end\nend\n```\n\n**测试：**\n```\n# inject 指向扩展命令的文件路径(或全路径)\nmgit hi --inject=hi.rb\n> m-git,DDParser\n```"
  },
  {
    "path": "docs/extension/hooks.md",
    "content": "### hook\n\n#### 一、什么是hook\n\nhook是在MGit 仓库中特定事件触发后被调用的脚本。hook最常见的使用场景包括根据仓库状态改变项目环境、接入持续集成工作流等。 由于脚本是可以完全定制，所以你可以用hook来自动化或者优化你开发工作流中任意部分。\n\n#### 二、MGit的hook\n\nhook脚本位于多仓库目录下的`/.mgit/hooks`内，下表列出个各hook的作用\n\n| hook脚本              | 作用                                                         |\n| --------------------- | ------------------------------------------------------------ |\n| pre_hook.rb           | 指令执行前hook(执行时机较早，内部只能获取当前指令和多仓库根目录) |\n| post_hook.rb          | 指令执行后hook                                               |\n| pre_exec_hook.rb      | 指令执行前hook(内部除当前指令和多仓库根目录外，还可获取执行仓库的 |\n| manifest_hook.rb      | mgit读取配置文件前执行的hook                                 |\n| post_download_hook.rb | mgit sync下载新仓库后执行的hook                              |\n| pre_push_hook.rb      | mgit push前执行的hook(类似pre_exec_hook)                     |\n\n【注意】执行顺序：\n\n1. mgit指令pre_hook\n2. mgit多仓库pre_hook\n3. manifest_hook\n4. pre_exec_hook/pre_push_hook/post_download_hook\n5. git前置hook\n6. git后置hook\n7. mgit多仓库post_hook\n8. mgit指令post_hook\n\n\n\n如下代码是   pre_hook.rb 的模板:  \n\n```\n\n#coding=utf-8\nmodule MGitTemplate\n  class PreHook\n    # hook接口，用于接受本次指令执行前的数据\n    #\n    # @param cmd [String] 本次执行指令\n    #\n    # @param opts [String] 本次执行指令参数\n    #\n    # @param mgit_root [String] mgit根目录\n    #\n    def self.run(cmd, opts, mgit_root)\n    \n    end\n  end\nend\n```\n\n"
  },
  {
    "path": "docs/extension/plugins.md",
    "content": "### 插件\n- 插件介绍，可以做什么\n- 如何扩展插件\n- 如何引用其他插件"
  },
  {
    "path": "docs/extension/submodule.md",
    "content": "### 用做子工具\n- 介绍其他工具如何调用mgit作为模块"
  },
  {
    "path": "docs/faq/faq.md",
    "content": "## F.A.Q"
  },
  {
    "path": "docs/references.md",
    "content": "<center style=\"font-family:verdana\">MGit相关文档</center>\n\n* [介绍](../README.md)\n\n* 使用篇\n    * [快速开始](use/how-to-start.md)\n    * [常用命令](use/common-commands.md)\n* 配置篇\n    * [Manifest](config/manifest.md)\n    * [config.yml](config/config-env.md)\n* 扩展篇\n    * [扩展指令](extension/cmd_ext.md)\n    * [MGit Hooks](extension/hooks.md)\n    \n      \n    \n* [F.A.Q](faq/faq.md)\n\n* [Command-line Reference](commands/commands.md)"
  },
  {
    "path": "docs/use/common-commands.md",
    "content": "## 常用命令\n\n> 指令描述符说明： \n>\n> < > : 占位参数\n>\n> [  ]  : 可选组合\n>\n> (  )  ：必选组合 \n>\n>  |     :  互斥参数 \n>\n> ···   ：可重复指定前一个参数\n\n\n### mgit    \n\n` mgit [-l [-a]|-s] [-v] [-h]`\n\n| 特有参数          | 说明                                                         |\n| ----------------- | ------------------------------------------------------------ |\n| `-a, --all`       | 指定操作所有(包含不被mgit管理)的仓库，可配合`-l`合并使用: `mgit -al` |\n| `-h, --help`      | 显示帮助。                                                   |\n| `-l, --list`      | 显示所有migt管理的仓库。                                     |\n| `-s, --size`      | 显示所有仓库的磁盘占用量。                                   |\n| `-v, --version`   | 显示mgit版本。                                               |\n| `-w, --workspace` | 显示当前mgit工程管理根目录(`.mgit`所在目录)。                |\n\n【注意】使用`-l, --list`参数列出所有被mgit管理的仓库，以表格的形式输出，其中表格头部为包含该仓库的目录（相对mgit根目录），同时标明本地缺失仓库。\n\n\n\n### add  \n\n` mgit add [<git-add-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]`\n\n 添加文件到暂存区\n\n| 特有参数     | 说明                                                         |\n| ------------ | ------------------------------------------------------------ |\n| `--mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `--el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `--help`     | 显示帮助。                                                   |\n\n【注意】执行前会校验各个仓库所在分支是否一致，否则会提示是否继续：\n\n\n\n### branch   \n\n`mgit branch [<git-branch-option>|--compact] [(--mrepo|--el-mrepo) <repo>...] [--help]`\n\n对多仓库进行分支操作\n\n| 特有参数     | 说明                                                         |\n| ------------ | ------------------------------------------------------------ |\n| `--mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `--compact`  | 以紧凑的方式显示所有仓库的当前分支。                         |\n| `--el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `--help`     | 显示帮助。                                                   |\n\n【注意】在指定`--compact`的情况下，其余传入的原生`git-branch-option`指令将不生效。指定`--compact`时，会将所有仓库的当前分支做一个归类，便于检查哪些仓库当前处于不同分支，对于特殊状态（HEAD游离和被锁定的仓库）也会显示出来。\n ![图片](branch_command_01.png)\n\n### checkout   \n\n`mgit checkout [<git-checkout-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]`\n\n克隆远程仓库\n\n| 特有参数     | 说明                                                         |\n| ------------ | ------------------------------------------------------------ |\n| `--mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `--el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `--help`     | 显示帮助。                                                   |\n\n【注意】\n\n1. 若指定了配置仓库，执行该指令时会先checkout配置仓库仓库，如果它本地有修改，则切换失败，需要自行将配置仓库处理干净后重试，也可以选择跳过继续。如果使用checkout切分支，\n   此处建议先**取消操作并清理配置仓库**的改动再重试，因为切换的目标分支上配置仓库中的配置文件可能与当前不同，不先完成它的切换，后续操作的仓库可能产生遗漏。\n   ![图片](checkout_command_01.png)\n2. 在配置仓库切换成功后，会根据配置仓库内的配置表来`checkout`其余仓库，此时如果有异常状态仓库，会给出提示：\n   ![图片](checkout_command_02.png)\n3. 在配置仓库切换成功后，配置表可能发生改变，此时可能会遇到仓库缺失问题，如：A分支10个仓库，B分支11个仓库，当前从A分支切换到B分支，B分支多的1个仓库本地没有，此时会自动下载缺失仓库，并根据当前多数仓库所处分支进行推荐切换。\n   ![图片](checkout_command_03.png)\n### clean\n\n`mgit clean [(-m|-e) <repo>...] [-h]`\n\n用于清空所有工作区和暂存区，即对所有仓库或指定仓库执行`git add . && git reset --hard`\n\n| 特有参数         | 说明                                                         |\n| ---------------- | ------------------------------------------------------------ |\n| `-m, --mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `-e, --el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `-h, --help`     | 显示帮助。                                                   |\n\n\n\n### commit\n\n`mgit commit [<git-commit-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]`\n\n| 特有参数     | 说明                                                         |\n| ------------ | ------------------------------------------------------------ |\n| `--mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `--el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `--help`     | 显示帮助。                                                   |\n\n【注意】\n\n- 如果当前仓库的暂存区没有待提交内容的话，则不会执行`commit`操作。\n- 执行前会校验各仓库分支是否统一。\n- 本指令不支持交互式操作，因此不支持`--ammend`。\n\n\n\n### config \n\n```\nmgit config (-m|-u) \nmgit config -c \nmgit config (-l | -s  )\nmgit config -h\n\n```\n\n| 特有参数                | 说明                                                         |\n| ----------------------- | ------------------------------------------------------------ |\n| `-c, --create-local`    | 在指定目录下创建本地配置文件，若目录不存在则自动创建。如执行：`mgit config -c /a/b/c`，则生成本地配置文件：`/a/b/c/local_manifest.json`。如果未传入值，如：`mgit config -c`，那么会在配置仓库中创建本地配置文件(若配置仓库存在的话)。 |\n| `-m, --update-manifest` | 该指令用于更新mgit所使用的配置文件，如：`mgit config -m manifest.json`。 |\n| `-u, --update-local`    | 该指令用于更新mgit所使用的本地配置文件，如：`mgit config -u /local_manifest.json`。 |\n| `-l, --list`            | 列出当前MGit所有配置，无参数，如：`mgit config -l`。         |\n| `-s, --set`             | 对MGit进行配置，遵守格式：`mgit config -s  `，如：`mgit config -s maxconcurrentcount 5`。 |\n| `-h, --help`            | 显示帮助。                                                   |\n\n\n命令使用细节请查看[config配置文档](../config/config-env.md)\n\n\n\n### delete \n\n`mgit delete <repo>... [-h]`\n\n该指令用于删除单一仓库的，包括被mgit管理存在于.mgit文件夹中的git实体和存在.mgit外部的工程文件。\n\n| 特有参数     | 说明       |\n| ------------ | ---------- |\n| `-h, --help` | 显示帮助。 |\n\n【注意】如果需要删除某个仓库，仅仅手动删除.mgit外部的工程文件并不完整，还需要删除.mgit/source-git内的git实体，因此如果需要删除一个仓库的话，最好使用该指令来完整删除。\n\n\n\n### fetch  \n\n`mgit fetch [<git-fetch-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]`\n\n 从远程仓库获取代码\n\n| 特有参数     | 说明                                                         |\n| ------------ | ------------------------------------------------------------ |\n| `--mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `--el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `--help`     | 显示帮助。                                                   |\n\n\n\n### forall  \n\n`mgit forall -c '<instruction>' [(-m|-e) <repo>...] [-n] [-h]`\n\n该指令用于在指定（或所有）仓库上执行自定义指令\n\n| 特有参数           | 说明                                                         |\n| ------------------ | ------------------------------------------------------------ |\n| `-c, --command`    | 指定需要执行的shell命令，如：`mgit -c \"git status -s\"`（注意要带引号）。 |\n| `-n, --concurrent` | 可选参数，若指定，则shell命令以多线程方式执行（若缺省则以单线程串行方式执行）。 |\n| `-m, --mrepo`      | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `-e, --el-mrepo`   | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `-h, --help`       | 显示帮助。                                                   |\n\n\n\n### info   \n\n`mgit info <repo>... [-h]`\n\n产看一个仓库的信息\n\n| 特有参数     | 说明       |\n| ------------ | ---------- |\n| `-h, --help` | 显示帮助。 |\n\n\n\n### log  \n\n`mgit log <repo> [-n] [-h]`\n\n 输出单一仓库的log\n\n| 特有参数       | 说明                                         |\n| -------------- | -------------------------------------------- |\n| `-h, --help`   | 显示帮助。                                   |\n| `-n, --number` | 指定需要显示的提交log个数，不指定则默认500。 |\n\n\n\n### merge\n\n```\nmgit merge [] [--pull] [(--mrepo|--el-mrepo) ...] [--help]\nmgit merge --continue\nmgit merge --abort\n```\n\n| 特有参数     | 说明                                                         |\n| ------------ | ------------------------------------------------------------ |\n| `--mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `--el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `--pull`     | 可选参数，指定后在合并仓库前会自动拉取远程分支更新代码，否则会有交互式询问。如：`mgit merge --pull` |\n| `--continue` | MGit自定义参数，仅在操作多仓库过程中出现问题停止，执行状态进入中间态后可用。该参数只能单独使用，解决问题后可执行`mgit merge --continue`继续操作其余仓库。 |\n| `--abort`    | Git原生参数，用于取消`git merge`中间态，但MGit做了增强，该参数可同时消除`mgit merge`中间态，且只能单独使用：`mgit merge --abort` |\n| `--help`     | 显示帮助。                                                   |\n\n【注意】\n\n1. 若指定了配置仓库，执行该指令时会先merge配置仓库，然后根据它内部的配置表merge其余仓库。配置仓库merge完成后，配置表可能发生改变，此时可能会检测到部分仓库本地缺失，此时会自动下载并引导分支切换（提示如checkout指令所述）。\n2. 执行前会校验各仓库分支是否统一。\n3. 中间态：由于该操作分为两段式，第一阶段操作配置仓库，第二阶段操作子仓库，且在操作前均可能拉取最新代码，如果操作失败会进入`中间态`。根据提示解决问题后执行`mgit merge --continue`即可继续执行，也可执行`mgit merge --abort`放弃操作。\n4. 该指令默认添加`--no-ff`参数，如果需要fast-forwarding，请手动添加`--ff`。\n\n\n\n### pull\n\n```\nmgit pull [ [(--mrepo|--el-mrepo) ...] [--auto-exec] [--no-check] [--include-lock] [--help]\nmgit pull --continue`\nmgit pull --abort\n```\n\n| 特有参数         | 说明                                                         |\n| ---------------- | ------------------------------------------------------------ |\n| `--auto-exec`    | 指定该参数会跳过所有交互场景，并自动选择需要的操作执行。该参数主要用于脚本调用mgit进行自动化操作，日常RD开发不应当使用。 |\n| `--mrepo`        | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `--el-mrepo`     | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `--no-check`     | 指定该参数意味着执行前跳过仓库的状态检查，直接对指定或所有仓库执行pull操作，有一定风险，请慎重执行。 |\n| `--include-lock` | 指定该参数意味着同时也操作lock仓库。                         |\n| `--continue`     | MGit自定义参数，仅在操作多仓库过程中出现问题停止，执行状态进入中间态后可用。该参数只能单独使用，解决问题后可执行`mgit pull --continue`继续操作其余仓库。 |\n| `--abort`        | MGit自定义参数，仅在操作多仓库过程中出现问题停止，执行状态进入中间态后可用。该参数用于清除操作中间态，且只能单独使用：`mgit pull --abort`。 |\n| `--help`         | 显示帮助。                                                   |\n\n【注意】\n\n1. 若指定了配置仓库，执行该指令时会先pull配置仓库，然后根据它内部的配置表pull其余仓库。\n2. 执行时，会查看当前分支（如A）和远程分支（若有的话，如origin/A），只有当前分支（A）与远程分支（origin/A）出现落后或分叉的仓库才会执行指令。\n3. 执行时，如果本地分支A和其跟踪的远程分支origin/A存在分叉，意味着执行pull之后会跟远程分支合并产生新节点，此时mgit会自动输入合法的log信息，并生成新提交。\n4. 为了保证本地保存的远程分支（origin/A）为最新，执行前会先fetch一次。\n5. 对于本地有异常的仓库，会提示执行操作，需要二次确认。\n6. 执行前会校验各仓库分支是否统一。\n7. 中间态：操作主仓库后如果导致配置文件冲突，会进入中间态。解决完冲突后，执行`mgit pull --continue`继续操作子仓库，或执行`mgit pull --abort`放弃本次操作。\n\n### push\n\n`mgit push [<git-push-option>|--auto] [(--mrepo|--el-mrepo) <repo>...] [--group-id] [--help]`\n\n| 特有参数     | 说明                                                         |\n| ------------ | ------------------------------------------------------------ |\n| `--mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `--el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `--group-id` | 指定一个group id，可将本次提交在远程做归类，若未指定则自动生成。生成的id将自动附加在url后：`mgit push --group-id  = git push origin HEAD:refs/for/%topic=` |\n| `--help`     | 显示帮助。                                                   |\n\n【注意】\n\n1. 执行时会查看当前分支（如A）和远程分支（若有的话，如origin/A），只有当前分支（A）超前远程分支（origin/A）的仓库才会执行指令。注意，为了保证本地保存到远程分支（origin/A）为最新，执行前会先fetch一次。\n2. 对于本地有异常的仓库，会提示执行操作（如`pull`指令描述所示）。\n3. 执行前会校验各仓库分支是否统一。\n\n\n\n### rebase\n\n```\nmgit rebase [<git-rebase-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]\nmgit rebase --continue\nmgit rebase --abort\n```\n\n| 特有参数     | 说明                                                         |\n| ------------ | ------------------------------------------------------------ |\n| `--mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `--el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `--continue` | MGit自定义参数，仅在操作多仓库过程中出现问题停止，执行状态进入中间态后可用。该参数只能单独使用，解决问题后可执行`mgit rebase --continue`继续操作其余仓库。 |\n| `--abort`    | Git原生参数，用于取消`git rebase`中间态，但MGit做了增强，该参数可同时消除`mgit rebase`中间态，且只能单独使用：`mgit rebase --abort` |\n| `--help`     | 显示帮助。                                                   |\n\n【注意】\n\n1. 执行前会校验各仓库分支是否统一。\n2. 该操作禁止使用`-i`和`--interactive`参数。\n3. 中间态：同merge操作。\n\n\n\n### reset \n\n`mgit reset [<git-reset-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]`\n\n| 特有参数     | 说明                                                         |\n| ------------ | ------------------------------------------------------------ |\n| `--mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `--el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `--help`     | 显示帮助。                                                   |\n\n【注意】执行前会校验各仓库分支是否统一。\n\n\n\n### snap\n\n`mgit snap [-m <message>|-r <snapshot_id>|-l] [-h]`\n\n该指令用于根据当前多仓库状态生成快照，也可以从快照中恢复。\n\n| 特有参数        | 说明                                                         |\n| --------------- | ------------------------------------------------------------ |\n| `-r, --restore` | 从快照中恢复当前仓库，该操作执行前会清空所有未提交的仓库改动，请谨慎执行。用法：`mgit snap -r `。 |\n| `-l, --list`    | 列出所有本地存储的快照。                                     |\n| `-m, --message` | 执行本次快照时附带的说明信息。用法：`mgit snap -m 'version x'`。 |\n| `-h, --help`    | 显示帮助。                                                   |\n\n\n\n### stash\n\n`mgit stash [<option> <value>...] [(--mrepo|--el-mrepo) <repo>...] [--help]`\n\n| 特有参数     | 说明                                                         |\n| ------------ | ------------------------------------------------------------ |\n| `--apply`    | 恢复储藏，用法：`mgit stash --apply 'stash_name'`。          |\n| `--mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `--clear`    | 清空所有储藏。                                               |\n| `--el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `--push`     | 添加储藏，用法：`mgit stash --push 'stash_name'`。           |\n| `--pop`      | 恢复储藏，用法：`mgit stash --pop 'stash_name'`。            |\n| `--list`     | 显示储藏列表。                                               |\n| `--help`     | 显示帮助。                                                   |\n\n\n\n### status\n\n`mgit status [(-m|-e) <repo>...] [-h]`\n\n| 特有参数         | 说明                                                         |\n| ---------------- | ------------------------------------------------------------ |\n| `-m, --mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `-e, --el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `-h, --help`     | 显示帮助。                                                   |\n\nstatus指令是一个比较重要的指令，MGit2.0版对其进行了加强，它的显示区域分为两部分：\n\n- 文件状态：显示四个分类的文件状态列表（`暂存区`，`工作区`，`特殊`，`冲突`）\n- 分支状态：显示当前分支和远程分支的对比状态（`超前`，`落后`，`同步`，`游离`）\n\n【注意】\n\n1. 文件状态显示区对所有文件进行归类，每个文件左侧会显示当前文件改动状态：`已修改`，`已添加`, `已删除`，`重命名`，`已拷贝`，`未跟踪`，`被忽略`，其中冲突区的文件会显示两个状态，如：`[添加|修改]`，左侧表示当前分支状态，右侧表示合并分支状态，即当前分支上的该文件为添加状态，合并过来的分支上的该文件为修改状态。\n2. 对于干净的仓库，无输出。\n\n\n\n###  sync \n\n`mgit sync [-a|-n|-c] [<repo>...] [-p] [-o] [-h]`\n\n该指令用于根据配置文件同步被锁定的仓库，更新仓库和下载缺失仓库。\n\n| 特有参数         | 说明                                                         |\n| ---------------- | ------------------------------------------------------------ |\n| `-a, --all`      | 对所有(包含**不被mgit管理**的)仓库操作:1.如果本地缺失则下载。2.如果本地存在且被锁定则同步到锁定状态。注意，如果需要下载仓库，需要配置仓库URL，否则跳过。 |\n| `-c, --clone`    | 下载一组仓库(包含**不被mgit管理**的仓库)，如: `mgit sync -c repo1 repo2...`。 |\n| `-n, --new-repo` | 下载配置表中指定**被mgit管理**，但本地不存在的仓库，已有仓库不做任何处理。 |\n| `-o, --no-link`  | 指定后，对于新下载的仓库不会将`.git`实体迁移到.mgit/source-git文件夹中管理。该参数适合开发中途接入mgit的用户使用。 |\n| `-p, --pull`     | 若被操作仓库本地存在的话，则进一步执行pull操作更新，配合其他指令使用，如: mgit sync -ap。 |\n| `-h, --help`     | 显示帮助。                                                   |\n\n特殊用法：\n\n- `mgit sync`：作用同`-a`，区别是只操作**被mgit管理的仓库**。\n- `mgit sync ...`：作用同`-a`，区别是只操作指定(包含**不被mgit管理**的)仓库。\n\n操作仓库作用域总结：\n\n| 指令            | 被mgit管理的仓库 | 不被mgit管理仓库 |\n| --------------- | ---------------- | ---------------- |\n| `mgit sync`     | ✔                | ✘                |\n| `mgit sync ...` | ✔                | ✔                |\n| `mgit sync -a`  | ✔                | ✔                |\n| `mgit sync -c`  | ✔                | ✔                |\n| `mgit sync -n`  | ✔                | ✘                |\n\n【注意】\n\n1. **同步**：是指仓库存在且被锁定（指定了`lock`字段）的情况下，将仓库切换到配置文件指定的锁定状态（`branch`，`tag`或`commit-id`)，此时：\n2. 需要切换分支的话，若有本地改动，则切换失败；若无本地分支，则提示是否创建并切换；若有对应分支，则直接切换\n3. 需要切到具体`tag`或`commit-id`的话，若仓库不存在对应值则失败。\n4. 在处理缺失仓库时，如果.mgit/source-git文件夹下有缺失的git实体，那么将直接从该实体clone出来（并将remote url修改为远程地址），否则直接从远程clone。\n\n\n\n### tag \n\n`mgit tag [<git-tag-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]`\n\n| 特有参数     | 说明                                                         |\n| ------------ | ------------------------------------------------------------ |\n| `--mrepo`    | 指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。 |\n| `--el-mrepo` | 指定需要排除执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：`--el-mrepo boxapp BBAAccount`，若缺省则对所有仓库执行指令。与`--mrepo`同时指定时无效。 |\n| `--help`     | 显示帮助。                                                   |\n\n\n\n## 一些使用技巧\n\n| 特有参数                                     | 说明                      |\n| -------------------------------------------- | ------------------------- |\n| mgit -l                                      | 查看当前被管理的仓库      |\n| mgit info <repo1> <repo2>... #repo大小写均可 | 查看某个仓库的详细信息    |\n| mgit branch --compact                        | 查看归并分支              |\n| mgit clean                                   | 一键清空工作区            |\n| mgit status                                  | 产看仓库分支超前/落后情况 |\n| mgit push                                    | 一键推动新分支、新提交    |\n\n"
  },
  {
    "path": "docs/use/how-to-start.md",
    "content": "## 快速开始\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```\n\n### 2、初始化多仓库Demo\n\n初始化多仓库使用 `mgit init` 命令;\n\n类似于 Git 从远程 clone 新仓库, 会将多个仓库 clone 到本地;\n\n下面通过一个demo体验一下MGit命令：\n\n```ruby\n# 2.1 建议在一个新文件夹中拉取demo\n$ mgit init -g https://github.com/baidu/m-git.git\n\n# 2.2 体验一下mgit命令\n$ mgit -l                 显示所有mgit管理的仓库\n$ mgit branch --compact   查看多仓库的分支\n$ mgit status             产看仓库分支超前/落后情况\n```\n\n### 3、已有多仓库如何迁移到 MGit 管理\n\n- 根据文档[配置 manifest.json](../config/manifest.md)\n\n  将要管理的仓库都配置到 manifest.json 中\n  \n- 将 manifest.json 放到一个仓库中管理\n\n  这个仓库同样会在 manifest.json 中描述，并且需要配置 \"config-repo\": true \n  \n  这个仓库称为配置仓库，也叫做**主仓库**，其他仓库叫做**子仓库**\n     \n- 使用 `mgit init -f manifest文件路径`  命令初始化多仓库，测试 manifest.json 配置是否正常\n\n  注意这个命令不会重复拉取主仓库，只会拉取所有的子仓库到当前目录，并在当前目录创建一个.mgit\n  \n  你可以在当前目录中看到每个仓库的源码，他们的路径可以通过  manifest.json 的 dest字段配置\n  \n  你也可以在 .mgit/source-git/ 下看到所有仓库的.git, 这是 MGit 对所有仓库的托管\n  \n- 本地测试成功后，你可以提交主仓库中的 manifest.json，推送主仓库的变更到远端\n  \n- 通过 `mgit init -g 主仓库地址` 命令初始化多仓库\n  \n\n     \n### 4、使用 MGit 管理多仓库的案例\n  \n  推荐使用**同名分支原则**管理多仓库: 子仓库的分支与主仓库保持一致（子仓库单独锁定分支的情况除外）\n   \n  推荐通过在主仓库中[配置 local_manifest.json](../config/manifest.md#3--local_manifest)， 控制要同时操作哪些仓库\n\n  例如： 一个工程中有 a、b、c、d 、e、f、g等多个仓库, 当一个需求 A 涉及到三个仓库 a 、b、 c时\n  \n  - 从 master 新建开发分支 feature_A\n    - 拉取主仓库到开发分支 feature_A `git checkout -b feature_A`(操作单仓库时可以直接使用git命令)\n    - [创建 local_manifest.json](../config/manifest.md#3--local_manifest) 在 local_manifest.json 中配置 MGit 只管理主仓库和子仓库 a 、b、 c \n      ```ruby\n          {\n            \"mgit-excluded\": true,\n            \"repositories\": {\n              \"config_repo\": {\n                \"mgit-excluded\": false\n              },\n              \"a\": {\n                \"mgit-excluded\": false\n               },\n              \"b\": {\n                \"mgit-excluded\": false\n               },\n              \"c\": {\n                \"mgit-excluded\": false\n               }\n            }\n          }\n      ```\n    - push 主仓库的变更到远程 （包含 local_manifest.json配置的变更）\n    - 拉取子仓库 a 、b、 c 的开发分支 feature_A  `mgit checkout -b feature_A`\n    - 使用 `mgit branch --compact` 命令查看分支状态\n    \n  - 在 feature_A 分支开发需求\n    - `mgit status`  查看多仓库状态\n    - `mgit add .`   添加到暂存区\n    - `mgit commit -m 'xxx'` 提交多仓库的变更\n    - `mgit push`    推送多仓库到远程\n    \n  - 合并 feature_A 到 master 分支\n    - `mgit merge feature_A -m \"xxx comment...\"` 将代码 merge 回主干分支\n    - 删除主仓库中的 local_manifest.json 文件（如果有增、删、改仓库配置的情况，需要更新到 manifest.json）\n    \n  \n"
  },
  {
    "path": "lib/m-git/argv/opt.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  class ARGV\n    # 单个选项, 如‘--k1=v1 --k2 v2 v3 --flag’\n    # '--k1=v1'     key: '--k1', value: 'v1'\n    # '--k2 v2 v3'  key: '--k2', value: ['v2', 'v3']\n    # '--flag'      key: '--flag', value: true\n    class Opt\n      attr_reader   :key\n      attr_accessor :value\n      attr_accessor :short_key        # 短参数格式，如’--command’对应的‘-c’\n      attr_accessor :priority         # 参数解析优先级\n      attr_accessor :info             # 参数说明\n\n      # @!attribute value值类型\n      # :array  :string  :boolean\n      attr_accessor :value_type\n\n      def initialize(key, default:nil, short_key:nil, priority:-1, info:nil, type: :array)\n        raise(\"初始化Opt选项必须有key\") if key.nil?\n        @key, @value, @short_key, @priority, @info = key, default, short_key, priority, info\n        @value_type = type\n      end\n\n      def empty?\n        value.nil? || value == '' || value == [] || value == false\n      end\n\n      def validate?\n        return false if empty?\n        value.is_a?(value_type)\n      end\n    end\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/argv/opt_list.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  class ARGV\n    # 参数对象列表\n    class OptList\n\n      # [Array<ARGV::Opt>] 参数对象数组\n      attr_accessor :opts\n\n      # attr_reader :valid_opts\n\n      def initialize(opts)\n        @opts = opts\n      end\n\n      def valid_opts\n        @opts.select { |e| !e.empty? }\n      end\n\n      # 获取某个参数对象\n      #\n      # @param key [String] 参数名，如‘--key’\n      #\n      # @return [ARGV::Opt] 参数对象，若参数未设置过则返回nil\n      #\n      def opt(key)\n        valid_opts.find { |e| (e.key == key || e.short_key == key) }\n      end\n      alias_method :opt_with, :opt\n\n      # 判断参数是否设置过值\n      #\n      # @return [Boolean] 参数是否设置过值\n      #\n      def did_set_opt?(key)\n        !opt(key).nil?\n      end\n\n      ## all opts ###\n\n      # 返回某个注册过的参数对象\n      #\n      # @param key [String] 参数名，如‘--key’\n      #\n      # @return [ARGV::Opt] 参数对象，无论参数是否设置过，只要注册过就返回\n      #\n      def registered_opt(key)\n        @opts.find { |e| (e.key == key || e.short_key == key) }\n      end\n\n      # 判断参数是否注册过\n      #\n      # @return [Boolean] 参数是否注册过\n      #\n      def did_register_opt?(key)\n        !registered_opt(key).nil?\n      end\n\n      # 将参数根据优先级排序（逆序）后返回\n      #\n      # @return [Array<ARGV::Opt>] 包含参数对象的数组\n      #\n      def opts_ordered_by_priority\n        # 按照优先级进行降序排序\n        opts.sort_by { |e| e.priority }.reverse\n      end\n    end\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/argv/parser.rb",
    "content": "\nmodule MGit\n  class ARGV\n    module Parser\n\n      # @param [Array] argv\n      #\n      # @return [String, Array, Array]\n      # 返回cmd 和 参数列表\n      # # 将参数初步分解，便于后续处理\n      # 如：argv = [command zzzz -u sdfsd sdf --mmmm yoo ssss qqq --test asdfa asd ad f --xxx asd as dfa --yoo=\"ajsdaf\" --ppp -abc]\n      # 分解为：command, [zzzz], [[-u, sdfsd, sdf], [--mmmm, yoo, ssss, qqq], [--test, asdfa, asd, ad, f], [--xxx, asd, as, dfa], [--yoo, \"ajsdaf\"], [--ppp], [-a], [-b], [-c]]\n      #\n      # 初步解析参数\n      def self.parse(argv)\n        absolute_cmd = argv.join(' ')\n        cmd = argv.shift\n        pure_opts = argv.join(' ')\n\n        # 将参数初步分解，便于后续处理\n        # 如：zzzz -u sdfsd sdf --mmmm yoo ssss qqq --test asdfa asd ad f --xxx asd as dfa --yoo=\"ajsdaf\" --ppp -abc\n        # 分解为：[[zzzz], [-u, sdfsd, sdf], [--mmmm, yoo, ssss, qqq], [--test, asdfa, asd, ad, f], [--xxx, asd, as, dfa], [--yoo, \"ajsdaf\"], [--ppp], [-a], [-b], [-c]]\n        temp = []\n        raw_opts = []\n        argv.each_with_index { |e, idx|\n\n          Foundation.help!(\"参数\\\"#{e}\\\"格式错误，请使用格式如：\\\"--long\\\"或\\\"-s\\\"\") if (e =~ /---/) == 0\n\n          # 检查是否是带'--'或'-'的参数\n          if (e =~ /-/) == 0\n            # 回收缓存\n            raw_opts.push(temp) if temp.length != 0\n            # 清空临时缓存\n            temp = []\n\n            # 如果是合并的短指令，如'-al = -a + -l'，则分拆后直接装入raw_opts数组\n            # 因为只有不需要传入值的短参数才能合并，因此不利用临时缓存读取后续参数值\n            if e.length > 2 && (e =~ /--/).nil? && !e.include?('=')\n              e.split('')[1..-1].each { |s|\n                raw_opts.push([\"-#{s}\"])\n              }\n              next\n            end\n\n            # 处理带‘=’的传值参数\n            loc = (e =~ /=/)\n            if loc\n              temp.unshift(e[(loc + 1)..-1])\n              temp.unshift(e[0..loc - 1])\n            elsif e.length != 0\n              temp.push(e)\n            end\n\n          elsif e.length != 0\n            temp.push(e)\n          end\n\n          if idx == argv.length - 1\n            raw_opts.push(temp)\n          end\n        }\n        ARGV.new(cmd, pure_opts, absolute_cmd, raw_opts)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/argv.rb",
    "content": "#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    require 'm-git/argv/parser'\n\n    # 指令名，如：\"mgit checkout -b branch_name\"的\"checkout\"\n    attr_reader :cmd\n\n    # 所有参数，如：\"mgit checkout -b branch_name\"的\"checkout -b branch_name\"\n    attr_reader :pure_opts\n\n    # 完整指令，如：\"mgit checkout -b branch_name\"\n    attr_reader :absolute_cmd\n\n    # 本次传入的mgit指令中自定义的部分，如：\"mgit checkout -b branch_name --mrepo boxapp BBAAccount --command test\"的\"[[--mrepo boxapp BBAAccount],[--command test]]\"\n    attr_reader :raw_opts\n\n    # 本次传入的mgit指令中git透传的部分，如：\"mgit checkout -b branch_name --mrepo boxapp BBAAccount\"的\"[[-b branch_name]]\"\n    # has define method git_opts\n\n    # 所有已注册的参数列表\n    attr_reader :opt_list\n\n    def initialize(cmd, pure_opts, absolute_cmd, raw_opts)\n      @cmd = cmd\n      @pure_opts = pure_opts\n      @absolute_cmd = absolute_cmd\n      @raw_opts = raw_opts\n      @git_opts = []\n    end\n\n    def register_opts(opts)\n      return if opts.nil?\n      @opt_list = OptList.new(opts)\n    end\n\n    # 注册解析指令\n    def resolve!\n      @raw_opts.each { |raw_opt|\n        next if @opt_list.did_register_opt?(raw_opt.first)\n        @git_opts.push(raw_opt)\n      }\n\n      @raw_opts -= @git_opts\n\n      __resolve_git_opts\n      __resolve_raw_opts\n    end\n\n    # 更新指令值\n    # @!attribute [Array / String / true / false] value\n    #\n    def update_opt(key, value, priority:nil, info:nil)\n      return unless @opt_list.did_register_opt?(key)\n      opt = @opt_list.registered_opt(key)\n      case opt.value_type\n      when Array\n        opt.value = Array(value)\n      when String\n        opt.value = value.is_a?(Array) ? value.first.to_s : value.to_s\n      else # boolean\n        opt.value = value\n      end\n\n      opt.priority = priority if !priority.nil?\n      opt.info = info if info.is_a?(String)\n    end\n\n    # 获取某个option\n    def opt(key)\n      @opt_list.opt(key)\n    end\n\n    # 获取某个option的描述信息\n    def info(key)\n      return '' unless @opt_list.did_register_opt?(key)\n      @opt_list.registered_opt(key)&.info\n    end\n\n    # 获取原生git指令（非自定义的指令）\n    #\n    # @param raw [Boolean] default: true，true：用空格拼接成一个字符串返回。false：直接返回数组，如‘-k k1 k2’ -> 【'-k','k1','k2'】\n    #\n    # @return [Type] description_of_returned_object\n    #\n    def git_opts(raw: true)\n      return @git_opts unless raw\n      opts = []\n      @git_opts.each { |e_arr|\n        opts += e_arr\n      }\n      opts.join(' ')\n    end\n\n    # 遍历本次调用中传入过值（或有默认值）的选项，未传入值且无默认值则不遍历\n    def enumerate_valid_opts\n      @opt_list.opts_ordered_by_priority.each { |opt|\n        next unless @opt_list.did_set_opt?(opt.key)\n        yield(opt) if block_given?\n      }\n    end\n\n    # 判断一个字符串是否是option（以'--'或'-'开头）\n    def is_option?(opt_str)\n      (opt_str =~ /-/) == 0 || (opt_str =~ /--/) == 0\n    end\n\n    # 输出本次指令的具体值信息，调试时使用\n    def show_detail\n      @opt_list.opts.each { |opt|\n        puts '======='\n        puts \"key:#{opt.key}\"\n        puts \"value:#{opt.value}\"\n        puts \"info:#{opt.info}\"\n        puts \"\\n\"\n      }\n    end\n\n    # 输出参数说明信息\n    def show_info\n      @opt_list.opts.each { |opt|\n        short_key = \"#{opt.short_key}, \" if !opt.short_key.nil?\n        puts \"\\n\"\n        puts Output.blue_message(\"[#{short_key}#{opt.key}]\")\n        puts \"#{opt.info}\"\n      }\n    end\n\n    private\n\n    def __resolve_git_opts\n      # 统一将值用双引号括起来，避免空格和特殊字符等引起错误\n      @git_opts.each { |e_arr|\n        next unless is_option?(e_arr.first)\n        e_arr.map!.with_index { |e, i|\n          # 如果是 -- / - 开始的option，则增加\"\" 避免特殊字符的错误，eg： --yoo=ajsdaf  => --yoo=\"ajsdaf\"\n          # 如果是 repo1 repo2 这样的option，则不作处理，eg： yoo ajsdaf => yoo ajsdaf\n          e = \"\\\"#{e}\\\"\" if !is_option?(e) && i > 0\n          e\n        }\n      }\n    end\n\n    def __resolve_raw_opts\n      @raw_opts.each { |raw_opt|\n        key = raw_opt.first\n        opt = @opt_list.registered_opt(key)\n\n        case opt.value_type\n        when :boolean\n          raise \"参数#{key}格式错误，禁止传入参数值！(用法：#{key})\" if raw_opt.count != 1\n          opt.value = true\n        when :string\n          raise \"参数#{key}格式错误，只能传入一个参数值！(用法：#{key} xxx)\" if raw_opt.count != 2\n          opt.value = raw_opt.last.to_s\n        when :array\n          raise \"参数#{key}格式错误，至少传入一个参数值！(用法：#{key} xxx xxx ...)\" if raw_opt.count < 2\n          opt.value = raw_opt[1..-1]\n        end\n      }\n    end\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/base_command.rb",
    "content": "#coding=utf-8\n#\n\nrequire 'm-git/command_manager'\n\nmodule MGit\n  class BaseCommand\n\n    def self.inherited(sub_klass)\n      CommandManager.register_command(sub_klass.cmd, sub_klass)\n    end\n\n    # 当前命令行的命令比如checkout / status /..\n    def self.cmd\n      name.split('::').last.downcase\n    end\n\n    # 引入所有自定义指令\n    Dir[File.join(__dir__, 'command', '*.rb')].each { |cmd|\n      require cmd\n    }\n\n    HIGH_PRIORITY_OPT_LIST = {\n      :help         =>  '--help',\n      :help_s       =>  '-h',\n      :auto_exec    => '--auto-exec'\n    }.freeze\n\n    # 注意使用时跟指令自身设置的参数对比，避免冲突\n    SELECTABLE_OPT_LIST = {\n      :mrepo            =>  '--mrepo',\n      :mrepo_s          =>  '-m',\n      :exclude_mrepo    =>  '--el-mrepo',\n      :exclude_mrepo_s  =>  '-e',\n      :include_lock     =>  '--include-lock',\n      :include_lock_s   =>  '-i',\n      :continue         =>  '--continue',\n      :abort            =>  '--abort',\n    }.freeze\n\n    # 初始化\n    #\n    # @param argv [ARGV::Opt] 输入参数对象\n    #\n    def initialize(argv)\n      # 指令解析\n      setup_argv(argv)\n      process_highest_priority_option(argv)\n      validate(argv)\n      @argv = argv\n    end\n\n    #--- 禁止覆写 ---\n    # 执行主方法\n    def run\n      begin\n        __config_repo_filter\n        pre_exec\n        execute(@argv)\n        post_exec\n      rescue SystemExit, Interrupt\n        did_interrupt\n      end\n    end\n    #---------------\n    # Workspace Bridge\n    def all_repos(except_config:false)\n      Workspace.all_repos(except_config: except_config)\n    end\n\n    def locked_repos\n      Workspace.locked_repos\n    end\n\n    def exec_light_repos\n      Workspace.exec_light_repos\n    end\n\n    def generate_config_repo\n      Workspace.generate_config_repo\n    end\n    # -----------\n    private\n    def __config_repo_filter\n      cfg = Workspace.filter_config\n      cfg.include_lock = !@argv.opt(SELECTABLE_OPT_LIST[:include_lock]).nil? || include_lock_by_default\n      cfg.select_repos = @argv.opt(SELECTABLE_OPT_LIST[:mrepo])\n      cfg.exclude_repos = @argv.opt(SELECTABLE_OPT_LIST[:exclude_mrepo])\n      cfg.auto_exec = @argv.opt_list.did_set_opt?(HIGH_PRIORITY_OPT_LIST[:auto_exec])\n    end\n    #--- 基类调用，禁止覆写 ---\n    # 配置参数对象\n    def setup_argv(argv)\n      argv.register_opts(options)\n      argv.resolve!\n\n      argv.opt_list.opts.each do |opt|\n        next if opt.empty?\n        revise_option_value(opt)\n      end\n    end\n\n    # 处理最高优指令\n    def process_highest_priority_option(argv)\n      if argv.opt_list.did_set_opt?(HIGH_PRIORITY_OPT_LIST[:help])\n        usage(argv)\n        exit\n      end\n    end\n\n    #-------------------------------------------------------\n\n    #--- 可选覆写 ---\n    # 此处有pre hook，覆写时需要先调用super, 特殊指令除外\n    def pre_exec\n      # 开始计时\n      MGit::DurationRecorder.start\n      # 配置根目录\n      Workspace.setup_multi_repo_root\n\n      # 配置log\n      MGit::Loger.config(Workspace.root)\n      MGit::Loger.info(\"~~~ #{@argv.absolute_cmd} ~~~\")\n\n      # 执行前置hook\n      HooksManager.execute_mgit_pre_hook(@argv.cmd, @argv.pure_opts)\n\n      # 解析配置文件\n      Workspace.setup_config\n\n      # 校验实体仓库\n      Workspace.setup_all_repos\n    end\n\n    # 此处有post hook，覆写时需要最后调用super, 特殊指令除外\n    def post_exec\n      # 执行后置hook\n      HooksManager.execute_mgit_post_hook(@argv.cmd, @argv.pure_opts, Workspace.exec_light_repos)\n      # 打点结束\n      duration = MGit::DurationRecorder.end\n      MGit::Loger.info(\"~~~ #{@argv.absolute_cmd}, 耗时：#{duration} s ~~~\")\n    end\n\n    # 【子类按需覆写修改返回值】返回true表示可以支持自动执行\n    def enable_auto_execution\n      false\n    end\n\n    # 【子类按需覆写修改返回值】返回true表示可以支持\"--mrepo\"等可选选项\n    def enable_repo_selection\n      false\n    end\n\n    # 【子类按需覆写修改返回值】是否使默认添加选项（如‘--help’）和可选添加的选项（如‘--mrepo’）支持短指令\n    def enable_short_basic_option\n      false\n    end\n\n    # 【子类按需覆写修改返回值】是否添加操作lock仓库的选项（如‘--include-lock’）\n    def enable_lock_operation\n      false\n    end\n\n    # 【子类按需覆写修改返回值】是否自动将lock仓库加入到操作集中\n    def include_lock_by_default\n      false\n    end\n\n    # 【子类按需覆写修改返回值】是否添加‘--continue’参数，要在指令中自行控制中间态操作\n    def enable_continue_operation\n      false\n    end\n\n    # 【子类按需覆写修改返回值】是否添加‘--abort’参数\n    def enable_abort_operation\n      false\n    end\n\n    # 【子类按需覆写修改实现】按下ctrl+c后调用\n    def did_interrupt\n    end\n\n    # 可覆写该方法，返回该指令的描述\n    def self.description\n    end\n\n    # 可覆写该方法，返回该指令的用法\n    def self.usage\n    end\n    #---------------\n\n    #--- 强制覆写 ---\n    # 子类指令执行主方法\n    def execute(argv)\n      Foundation.help!(\"请覆写父类方法: \\\"execute(argv)\\\"\")\n    end\n    #---------------\n\n    #--- 如果要接管某个指令（即为它添加自定义参数） ---\n    # --- 强制覆写（若该指令不带任何自定义参数，则无须覆写） ---\n    # 注册选项，覆写时注意返回\"[...].concat(super)\"\n    def options\n      opts = [\n          ARGV::Opt.new(HIGH_PRIORITY_OPT_LIST[:help],\n                        short_key:(HIGH_PRIORITY_OPT_LIST[:help_s] if enable_short_basic_option),\n                        info:\"显示帮助。\",\n                        type: :boolean)\n      ]\n\n      opts.push(\n          ARGV::Opt.new(HIGH_PRIORITY_OPT_LIST[:auto_exec],\n                        info:'指定该参数会跳过所有交互场景，并自动选择需要的操作执行。该参数主要用于脚本调用mgit进行自动化操作，日常RD开发不应当使用。',\n                        type: :boolean)\n      ) if enable_auto_execution\n\n      opts.push(\n          ARGV::Opt.new(SELECTABLE_OPT_LIST[:mrepo],\n          short_key:(SELECTABLE_OPT_LIST[:mrepo_s] if enable_short_basic_option),\n          info:'指定需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：\"--mrepo boxapp BBAAccount\"，若缺省则对所有仓库执行指令。'),\n          ARGV::Opt.new(SELECTABLE_OPT_LIST[:exclude_mrepo],\n          short_key:(SELECTABLE_OPT_LIST[:exclude_mrepo_s] if enable_short_basic_option),\n          info:'指定不需要执行该指令的仓库，可指定一个或多个，空格隔开，大小写均可，如：\"--el-mrepo boxapp BBAAccount\"，若缺省则对所有仓库执行指令。与\"--mrepo\"同时指定时无效。')\n      ) if enable_repo_selection\n\n      opts.push(\n          ARGV::Opt.new(SELECTABLE_OPT_LIST[:include_lock], info:'指定该参数意味着同时也操作lock仓库。',\n                        type: :boolean)\n      ) if enable_lock_operation\n\n      opts.push(\n          ARGV::Opt.new(SELECTABLE_OPT_LIST[:continue],\n                        info:'MGit自定义参数，仅在操作多仓库过程中出现问题停止，执行状态进入中间态后可用。该参数只能单独使用，解决问题后可执行\"mgit <cmd> --continue\"继续操作其余仓库。',\n                        type: :boolean)\n      ) if enable_continue_operation\n\n      opts.push(\n          ARGV::Opt.new(SELECTABLE_OPT_LIST[:abort],\n                        info:'MGit自定义参数，仅在操作多仓库过程中出现问题停止，执行状态进入中间态后可用。该参数用于清除操作中间态，且只能单独使用：\"mgit <cmd> --abort\"。',\n                        type: :boolean)\n      ) if enable_abort_operation\n\n      opts\n    end\n\n    # 解析参数并更新参数值到列表中\n    def revise_option_value(opt)\n    end\n\n    # --- 可选覆写（若该指令不带任何自定义参数，则无须覆写） ---\n    # 判断是否有必须参数漏传或数据格式不正确\n    def validate(argv)\n    end\n\n    # mgit是否输入--continue希望继续上次操作\n    def mgit_try_to_continue?\n      @argv.opt_list.did_set_opt?(SELECTABLE_OPT_LIST[:continue])\n    end\n\n    def mgit_try_to_abort?\n      @argv.opt_list.did_set_opt?(SELECTABLE_OPT_LIST[:abort])\n    end\n\n    # 显示指令使用信息\n    def usage(argv)\n      puts \"#{Output.blue_message(\"[指令说明]\")}\\n#{self.class.description}\"\n      puts \"\\n#{Output.blue_message(\"[指令格式]\")}\\n#{self.class.usage}\"\n      argv.show_info\n    end\n\n  end\nend\n"
  },
  {
    "path": "lib/m-git/command/add.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope [command] add\n  # follow git add\n  # eg: mgit add .\n  #\n  class Add < BaseCommand\n\n    # @overload\n    #\n    def execute(argv)\n      Workspace.check_branch_consistency\n      Output.puts_start_cmd\n      _, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, all_repos)\n      Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0\n    end\n\n    # @overload\n    # @return [Boolean]\n    #\n    def enable_repo_selection\n      true\n    end\n\n    # @overload\n    #\n    def self.description\n      \"将文件改动加入暂存区。\"\n    end\n\n    # @overload\n    #\n    def self.usage\n      \"mgit add [<git-add-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/branch.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope [command] branch\n  # follow git branch\n  # eg: mgit branch --compact\n  #\n  class Branch < BaseCommand\n\n    OPT_LIST = {\n      :compact    =>  '--compact',\n    }.freeze\n\n    def options\n      [\n          ARGV::Opt.new(OPT_LIST[:compact], info:\"以归类的方式显示所有仓库的当前分支。\", type: :boolean)\n      ].concat(super)\n    end\n\n    def execute(argv)\n      Output.puts_start_cmd\n\n      if argv.opt(OPT_LIST[:compact])\n        show_compact_branches(argv)\n        return\n      end\n\n      # 无自定义参数则透传\n      extcute_as_common(argv)\n    end\n\n    # 常规执行\n    def extcute_as_common(argv)\n      error_repos = {}\n      all_repos.sort_by { |repo| repo.name }.each { |repo|\n        success, output = repo.execute_git_cmd(argv.cmd, argv.git_opts)\n        if success && output.length > 0\n          puts Output.generate_title_block(repo.name) {\n            output\n          } + \"\\n\"\n        elsif !success\n          error_repos[repo.name] = output\n        end\n      }\n      if error_repos.length > 0\n        Workspace.show_error(error_repos)\n      else\n        Output.puts_succeed_cmd(argv.absolute_cmd)\n      end\n    end\n\n    # 以紧凑模式执行\n    def show_compact_branches(argv)\n      show_branches_for_repos(all_repos, false)\n      show_branches_for_repos(locked_repos, true)\n      Output.puts_succeed_cmd(argv.absolute_cmd)\n    end\n\n    # 紧凑地显示一组仓库分支\n    def show_branches_for_repos(repos, locked)\n      return if repos.nil?\n\n      list = {}\n      repos.sort_by { |repo| repo.name }.each { |repo|\n        branch = repo.status_checker.current_branch(strict_mode:false)\n        branch = 'HEAD游离，不在任何分支上！' if branch.nil?\n        list[branch] = [] if list[branch].nil?\n        list[branch].push(repo.name)\n      }\n      list.each { |branch, repo_names|\n        Output.puts_remind_block(repo_names, \"以上仓库的当前分支：#{branch}#{' [锁定]' if locked}\")\n        puts \"\\n\"\n      }\n    end\n\n    def enable_repo_selection\n      true\n    end\n\n    def self.description\n      \"创建、显示、删除分支。\"\n    end\n\n    def self.usage\n      \"mgit branch [<git-branch-option>|--compact] [(--mrepo|--el-mrepo) <repo>...] [--help]\"\n    end\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/checkout.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope [command] checkout\n  # follow git checkout\n  # eg: mgit checkout master\n  #\n  class Checkout < BaseCommand\n\n    def execute(argv)\n      Output.puts_start_cmd\n\n      # 优先checkout配置仓库\n      config_repo = generate_config_repo\n      checkout_config_repo(argv.cmd, argv.git_opts, config_repo)\n\n      do_repos = []\n      dirty_repos = []\n\n      Output.puts_processing_message(\"检查各仓库状态...\")\n      Workspace.serial_enumerate_with_progress(all_repos) { |repo|\n        if !config_repo.nil? && repo.name == config_repo.name\n          next\n        elsif repo.status_checker.status != Repo::Status::GIT_REPO_STATUS[:dirty]\n          do_repos.push(repo)\n        else\n          dirty_repos.push(repo)\n        end\n      }\n      Output.puts_success_message(\"检查完成！\\n\")\n\n      if dirty_repos.length > 0\n        remind_repos = []\n        remind_repos.push(['有本地改动', dirty_repos.map { |e| e.name }]) if dirty_repos.length > 0\n        Output.interact_with_multi_selection_combined_repos(remind_repos, \"以上仓库状态异常\", ['a: 跳过并继续', 'b: 强制执行', 'c: 终止']) { |input|\n          if input == 'b'\n            do_repos += dirty_repos\n            do_repos.uniq! { |repo| repo.name }\n          elsif input == 'c' || input != 'a'\n            Output.puts_cancel_message\n            return\n          end\n        }\n      end\n\n      if do_repos.length == 0\n        if config_repo.nil?\n          Output.puts_nothing_to_do_cmd\n        else\n          Output.puts_succeed_cmd(argv.absolute_cmd)\n        end\n      else\n        Output.puts_processing_message(\"开始checkout子仓库...\")\n        _, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, do_repos)\n        Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0\n      end\n    end\n\n    def checkout_config_repo(cmd, opts, repo)\n      return if repo.nil?\n      if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]\n        remind_config_repo_fail(\"主仓库\\\"#{repo.name}\\\"有改动，无法执行！\")\n      else\n        Output.puts_processing_message(\"开始checkout主仓库...\")\n        success, output = repo.execute_git_cmd(cmd, opts)\n        if !success\n          remind_config_repo_fail(\"主仓库\\\"#{repo.name}\\\"执行\\\"#{cmd}\\\"失败：\\n#{output}\")\n        else\n          Output.puts_success_message(\"主仓库checkout成功！\\n\")\n        end\n\n        # 刷新配置表\n        Workspace.update_config { |missing_repos|\n          if missing_repos.length > 0\n            all_repos.concat(missing_repos)\n            # missing_repos包含新下载的和当前分支已有的新仓库，其中已有仓库包含在@all_repos内，需要去重\n            all_repos.uniq! { |repo| repo.name }\n          end\n        }\n\n      end\n    end\n\n    def remind_config_repo_fail(msg)\n      Output.puts_fail_message(msg)\n      return if Output.continue_with_user_remind?(\"是否继续操作其余仓库？\")\n      Output.puts_cancel_message\n      exit\n    end\n\n    def enable_repo_selection\n      true\n    end\n\n    def self.description\n      \"切换分支或恢复工作区改动。\"\n    end\n\n    def self.usage\n      \"mgit checkout [<git-checkout-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/clean.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope [command] clean 清除所有仓库中工作区的变更\n  # follow git combinatorial command\n  # eg: git add . && git reset --hard\n  #\n  class Clean < BaseCommand\n\n    def execute(argv)\n\n      Output.puts_start_cmd\n\n      # 清除中间态\n      OperationProgressManager::PROGRESS_TYPE.each { |type, type_value|\n        if OperationProgressManager.is_in_progress?(Workspace.root, type_value)\n          Output.puts_processing_message(\"清除#{type.to_s}中间态...\")\n          OperationProgressManager.remove_progress(Workspace.root, type_value)\n          Output.puts_success_message(\"清除成功！\")\n        end\n      }\n\n      do_repos = []\n      all_repos.each { |repo|\n        do_repos.push(repo) if repo.status_checker.status != Repo::Status::GIT_REPO_STATUS[:clean]\n      }\n\n      if do_repos.length > 0\n        Workspace.check_branch_consistency\n        Output.puts_processing_message(\"正在将改动加入暂存区...\")\n        _, error_repos1 = Workspace.execute_git_cmd_with_repos('add', '.', do_repos)\n        Output.puts_processing_message(\"正在重置...\")\n        _, error_repos2 = Workspace.execute_git_cmd_with_repos('reset', '--hard', do_repos)\n        Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos1.length + error_repos2.length == 0\n      else\n        Output.puts_success_message(\"所有仓库均无改动，无须执行。\")\n      end\n\n    end\n\n    def validate(argv)\n      Foundation.help!(\"输入非法参数：#{argv.git_opts}。请通过\\\"mgit #{argv.cmd} --help\\\"查看用法。\") if argv.git_opts.length > 0\n    end\n\n    def enable_repo_selection\n      true\n    end\n\n    def enable_short_basic_option\n      true\n    end\n\n    def self.description\n      \"强制清空暂存区和工作区，相当于对指定或所有仓库执行\\\"git add . && git reset --hard\\\"操作\"\n    end\n\n    def self.usage\n      \"mgit clean [(-m|-e) <repo>...] [-h]\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/commit.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope [command] commit\n  # follow git commit\n  # eg: mgit commit -m 'Just for fun'\n  #\n  class Commit < BaseCommand\n\n    private def validate(argv)\n      super\n      # 禁用--amend\n      if argv.git_opts.include?('--amend')\n        Output.puts_fail_message(\"MGit不支持\\\"--amend\\\"操作,请重试。\")\n        Output.puts_cancel_message\n        exit\n      end\n    end\n\n    def execute(argv)\n      Workspace.check_branch_consistency\n\n      Output.puts_start_cmd\n\n      do_repos = []\n      remind_repos = []\n      do_nothing_repos = []\n\n      Output.puts_processing_message(\"检查各仓库状态...\")\n      Workspace.serial_enumerate_with_progress(all_repos) { |repo|\n        if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]\n          index_dirty_mask = Repo::Status::GIT_REPO_STATUS_DIRTY_ZONE[:index]\n          # 仅在暂存区有未提交的改动时执行\n          if repo.status_checker.dirty_zone & index_dirty_mask == index_dirty_mask\n            do_repos.push(repo)\n          else\n            remind_repos.push(repo.name)\n          end\n        else\n          do_nothing_repos.push(repo.name)\n        end\n      }\n      Output.puts_success_message(\"检查完成！\\n\")\n\n      if remind_repos.length > 0 && !Output.continue_with_interact_repos?(remind_repos, \"以上仓库暂存区无可提交内容，仅存在工作区改动或未跟踪文件，若需要提交这些改动请先add到暂存区。是否跳过并继续？\")\n        Output.puts_cancel_message\n        return\n      end\n\n      if do_repos.length != 0\n        if argv.git_opts.include?('-m ') || !Output.continue_with_user_remind?(\"未添加\\\"-m\\\"参数，请使用如[ mgit commit -m \\\"my log\\\" ]的形式提交。是否取消执行并重新输入（若确实有意执行该指令请忽略本提示）？\")\n\n          # commit前调用hook\n          HooksManager.execute_mgit_pre_exec_hook(argv.cmd, argv.pure_opts, do_repos.map { |e| e.config })\n\n          msg = \"，另有#{do_nothing_repos.length}个仓库暂存区无待提交内容，无须执行\" if do_nothing_repos.length > 0\n          Output.puts_remind_block(do_repos.map { |repo| repo.name }, \"开始commit以上仓库#{msg}...\")\n          _, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, do_repos)\n          Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0\n        else\n          Output.puts_cancel_message\n          return\n        end\n      else\n        Output.puts_remind_message(\"所有仓库均无改动，无须执行！\")\n      end\n    end\n\n    def enable_repo_selection\n      true\n    end\n\n    def self.description\n      \"将修改记录到版本库。\"\n    end\n\n    def self.usage\n      \"mgit commit [<git-commit-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/config.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope [command] config 配置 .mgit/config.yml 文件信息\n  #\n  # eg: mgit config -s key 'value'\n  #\n  class Config < BaseCommand\n\n    OPT_LIST = {\n      :create_local          =>  '--create-local',\n      :create_local_s        =>  '-c',\n      :update_manifest       =>  '--update-manifest',\n      :update_manifest_s     =>  '-m',\n      :update_local          =>  '--update-local',\n      :update_local_s        =>  '-u',\n      :list                  =>  '--list',\n      :list_s                =>  '-l',\n      :set                   =>  '--set',\n      :set_s                 =>  '-s'\n    }.freeze\n\n    def options\n      return [\n          ARGV::Opt.new(OPT_LIST[:update_manifest],\n                        short_key:OPT_LIST[:update_manifest_s],\n                        info:\"该指令用于更新mgit所使用的配置文件，如：\\\"mgit config -m <new_path>/manifest.json\\\"。\",\n                        type: :string),\n          ARGV::Opt.new(OPT_LIST[:update_local],\n                        short_key:OPT_LIST[:update_local_s],\n                        info:\"该指令用于更新mgit所使用的本地配置文件，如：\\\"mgit config -u <new_path>/local_manifest.json\\\"。\",\n                        type: :string),\n          ARGV::Opt.new(OPT_LIST[:create_local],\n                        short_key:OPT_LIST[:create_local_s],\n                        info:\"在指定目录下创建本地配置文件，若目录不存在则自动创建。如执行：\\\"mgit config -c /a/b/c\\\"，则生成本地配置文件：\\\"/a/b/c/local_manifest.json\\\"。如果未传入值，如：\\\"mgit config -c\\\"，那么若配置仓库存在的话，会在配置仓库中创建本地配置文件。\",\n                        type: :string),\n          ARGV::Opt.new(OPT_LIST[:list],\n                        short_key:OPT_LIST[:list_s],\n                        info:\"列出当前MGit所有配置，无参数，如：\\\"mgit config -l\\\"。\",\n                        type: :boolean),\n          ARGV::Opt.new(OPT_LIST[:set],\n                        short_key:OPT_LIST[:set_s],\n                        info:\"对MGit进行配置，遵守格式：\\\"mgit config -s <key> <value>\\\"，如：\\\"mgit config -s maxconcurrentcount 5\\\"。\")\n      ].concat(super)\n    end\n\n    def validate(argv)\n      Foundation.help!(\"输入非法参数：#{argv.git_opts}。请通过\\\"mgit #{argv.cmd} --help\\\"查看用法。\") if argv.git_opts.length > 0\n\n      if set_kv = argv.opt(OPT_LIST[:set])\n        Foundation.help!(\"参数#{OPT_LIST[:set]}格式错误，只需传入key和value两个值！\") if set_kv.value.count != 2\n      end\n    end\n\n    # --- 覆写前后hook，不需要预设操作 ---\n    def pre_exec\n      # 开始计时\n      MGit::DurationRecorder.start\n      Workspace.setup_multi_repo_root\n      # 配置log\n      MGit::Loger.config(Workspace.root)\n      MGit::Loger.info(\"~~~ #{@argv.absolute_cmd} ~~~\")\n    end\n\n    def post_exec\n      # 打点结束\n      duration = MGit::DurationRecorder.end\n      MGit::Loger.info(\"~~~ #{@argv.absolute_cmd}, 耗时：#{duration} s ~~~\")\n    end\n    # --------------------------------\n\n    def execute(argv)\n      argv.enumerate_valid_opts { |opt|\n        if opt.key == OPT_LIST[:update_manifest]\n          update_mgit_config(opt.value)\n          return\n        elsif opt.key == OPT_LIST[:update_local]\n          update_local_config(opt.value)\n          return\n        elsif opt.key == OPT_LIST[:create_local]\n          dir = opt.value\n          if opt.value.is_a?(TrueClass)\n            Workspace.setup_config\n            if Workspace.config.config_repo.nil?\n              Foundation.help!(\"未找到配置仓库，请为参数\\\"--create-local\\\"或\\\"-c\\\"指定一个具体文件夹目录并重试！\")\n            else\n              dir = Workspace.config.config_repo.abs_dest(Workspace.root)\n            end\n          end\n          create_local_config(dir)\n          return\n        elsif opt.key == OPT_LIST[:list]\n          dump_config\n        elsif opt.key == OPT_LIST[:set]\n          set_config(opt.value)\n        end\n      }\n    end\n\n    # 更新配置表软链接\n    def update_mgit_config(config_path)\n      config = Manifest.parse(Utils.expand_path(config_path))\n      Utils.execute_under_dir(\"#{File.join(Workspace.root, Constants::PROJECT_DIR[:source_config])}\") {\n        mgit_managed_config_link_path = File.join(Dir.pwd, Constants::CONFIG_FILE_NAME[:manifest])\n        mgit_managed_config_cache_path = File.join(Dir.pwd, Constants::CONFIG_FILE_NAME[:manifest_cache])\n\n        # 在.mgit/source-config文件夹下创建原始配置文件的软连接\n        if config.path != mgit_managed_config_link_path\n          Utils.link(config.path, mgit_managed_config_link_path)\n        end\n\n        # 将配置缓存移动到.mgit/source-config文件夹下\n        if config.cache_path != mgit_managed_config_cache_path\n          FileUtils.rm_f(mgit_managed_config_cache_path) if File.exist?(mgit_managed_config_cache_path)\n          FileUtils.mv(config.cache_path, Dir.pwd)\n        end\n\n        Output.puts_success_message(\"配置文件更新完毕！\")\n      }\n    end\n\n    # 更新本地配置表软链接\n    def update_local_config(config_path)\n      config_path = Utils.expand_path(config_path)\n      Utils.execute_under_dir(\"#{File.join(Workspace.root, Constants::PROJECT_DIR[:source_config])}\") {\n        mgit_managed_local_config_link_path = File.join(Dir.pwd, Constants::CONFIG_FILE_NAME[:local_manifest])\n        # 在.mgit/source-config文件夹下创建原始本地配置文件的软连接\n        if config_path != mgit_managed_local_config_link_path\n          Utils.link(config_path, mgit_managed_local_config_link_path)\n        end\n\n        Output.puts_success_message(\"本地配置文件更新完毕！\")\n      }\n    end\n\n    # 新建本地配置表软链接\n    def create_local_config(dir)\n      path = Utils.expand_path(File.join(dir, Constants::CONFIG_FILE_NAME[:local_manifest]))\n      if File.exist?(path) && !Output.continue_with_user_remind?(\"本地配置文件\\\"#{path}\\\"已经存在，是否覆盖？\")\n        Output.puts_cancel_message\n        return\n      end\n\n      FileUtils.mkdir_p(dir)\n      file = File.new(path, 'w')\n      if !file.nil?\n        file.write(Template.default_template)\n        file.close\n      end\n\n      Utils.link(path, File.join(Workspace.root, Constants::PROJECT_DIR[:source_config], Constants::CONFIG_FILE_NAME[:local_manifest]))\n      Output.puts_success_message(\"本地配置文件生成完毕：#{path}\")\n    end\n\n    # 列出所有配置\n    def dump_config\n      begin\n        MGitConfig.dump_config(Workspace.root)\n      rescue Error => e\n        Foundation.help!(e.msg)\n      end\n    end\n\n    # 设置配置\n    def set_config(key_value_arr)\n      key = key_value_arr.first\n      value = key_value_arr.last\n      begin\n        MGitConfig.update(Workspace.root) { |config|\n          if MGitConfig::CONFIG_KEY.keys.include?(key.to_sym)\n            valid_value = MGitConfig.to_suitable_value_for_key(Workspace.root, key, value)\n            if !valid_value.nil?\n              config[key] = valid_value\n            else\n              type = MGitConfig::CONFIG_KEY[key.to_sym][:type]\n              Foundation.help!(\"#{value}不匹配类型：#{type}，请重试。\")\n            end\n          else\n            Foundation.help!(\"非法key值：#{key}。使用mgit config -l查看所有可配置字段。\")\n          end\n        }\n        Output.puts_success_message(\"配置成功！\")\n      rescue Error => e\n        Foundation.help!(e.msg)\n      end\n    end\n\n    # 允许使用短命令\n    def enable_short_basic_option\n      true\n    end\n\n    def self.description\n      \"用于更新多仓库配置信息。\"\n    end\n\n    def self.usage\n      \"mgit config [-s <config_key> <config_value>] [-l]\\nmgit config [(-m|-u) <path_to_manifest> | -c <dir_contains_local>]\\nmgit config [-h]\"\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/command/delete.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope [command] delete 删除某个仓库的`所有`文件，包括工作区、暂存区和版本库\n  #\n  # eg: mgit delete subA\n  #\n  class Delete < BaseCommand\n\n    # --- 覆写前后hook，不需要预设操作 ---\n    def pre_exec\n      MGit::DurationRecorder.start\n      Workspace.setup_multi_repo_root\n      # 配置log\n      MGit::Loger.config(Workspace.root)\n      MGit::Loger.info(\"~~~ #{@argv.absolute_cmd} ~~~\")\n      Workspace.setup_config\n    end\n\n    def post_exec\n      # 打点结束\n      duration = MGit::DurationRecorder.end\n      MGit::Loger.info(\"~~~ #{@argv.absolute_cmd}, 耗时：#{duration} s ~~~\")\n    end\n    # --------------------------------\n\n    def execute(argv)\n      delete_repo_names = parse_repo_name(argv)\n      include_central = Workspace.config.light_repos.find do |e|\n        delete_repo_names.include?(e.name.downcase) && e.is_config_repo\n      end\n      Foundation.help!(\"禁止删除配置仓库=> #{include_central.name}\") if include_central\n\n      Output.puts_start_cmd\n      delete_light_repos = Workspace.config.light_repos.select { |e| delete_repo_names.include?(e.name.downcase) }\n      extra_repo_names = delete_repo_names - delete_light_repos.map { |e| e.name.downcase}\n      if delete_light_repos.length > 0\n        error_repos = {}\n        delete_light_repos.each { |light_repo|\n          begin\n            git_dir = light_repo.git_store_dir(Workspace.root)\n            repo_dir = light_repo.abs_dest(Workspace.root)\n            if !Dir.exist?(git_dir) && !Dir.exist?(repo_dir)\n              Output.puts_remind_message(\"#{light_repo.name}本地不存在，已跳过。\")\n            end\n\n            # 删除git实体\n            if Dir.exist?(git_dir)\n              Output.puts_processing_message(\"删除仓库#{light_repo.name}的.git实体...\")\n              FileUtils.remove_dir(git_dir, true)\n            end\n\n            # 删除工作区文件\n            if Dir.exist?(repo_dir)\n              Output.puts_processing_message(\"删除仓库#{light_repo.name}工作区文件...\")\n              FileUtils.remove_dir(repo_dir, true)\n            end\n          rescue => e\n            error_repos[light_repo.name] = e.message\n          end\n        }\n        if error_repos.length > 0\n          Workspace.show_error(error_repos)\n        else\n          Output.puts_succeed_cmd(argv.absolute_cmd)\n        end\n      end\n\n      if extra_repo_names.length > 0\n        Output.puts_fail_block(extra_repo_names, \"以上仓库配置表中未定义，请重试！\")\n      end\n    end\n\n    def parse_repo_name(argv)\n      return if argv.git_opts.nil?\n\n      repos = argv.git_opts.split(' ')\n      extra_opts = repos.select { |e| argv.is_option?(e) }\n      Foundation.help!(\"输入非法参数：#{extra_opts.join('，')}。请通过\\\"mgit #{argv.cmd} --help\\\"查看用法。\") if extra_opts.length > 0\n      Foundation.help!(\"未输入查询仓库名！请使用这种形式查询：mgit info repo1 repo2 ...\") if repos.length == 0\n      repos.map { |e| e.downcase }\n    end\n\n    def enable_short_basic_option\n      true\n    end\n\n    def self.description\n      \"删除指定单个或多个仓库（包含被管理的.git文件和工程文件以及跟该.git关联的所有缓存）。\"\n    end\n\n    def self.usage\n      \"mgit delete <repo1> <repo2>... [-h]\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/fetch.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope [command] fetch\n  # follow git fetch\n  # eg: mgit fetch\n  #\n  class Fetch < BaseCommand\n    def execute(argv)\n      Output.puts_start_cmd\n      _, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, all_repos)\n      if error_repos.length == 0\n        Output.puts_succeed_cmd(argv.absolute_cmd)\n        Timer.show_time_consuming_repos\n      end\n    end\n\n    def enable_repo_selection\n      true\n    end\n\n    def self.description\n      \"与远程仓库同步分支引用和数据对象。\"\n    end\n\n    def self.usage\n      \"mgit fetch [<git-fetch-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]\"\n    end\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/forall.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope [command] forall 对管理的仓库依次执行shell命令\n  #\n  # eg: mgit forall -c 'git status'\n  #\n  class Forall < BaseCommand\n\n    OPT_LIST = {\n      :command      => '--command',\n      :command_s    => '-c',\n      :concurrent   => '--concurrent',\n      :concurrent_s => '-n',\n    }.freeze\n\n    def options\n      return [\n          ARGV::Opt.new(OPT_LIST[:command],\n                        short_key:OPT_LIST[:command_s],\n                        info:'必须参数，指定需要执行的shell命令，如：\"mgit forall -c \\'git status -s\\'\"（注意要带引号）。',\n                        type: :string),\n          ARGV::Opt.new(OPT_LIST[:concurrent],\n                        short_key:OPT_LIST[:concurrent_s],\n                        info:'可选参数，若指定，则shell命令以多线程方式执行。',\n                        type: :boolean)\n      ].concat(super)\n    end\n\n    def validate(argv)\n      Foundation.help!(\"输入非法参数：#{argv.git_opts}。请通过\\\"mgit #{argv.cmd} --help\\\"查看用法。\") if argv.git_opts.length > 0\n      Foundation.help!(\"请输入必须参数--command，示例：mgit forall -c 'git status'\") if argv.opt(OPT_LIST[:command]).nil?\n    end\n\n    def execute(argv)\n      # 校验分支统一\n      Workspace.check_branch_consistency\n\n      Output.puts_start_cmd\n      for_all_cmd = argv.opt(OPT_LIST[:command]).value\n\n      use_concurrent = !argv.opt(OPT_LIST[:concurrent]).nil?\n      if use_concurrent\n        succeed_repos, error_repos = Workspace.execute_common_cmd_with_repos_concurrent(for_all_cmd, all_repos)\n      else\n        succeed_repos, error_repos = Workspace.execute_common_cmd_with_repos(for_all_cmd, all_repos)\n      end\n\n      no_output_repos = []\n      succeed_repos.each { |repo_name, output|\n        if output.length > 0\n          puts Output.generate_title_block(repo_name) { output } + \"\\n\"\n        else\n          no_output_repos.push(repo_name)\n        end\n      }\n\n      Output.puts_remind_block(no_output_repos, \"以上仓库无输出！\") if no_output_repos.length > 0\n      Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0\n    end\n\n    def enable_repo_selection\n      true\n    end\n\n    def enable_short_basic_option\n      true\n    end\n\n    def self.description\n      \"对多仓库批量执行指令。\"\n    end\n\n    def self.usage\n      \"mgit forall -c '<instruction>' [(-m|-e) <repo>...] [-n] [-h]\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/info.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope 指定仓库的信息\n  #\n  class Info < BaseCommand\n\n    def execute(argv)\n      Output.puts_start_cmd\n\n      query_repo_names = parse_repo_name(argv)\n      quere_repos = (all_repos + locked_repos).select { |e| query_repo_names.include?(e.name.downcase) }\n      if quere_repos.length > 0\n        quere_repos.each { |repo|\n          puts Output.generate_title_block(repo.name) {\n            info = []\n            info.push(['仓库位置'.bold, [\"#{repo.path}\"]])\n            info.push(['占用磁盘大小'.bold, [\"#{calculate_size(repo)}\"]])\n            info.push(['创建时间'.bold, [\"#{File.ctime(repo.path)}\"]])\n\n            current_branch = repo.status_checker.current_branch(strict_mode:false)\n            branch_message = repo.status_checker.branch_message\n            info.push(['当前分支'.bold, [\"#{current_branch.nil? ? '无' : current_branch}\"]])\n            info.push(['分支状态'.bold, [\"#{branch_message}\"]])\n\n            info.push(['文件改动'.bold, [\"#{repo.status_checker.status != Repo::Status::GIT_REPO_STATUS[:clean] ? '有本地改动,请用status指令查看细节' : '本地无修改'}\"]])\n            info.push(['Stash状态'.bold, [\"#{check_stash(repo)}\"]])\n            Output.generate_table_combination(info) + \"\\n\\n\"\n          }\n        }\n        Output.puts_succeed_cmd(argv.absolute_cmd)\n      else\n        Output.puts_fail_message(\"未找到与输入仓库名匹配的仓库，请重试！\")\n      end\n\n    end\n\n    def parse_repo_name(argv)\n      return nil if argv.git_opts.nil?\n      repos = argv.git_opts.split(' ')\n      extra_opts = repos.select { |e| argv.is_option?(e) }\n      Foundation.help!(\"输入非法参数：#{extra_opts.join('，')}。请通过\\\"mgit #{argv.cmd} --help\\\"查看用法。\") if extra_opts.length > 0\n      Foundation.help!(\"未输入查询仓库名！请使用这种形式查询：mgit info repo1 repo2 ...\") if repos.length == 0\n      repos.map { |e| e.downcase }\n    end\n\n    def calculate_size(repo)\n      success, output = repo.execute(\"du -sh #{repo.path} | awk '{print $1}'\")\n      return '计算失败'.red unless success\n      output.chomp\n    end\n\n    def check_stash(repo)\n      success, output = repo.execute(\"git -C \\\"#{repo.path}\\\" stash list\")\n      return \"查询失败\".red unless success\n      output.length > 0 ? '有内容' : '无内容'\n    end\n\n    def enable_short_basic_option\n      false\n    end\n\n    def self.description\n      \"输出指定仓库的信息。\"\n    end\n\n    def self.usage\n      \"mgit info <repo>... [-h]\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/init.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope 初始化多仓库命令\n  #\n  class Init < BaseCommand\n\n    OPT_LIST = {\n      :git_source       =>  '--git-source',\n      :git_source_s     =>  '-g',\n      :config_source    =>  '--config-source',\n      :config_source_s  =>  '-f',\n      :branch           =>  '--branch',\n      :branch_s         =>  '-b',\n      :local_config     =>  '--local-config',\n      :local_config_s   =>  '-l',\n      :all              =>  '--all',\n      :all_s            =>  '-a'\n    }.freeze\n\n    def options\n      return [\n          ARGV::Opt.new(OPT_LIST[:git_source],\n                        short_key:OPT_LIST[:git_source_s],\n                        info:'通过包含多仓库配置表的git仓库来初始化，传入远程仓库地址，如：\"mgit init -g https://someone@bitbucket.org/someone\"。不可与\"-f\"，\"--config-source\"同时指定。',\n                        type: :string),\n          ARGV::Opt.new(OPT_LIST[:config_source],\n                        short_key:OPT_LIST[:config_source_s],\n                        info:'通过本地的多仓库配置表来初始化，传入本地配置文件路径，如：\"mgit init -f <local_config_path>/manifest.json\"。不可与\"-g\"，\"--git-source\"同时指定。',\n                        type: :string),\n          ARGV::Opt.new(OPT_LIST[:branch],\n                        short_key:OPT_LIST[:branch_s],\n                        info:'指定配置仓库克隆分支。',\n                        type: :string),\n          ARGV::Opt.new(OPT_LIST[:local_config],\n                        short_key:OPT_LIST[:local_config_s],\n                        info:'指定是否自动生成本地配置文件模版。指定后会在主仓库下生成名为local_manifest.json的本地配置文件，其内容只包含主仓库信息，并指定其余仓库不纳入mgit管理。初始化时指定该参数将只下载主仓库，若同时指定了\"-a\"或\"--all\"，则其余仓库也会被下载。',\n                        type: :boolean),\n          ARGV::Opt.new(OPT_LIST[:all],\n                        short_key:OPT_LIST[:all_s],\n                        info:'指定后会下载所有在配置表中配置了远程地址的仓库，无论该仓库是否被纳入mgit管理（无论是否指定\"mgit_excluded:true\"）。',\n                        type: :boolean)\n      ].concat(super)\n    end\n\n    def validate(argv)\n      Foundation.help!(\"输入非法参数：#{argv.git_opts}\") if argv.git_opts.length > 0\n\n      git_source_opt = argv.opt(OPT_LIST[:git_source])\n      file_source_opt = argv.opt(OPT_LIST[:config_source])\n      if !git_source_opt.nil? && !file_source_opt.nil?\n        Foundation.help!(\"不能同时指定参数\\\"#{OPT_LIST[:git_source]}\\\"和\\\"#{OPT_LIST[:config_source]}!\\\"\")\n      elsif git_source_opt.nil? && file_source_opt.nil?\n        Foundation.help!(\"缺失参数\\\"#{OPT_LIST[:git_source]}\\\"或\\\"#{OPT_LIST[:config_source]}!\\\"\")\n      end\n    end\n\n    # --- 覆写前后hook，不需要预设操作 ---\n    def pre_exec\n      Output.puts_processing_message(\"开始初始化多仓库...\")\n      # 开始计时\n      MGit::DurationRecorder.start\n\n      initial_multi_repo_root\n\n      # 配置log\n      MGit::Loger.config(Workspace.root)\n      MGit::Loger.info(\"~~~ #{@argv.absolute_cmd} ~~~\")\n    end\n\n    def post_exec\n      Output.puts_success_message(\"多仓库初始化成功！\")\n      # 打点结束\n      duration = MGit::DurationRecorder.end\n      MGit::Loger.info(\"~~~ #{@argv.absolute_cmd}, 耗时：#{duration} s ~~~\")\n    end\n\n    # --------------------------------\n\n    def execute(argv)\n      init_dir\n\n      begin\n        git_url_opt = argv.opt(OPT_LIST[:git_source])\n        local_url_opt = argv.opt(OPT_LIST[:config_source])\n        clone_all = argv.opt_list.did_set_opt?(OPT_LIST[:all])\n        if !git_url_opt.nil?\n          git_url = git_url_opt.value\n          branch = argv.opt(OPT_LIST[:branch]).value if argv.opt_list.did_set_opt?(OPT_LIST[:branch])\n          use_local = argv.opt_list.did_set_opt?(OPT_LIST[:local_config])\n          clone_with_git_url(git_url, branch, use_local, clone_all)\n        elsif !local_url_opt.nil?\n          clone_with_local_config(local_url_opt.value, clone_all)\n        end\n      rescue Interrupt => e\n        terminate!(e.message)\n      end\n    end\n\n    def init_dir\n      Constants::PROJECT_DIR.each { |key, relative_dir|\n        abs_dir = File.join(Workspace.root, relative_dir)\n        FileUtils.mkdir_p(abs_dir)\n        if key == :hooks\n          setup_hooks(File.join(abs_dir, Constants::HOOK_NAME[:pre_hook]),\n          File.join(abs_dir, Constants::HOOK_NAME[:post_hook]),\n          File.join(abs_dir, Constants::HOOK_NAME[:manifest_hook]),\n          File.join(abs_dir, Constants::HOOK_NAME[:post_download_hook]))\n        end\n      }\n    end\n\n    def write_content(path, content)\n      file = File.new(path, 'w')\n      if !file.nil?\n        file.write(content)\n        file.close\n      end\n    end\n\n    def setup_hooks(pre_hook_path, post_hook_path, manifest_hook_path, post_download_hook)\n      write_content(pre_hook_path, Template::PRE_CUSTOMIZED_HOOK_TEMPLATE)\n      write_content(post_hook_path, Template::POST_CUSTOMIZED_HOOK_TEMPLATE)\n      write_content(manifest_hook_path, Template::MANIFEST_HOOK_TEMPLATE)\n      write_content(post_download_hook, Template::POST_DOWNLOAD_HOOK_TEMPLATE)\n    end\n\n    def initial_multi_repo_root\n\n      if exist_root = Workspace.multi_repo_root_path\n        Foundation.help!(\"当前已在多仓库目录下，请勿重复初始化！\\n`#{exist_root}`\")\n      end\n\n      @origin_root = Dir.pwd\n      tmp_root = Utils.generate_init_cache_path(@origin_root)\n      FileUtils.mkdir_p(tmp_root)\n      Workspace.setup_multi_repo_root(tmp_root)\n    end\n\n    def setup_local_config(path, config_repo, use_local)\n      if use_local\n        content = Template.local_config_template(config_repo)\n      else\n        content = Template.default_template\n      end\n\n      write_content(path, content)\n      Utils.link(path, File.join(Workspace.root, Constants::PROJECT_DIR[:source_config], Constants::CONFIG_FILE_NAME[:local_manifest]))\n    end\n\n    def clone_with_git_url(git_url, branch, use_local, clone_all)\n      # 先将主仓库clone到mgit root目录下\n      central_repo_temp_path = File.join(Workspace.root, Constants::CENTRAL_REPO)\n      Output.puts_processing_message(\"正在克隆主仓库...\")\n      Utils.execute_shell_cmd(\"git clone -b #{branch.nil? ? 'master' : branch} -- #{git_url} #{central_repo_temp_path}\") { |stdout, stderr, status|\n        if status.success?\n          # 获取主仓库中的配置文件\n          begin\n            config = Manifest.parse(central_repo_temp_path, strict_mode:false)\n          rescue Error => e\n            terminate!(e.msg)\n          end\n          central_light_repo = config.config_repo\n          terminate!(\"配置文件中未找到主仓库配置，请添加后重试！\") if central_light_repo.nil?\n          terminate!(\"配置文件中主仓库url与传入的url不一致, 请处理后重试！\") if git_url != central_light_repo.url\n\n          central_repo_dest_path = central_light_repo.abs_dest(Workspace.root)\n\n          # 如果不是子目录则删除已有\n          if Dir.exist?(central_repo_dest_path) && !central_repo_temp_path.include?(central_repo_dest_path)\n            FileUtils.remove_dir(central_repo_dest_path, true)\n          end\n          FileUtils.mkdir_p(central_repo_dest_path)\n          mv_cmd = \"mv #{central_repo_temp_path + '/{*,.[^.]*}'} #{central_repo_dest_path}\"\n\n          `#{mv_cmd}`\n          FileUtils.rm_rf(central_repo_temp_path)\n\n          # 链接.git实体\n          Utils.link_git(central_repo_dest_path, central_light_repo.git_store_dir(Workspace.root))\n          Output.puts_success_message(\"主仓库克隆完成！\")\n\n          # 链接本地配置文件\n          setup_local_config(File.join(central_repo_dest_path, Constants::CONFIG_FILE_NAME[:local_manifest]), central_light_repo.name, use_local)\n\n          # 由于更新了名字和可能的位置移动，重新解析配置文件\n          begin\n            config = Manifest.parse(central_repo_dest_path, strict_mode:false)\n          rescue Error => e\n            terminate!(e.msg)\n          end\n\n          # clone其余仓库\n          clone_sub_repos(config.repo_list(exclusion:[config.config_repo.name], all:clone_all), branch)\n          finish_init(config)\n        else\n          terminate!(\"主仓库克隆失败，初始化停止，请重试：\\n#{stderr}\")\n        end\n      }\n    end\n\n    def clone_with_local_config(config_path, clone_all)\n      terminate!(\"指定路径\\\"#{config_path}\\\"文件不存在！\") if !File.exist?(config_path)\n\n      begin\n        config = Manifest.parse(Utils.expand_path(config_path), strict_mode:false)\n      rescue Error => e\n        terminate!(e.msg)\n      end\n\n      if !config.config_repo.nil?\n        clone_list = config.repo_list(exclusion: [config.config_repo.name], all:clone_all)\n      else\n        clone_list = config.repo_list(all:clone_all)\n      end\n\n      clone_sub_repos(clone_list, 'master')\n      finish_init(config)\n    end\n\n    def clone_sub_repos(repo_list, default_branch)\n      if repo_list.length != 0\n        Output.puts_processing_message(\"正在克隆子仓库...\")\n        try_repos = repo_list\n        retry_repos = []\n        error_repos = {}\n\n        mutex = Mutex.new\n        mutex_progress = Mutex.new\n\n        task_count = 0\n        total_try_count = 4\n        total_retry_count = total_try_count - 1\n        # 保证失败仓库有3次重新下载的机会\n        total_try_count.times { |try_count|\n          Workspace.concurrent_enumerate(try_repos) { |light_repo|\n            # 如果mainfest中指定了分支，就替换default branch\n            branch = light_repo.branch ? light_repo.branch : default_branch\n            Utils.execute_shell_cmd(light_repo.clone_url(Workspace.root, clone_branch:branch)) { |stdout, stderr, status|\n              repo_dir = light_repo.abs_dest(Workspace.root)\n              if status.success?\n                Utils.link_git(repo_dir, light_repo.git_store_dir(Workspace.root))\n\n                mutex_progress.lock\n                task_count += 1\n                Output.puts_success_message(\"(#{task_count}/#{try_repos.length}) \\\"#{light_repo.name}\\\"克隆完成！\")\n                mutex_progress.unlock\n              else\n                Output.puts_remind_message(\"\\\"#{light_repo.name}\\\"克隆失败，已加入重试队列。\") if try_count < total_retry_count\n                mutex.lock\n                retry_repos.push(light_repo)\n                error_repos[light_repo.name] = stderr\n                FileUtils.remove_dir(repo_dir, true) if Dir.exist?(repo_dir)\n                mutex.unlock\n              end\n            }\n          }\n\n          if retry_repos.length == 0 || try_count >= total_retry_count\n            break\n          else\n            Output.puts_processing_block(error_repos.keys, \"以上仓库克隆失败，开始第#{try_count + 1}次重试(最多#{total_retry_count}次)...\")\n            try_repos = retry_repos\n            retry_repos = []\n            error_repos = {}\n            task_count = 0\n          end\n        }\n\n        if error_repos.length > 0\n          Workspace.show_error(error_repos)\n          terminate!(\"初始化停止，请重试！\")\n        end\n      end\n    end\n\n    def link_config(config_path, config_cache_path, root)\n      Utils.execute_under_dir(\"#{File.join(root, Constants::PROJECT_DIR[:source_config])}\") {\n        # 在.mgit/source-config文件夹下创建原始配置文件的软连接\n        config_link_path = File.join(Dir.pwd, Constants::CONFIG_FILE_NAME[:manifest])\n        Utils.link(config_path, config_link_path) if config_path != config_link_path\n\n        # 将配置缓存移动到.mgit/source-config文件夹下\n        FileUtils.mv(config_cache_path, Dir.pwd) if File.dirname(config_cache_path) != Dir.pwd\n      }\n    end\n\n    def terminate!(msg)\n      Output.puts_fail_message(msg)\n      Output.puts_processing_message(\"删除缓存...\")\n      FileUtils.remove_dir(Workspace.root, true) if Dir.exist?(Workspace.root)\n      Output.puts_success_message(\"删除完成！\")\n      exit\n    end\n\n    def move_project_to_root\n      Dir.foreach(Workspace.root) { |item|\n        if item != '.' && item != '..' && item != '.DS_Store'\n          FileUtils.mv(File.join(Workspace.root, item), @origin_root)\n        end\n      }\n      FileUtils.remove_dir(Workspace.root, true) if Dir.exist?(Workspace.root)\n    end\n\n    def finish_init(config)\n      move_project_to_root\n      config_path, config_cache_path = config.path.sub(Workspace.root, @origin_root), config.cache_path.sub(Workspace.root, @origin_root)\n      link_config(config_path, config_cache_path, @origin_root)\n    end\n\n    def enable_short_basic_option\n      true\n    end\n\n    def self.description\n      \"初始化多仓库目录。\"\n    end\n\n    def self.usage\n      \"mgit init (-f <path> | -g <url> [-b <branch>] [-l]) [-a]\"\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/command/log.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope 查询多仓库的日志\n  #\n  class Log < BaseCommand\n\n    OPT_LIST = {\n      :number    =>  '--number',\n      :number_s  =>  '-n'\n    }.freeze\n\n    def options\n      return [\n          ARGV::Opt.new(OPT_LIST[:number], short_key:OPT_LIST[:number_s], default:500, info:\"指定需要显示的提交log个数，默认500。\", type: :string)\n      ].concat(super)\n    end\n\n    def revise_option_value(opt)\n      opt.value = Integer(opt.value) if opt.key == OPT_LIST[:number]\n    end\n\n    def execute(argv)\n      repo_name = parse_repo_name(argv)\n      repo = all_repos.find { |e| e.name.downcase == repo_name.downcase }\n      number = argv.opt(OPT_LIST[:number]).value\n      if repo.nil?\n        Output.puts_fail_message(\"未找到与输入仓库名\\\"#{repo_name}\\\"匹配的仓库，请重试！\") && return\n        return\n      end\n      # print(Output.processing_message(\"正在提取#{repo.name}最新的#{number}条log信息...\"))\n      success, output = repo.execute_git_cmd(argv.cmd, \"-n #{number}\")\n      if success\n        if output.length > 0\n          Output.puts_in_pager(output.gsub(/commit.*/) { |s| Output.yellow_message(s) })\n          # print(\"\\r\")\n        else\n          Output.puts_remind_message(\"无提交记录\")\n        end\n      else\n        Output.puts_fail_message(\"执行失败：#{output}\")\n      end\n    end\n\n    def parse_repo_name(argv)\n      return nil if argv.git_opts.nil?\n      repo = argv.git_opts.split(' ')\n      extra_opts = repo.select { |e| argv.is_option?(e) }\n      Foundation.help!(\"输入非法参数：#{extra_opts.join('，')}。请通过\\\"mgit #{argv.cmd} --help\\\"查看用法。\") if extra_opts.length > 0\n      Foundation.help!(\"未输入查询仓库名！请使用这种形式查询：mgit log some_repo\") if repo.length == 0\n      Foundation.help!(\"仅允许查询一个仓库！\") if repo.length > 1\n      repo.first\n    end\n\n    def is_integer?(string)\n      true if Integer(string) rescue false\n    end\n\n    def enable_short_basic_option\n      true\n    end\n\n    def self.description\n      \"输出指定(单个)仓库的提交历史。\"\n    end\n\n    def self.usage\n      \"mgit log <repo> [-n] [-h]\"\n    end\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/merge.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope 类似 git merge\n  #\n  class Merge < BaseCommand\n\n    PROGRESS_STAGE = {\n      :new_start          => 0,\n      :did_pull_config    => 1,\n      :did_refresh_config => 2,\n      :did_pull_sub       => 3\n    }.freeze\n    PROGRESS_STAGE_KEY = 'progress_stage'\n\n    PROGRESS_AUTO = 'auto_exec'\n\n    OPT_LIST = {\n      :pull      => '--pull'\n    }.freeze\n\n    def options\n      return [\n          ARGV::Opt.new(OPT_LIST[:pull], info:'可选参数，指定后在合并仓库前会自动拉取远程分支更新代码，否则会有交互式询问。如：\"mgit merge --pull\"。', type: :boolean)\n      ].concat(super)\n    end\n\n    def __progress_type\n      OperationProgressManager::PROGRESS_TYPE[:merge]\n    end\n\n    def execute(argv)\n      return if do_abort(argv)\n\n      Workspace.check_branch_consistency\n\n      Output.puts_start_cmd\n\n      config_repo = generate_config_repo\n\n      if mgit_try_to_continue?\n        # 不处于中间态禁止执行\n        Foundation.help!(\"当前并不处于操作中间态，无法进行continue操作！\") if !OperationProgressManager.is_in_progress?(Workspace.root, __progress_type)\n\n        # 读取指令缓存失败禁止执行\n        context, _ = OperationProgressManager.load_context(Workspace.root, __progress_type)\n        Foundation.help!(\"缓存指令读取失败，continue无法继续进行，请重新执行完整指令。\") if context.nil? || !context.validate?\n\n        # 分支不匹配禁止执行\n        Foundation.help!(\"当前主仓库所在分支跟上次操作时所在分支(#{context.branch})不一致，请切换后重试。\") if config_repo.status_checker.current_branch(use_cache:true) != context.branch\n\n        if !context.repos.nil?\n          Output.puts_processing_message(\"加载上次即将操作的子仓库...\")\n          Workspace.update_all_repos(context.repos)\n        end\n\n        cmd = context.cmd\n        opts = context.opts\n        config_error = continue_execute(cmd, opts, config_repo, context.other[PROGRESS_STAGE_KEY], context.other[PROGRESS_AUTO])\n        if config_error\n          Output.puts_fail_block([config_repo.name], \"主仓库操作失败：#{config_error}\")\n          return\n        end\n      else\n        # 处于中间态则提示\n        if OperationProgressManager.is_in_progress?(Workspace.root, __progress_type)\n          if Output.continue_with_user_remind?(\"当前处于操作中间态，建议取消操作并执行\\\"mgit merge --continue\\\"继续操作未完成仓库。\\n    继续执行将清除中间态并重新操作所有仓库，是否取消？\")\n            Output.puts_cancel_message\n            return\n          end\n        end\n\n        cmd = argv.cmd\n        opts = argv.git_opts\n        if !opts.include?('--continue') && !opts.include?('--abort')\n          # 自动添加--no-ff\n          if !opts.include?('--no-ff') && !opts.include?('--ff') && !opts.include?('--ff-only') && !opts.include?('--squash')\n            opts += ' --no-ff'\n          end\n\n          # 检测提示\n          if !opts.include?('--no-commit') &&\n            !opts.include?('--commit') &&\n            !opts.include?('-m ') &&\n              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，是否取消并重新输入？\")\n\n            Output.puts_cancel_message\n            return\n\n          end\n        end\n\n        # 优先操作配置仓库\n        config_error = merge_config_repo(cmd, opts, config_repo, argv.opt_list.did_set_opt?(OPT_LIST[:pull]))\n        if config_error\n          Output.puts_fail_block([config_repo.name], \"主仓库操作失败：#{config_error}\")\n          return\n        end\n      end\n\n      do_repos = []\n      dirty_repos = []\n      detached_repos = []\n      no_tracking_repos = []\n\n      Output.puts_processing_message(\"检查各仓库状态...\")\n      Workspace.serial_enumerate_with_progress(all_repos) { |repo|\n        next if !config_repo.nil? && repo.name == config_repo.name\n\n        status = repo.status_checker.status\n        branch_status = repo.status_checker.branch_status\n\n        if status == Repo::Status::GIT_REPO_STATUS[:clean] &&\n          branch_status != Repo::Status::GIT_BRANCH_STATUS[:detached] &&\n          branch_status != Repo::Status::GIT_BRANCH_STATUS[:no_tracking]\n          do_repos.push(repo)\n        else\n          if status == Repo::Status::GIT_REPO_STATUS[:dirty]\n            dirty_repos.push(repo)\n          end\n          if branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]\n            detached_repos.push(repo)\n          elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]\n            no_tracking_repos.push(repo)\n          end\n        end\n      }\n      Output.puts_success_message(\"检查完成！\\n\")\n\n      if dirty_repos.length > 0 || no_tracking_repos.length > 0 || detached_repos.length > 0\n        remind_repos = []\n        remind_repos.push(['有本地改动', dirty_repos.map { |e| e.name }])\n        remind_repos.push(['未追踪远程分支(建议:mgit branch -u origin/<branch>)', no_tracking_repos.map { |e| e.name }]) if no_tracking_repos.length > 0\n        remind_repos.push(['HEAD游离,当前不在任何分支上', detached_repos.map { |e| e.name }]) if detached_repos.length > 0\n        Output.interact_with_multi_selection_combined_repos(remind_repos, \"以上仓库状态异常\", ['a: 跳过并继续', 'b: 强制执行', 'c: 终止']) { |input|\n          if input == 'b'\n            do_repos += dirty_repos\n            do_repos += detached_repos\n            do_repos += no_tracking_repos\n            do_repos.uniq! { |repo| repo.name }\n          elsif input == 'c' || input != 'a'\n            Output.puts_cancel_message\n            return\n          end\n        }\n      end\n\n      error_repos = {}\n      if do_repos.length == 0\n        Output.puts_remind_message(\"没有仓库需要执行merge指令！\") if config_repo.nil?\n      else\n        Output.puts_processing_message(\"开始merge子仓库...\")\n        _, error_repos = Workspace.execute_git_cmd_with_repos(cmd, opts, do_repos)\n      end\n\n      if config_error.nil? && error_repos.length == 0\n        Output.puts_succeed_cmd(\"#{cmd} #{opts}\")\n      end\n\n      # 清除中间态\n      OperationProgressManager.remove_progress(Workspace.root, __progress_type)\n    end\n\n    # 合并主仓库\n    #\n    # @param cmd [String] 合并指令\n    #\n    # @param opts [String] 合并参数\n    #\n    # @param repo [Repo] 配置仓库对象\n    #\n    # @param exec_repos [Array<Repo>] 本次操作的所有仓库（含配置仓库）\n    #\n    def merge_config_repo(cmd, opts, repo, auto_update)\n      if !repo.nil?\n        branch_status = repo.status_checker.branch_status\n        if branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]\n          remind_config_repo_fail(\"主仓库\\\"#{repo.name}\\\"HEAD游离，当前不在任何分支上，无法执行!\")\n        elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]\n          remind_config_repo_fail(\"主仓库\\\"#{repo.name}\\\"未跟踪对应远程分支，无法执行！(需要执行'mgit branch -u origin/<branch>')\")\n        elsif repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]\n          remind_config_repo_fail(\"主仓库\\\"#{repo.name}\\\"有改动，无法执行\\\"#{cmd}\\\"!\")\n        else\n          return continue_execute(cmd, opts, repo, PROGRESS_STAGE[:new_start], auto_update)\n        end\n      end\n    end\n\n    def continue_execute(cmd, opts, repo, check_point, auto_update)\n\n      # 现场信息\n      exec_subrepos = all_repos(except_config:true)\n      is_all = Workspace.is_all_exec_sub_repos?(exec_subrepos)\n      context = OperationProgressContext.new(__progress_type)\n      context.cmd = cmd\n      context.opts = opts\n      context.repos = is_all ? nil : exec_subrepos.map { |e| e.name } # nil表示操作所有子仓库\n      context.branch = repo.status_checker.current_branch(use_cache:true)\n\n      # 更新主仓库\n      update_config_repo(repo, context, auto_update) if check_point < PROGRESS_STAGE[:did_pull_config]\n\n      if check_point < PROGRESS_STAGE[:did_refresh_config]\n        # 操作主仓库\n        config_error = exec_config_repo(repo, cmd, opts)\n        return config_error if config_error\n        # 更新配置表\n        refresh_config(repo, context, auto_update)\n      end\n\n      if check_point < PROGRESS_STAGE[:did_pull_sub]\n        # 如果本次操作所有子仓库，则再次获取所有子仓库（因为配置表可能已经更新，子仓库列表也有更新，此处获取的仓库包含：已有的子仓库 + 合并后新下载仓库 + 从缓存弹出的仓库）\n        exec_subrepos = all_repos(except_config:true) if is_all\n        # 更新子仓库\n        update_subrepos(exec_subrepos, context, auto_update)\n      end\n\n      config_error\n    end\n\n    def update_config_repo(repo, context, auto)\n      if auto || Output.continue_with_user_remind?(\"即将合并主仓库，是否先拉取远程代码更新？\")\n        Output.puts_processing_message(\"正在更新主仓库...\")\n        success, output = repo.execute_git_cmd('pull', '')\n        if !success\n          context.other = {\n            PROGRESS_STAGE_KEY => PROGRESS_STAGE[:did_pull_config],\n            PROGRESS_AUTO => auto\n          }\n          OperationProgressManager.trap_into_progress(Workspace.root, context)\n          show_progress_error(\"主仓库更新失败\", \"#{output}\")\n        else\n          Output.puts_success_message(\"更新成功！\\n\")\n        end\n      end\n    end\n\n    def exec_config_repo(repo, cmd, opts)\n      error = nil\n      Output.puts_processing_message(\"开始操作主仓库...\")\n      success, output = repo.execute_git_cmd(cmd, opts)\n      if success\n        Output.puts_success_message(\"操作成功！\\n\")\n      else\n        Output.puts_fail_message(\"操作失败！\\n\")\n        error = output\n      end\n      return error\n    end\n\n    # 刷新配置表\n    def refresh_config(repo, context, auto)\n      begin\n        Workspace.update_config(strict_mode:false) { |missing_repos|\n          if missing_repos.length > 0\n            # 这里分支引导仅根据主仓库来进行，如果使用all_repos来作为引导\n            # 基准，可能不准确(因为all_repos可能包含merge分支已有的本地\n            # 仓库，而这些仓库所在分支可能五花八门，数量也可能多于处于正确\n            # 分支的仓库)。\n            success_missing_repos = Workspace.guide_to_checkout_branch(missing_repos, [repo], append_message:\"拒绝该操作本次执行将忽略以上仓库\")\n            all_repos.concat(success_missing_repos)\n            # success_missing_repos包含新下载的和当前分支已有的新仓库，其中已有仓库包含在@all_repos内，需要去重\n            all_repos.uniq! { |repo| repo.name }\n          end\n        }\n        refresh_context(context)\n      rescue Error => e\n        if e.type == MGIT_ERROR_TYPE[:config_generate_error]\n          context.other = {\n            PROGRESS_STAGE_KEY => PROGRESS_STAGE[:did_refresh_config],\n            PROGRESS_AUTO => auto\n          }\n          OperationProgressManager.trap_into_progress(Workspace.root, context)\n          show_progress_error(\"配置表生成失败\", \"#{e.msg}\")\n        end\n      end\n    end\n\n    def update_subrepos(subrepos, context, auto)\n      if auto || Output.continue_with_user_remind?(\"即将合并子仓库，是否先拉取远程代码更新？\")\n        Output.puts_processing_message(\"正在更新子仓库...\")\n        _, error_repos = Workspace.execute_git_cmd_with_repos('pull', '', subrepos)\n        if error_repos.length > 0\n          context.other = {\n            PROGRESS_STAGE_KEY => PROGRESS_STAGE[:did_pull_sub],\n            PROGRESS_AUTO => auto\n          }\n          OperationProgressManager.trap_into_progress(Workspace.root, context)\n          show_progress_error(\"子仓库更新失败\", \"见上述输出\")\n        else\n          Output.puts_success_message(\"更新成功！\\n\")\n        end\n      end\n    end\n\n    def refresh_context(context)\n      exec_subrepos = all_repos(except_config:true)\n      is_all = Workspace.is_all_exec_sub_repos?(exec_subrepos)\n      context.repos = is_all ? nil : exec_subrepos.map { |e| e.name } # nil表示操作所有子仓库\n    end\n\n    def remind_config_repo_fail(msg)\n      Output.puts_fail_message(msg)\n      if Output.continue_with_user_remind?(\"是否继续操作其余仓库？\")\n        return\n      else\n        Output.puts_cancel_message\n        exit\n      end\n    end\n\n    def do_abort(argv)\n      if argv.git_opts.include?('--abort')\n        Output.puts_start_cmd\n        OperationProgressManager.remove_progress(Workspace.root, __progress_type)\n        do_repos = all_repos.select { |repo| repo.status_checker.is_in_merge_progress? }\n\n        if do_repos.length > 0\n          append_message = \"，另有#{all_repos.length - do_repos.length}个仓库无须操作\" if do_repos.length < all_repos.length\n          Output.puts_processing_block(do_repos.map { |e| e.name }, \"开始操作以上仓库#{append_message}...\")\n          _, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, do_repos)\n          Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0\n        else\n          Output.puts_success_message(\"没有仓库需要操作！\")\n        end\n\n        return true\n      else\n        return false\n      end\n    end\n\n    def show_progress_error(summary, detail)\n      error = \"#{summary} 已进入操作中间态。\n\n原因：\n  #{detail}\n\n可选：\n  - 使用\\\"mgit merge --continue\\\"继续合并。\n  - 使用\\\"mgit merge --abort\\\"取消合并。\"\n      Foundation.help!(error, title:'暂停')\n    end\n\n    def enable_repo_selection\n      return true\n    end\n\n    def enable_continue_operation\n      return true\n    end\n\n    def self.description\n      return \"合并两个或多个开发历史。\"\n    end\n\n    def self.usage\n      return \"mgit merge [<git-merge-option>] [--pull] [(--mrepo|--el-mrepo) <repo>...] [--help]\\nmgit merge --continue\\nmgit merge --abort\"\n    end\n\n    # 判断是否需要fast forward\n    # def is_fast_forward?(argv, repos)\n    #   git_opts = argv.git_opts(raw:false)\n    #   git_opts.each { |opt_arr|\n    #     if !argv.is_option?(opt_arr.first)\n    #       repos.each { |repo|\n    #         opt_arr.each { |branch|\n    #           if repo.status_checker.is_ancestor_of_branch?(branch)\n    #             return true\n    #           end\n    #         }\n    #       }\n    #     end\n    #   }\n    #   return false\n    # end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/pull.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope 类似 git pull\n  #\n  class Pull < BaseCommand\n\n    OPT_LIST = {\n      :no_check    => '--no-check',\n    }.freeze\n\n    def options\n      [\n          ARGV::Opt.new(OPT_LIST[:no_check], info:'指定该参数意味着执行前跳过仓库的状态检查，直接对指定或所有仓库执行pull操作，有一定风险，请慎重执行。', type: :boolean),\n      ].concat(super)\n    end\n\n    def __progress_type\n      OperationProgressManager::PROGRESS_TYPE[:pull]\n    end\n\n    def execute(argv)\n      if argv.opt(OPT_LIST[:no_check])\n        simple_pull(argv)\n        return\n      end\n\n      verbose_pull(argv)\n    end\n\n    def verbose_pull(argv)\n      return if do_abort(argv)\n\n      Output.puts_start_cmd\n\n      # 获取远程仓库当前分支信息\n      Workspace.pre_fetch\n\n      config_repo = generate_config_repo\n\n      if mgit_try_to_continue?\n        # 不处于中间态禁止执行\n        Foundation.help!(\"当前并不处于操作中间态，无法进行continue操作！\") if !OperationProgressManager.is_in_progress?(Workspace.root, __progress_type)\n\n        # 读取指令缓存失败禁止执行\n        context, _ = OperationProgressManager.load_context(Workspace.root, __progress_type)\n        Foundation.help!(\"缓存指令读取失败，continue无法继续进行，请重新执行完整指令。\") if context.nil? || !context.validate?\n\n        # 分支不匹配禁止执行\n        Foundation.help!(\"当前主仓库所在分支跟上次操作时所在分支(#{context.branch})不一致，请切换后重试。\") if config_repo.status_checker.current_branch(use_cache:true) != context.branch\n\n        if !context.repos.nil?\n          Output.puts_processing_message(\"加载上次即将操作的子仓库...\")\n          Workspace.update_all_repos(context.repos)\n        end\n\n        Output.puts_success_message(\"已跳过主仓库。\")\n\n      else\n\n        # 处于中间态则提示\n        if OperationProgressManager.is_in_progress?(Workspace.root, __progress_type)\n          if Output.continue_with_user_remind?(\"当前处于操作中间态，建议取消操作并执行\\\"mgit pull --continue\\\"继续操作子仓库。\\n    继续执行将清除中间态并重新操作所有仓库，是否取消？\")\n            Output.puts_cancel_message\n            return\n          end\n        end\n\n        # 优先pull配置仓库\n        config_error = pull_config_repo(argv.cmd, argv.git_opts, config_repo)\n        if config_error\n          Output.puts_fail_block([config_repo.name], \"主仓库操作失败：#{config_error}\")\n          return\n        end\n      end\n\n      do_repos = []\n      diverged_repos = []\n      no_remote_repos = []\n      no_tracking_repos = []\n      dirty_repos = []\n      detached_repos = []\n      remote_inconsist_repos = []\n      do_nothing_repos = []\n\n      Output.puts_processing_message(\"检查各仓库状态...\")\n      Workspace.serial_enumerate_with_progress(all_repos) { |repo|\n        next if !config_repo.nil? && repo.name == config_repo.name\n\n        Timer.start(repo.name)\n        status = repo.status_checker.status\n        branch_status = repo.status_checker.branch_status\n\n        if branch_status == Repo::Status::GIT_BRANCH_STATUS[:up_to_date] || branch_status == Repo::Status::GIT_BRANCH_STATUS[:ahead]\n          # 领先和最新的仓库均不操作\n          do_nothing_repos.push(repo)\n        else\n\n          url_consist = repo.url_consist?\n          is_dirty = status == Repo::Status::GIT_REPO_STATUS[:dirty]\n          dirty_repos.push(repo) if is_dirty\n          remote_inconsist_repos.push(repo) if !url_consist\n\n          # 仅有分叉或落后，且工作区干净的仓库直接加入到操作集\n          if branch_status == Repo::Status::GIT_BRANCH_STATUS[:diverged] && !is_dirty && url_consist\n            do_repos.push(repo)\n            diverged_repos.push(repo.name)\n          elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:behind] && !is_dirty && url_consist\n            do_repos.push(repo)\n          elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_remote]\n            no_remote_repos.push(repo)\n          elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]\n            no_tracking_repos.push(repo)\n          elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]\n            detached_repos.push(repo)\n          end\n\n        end\n        Timer.stop(repo.name)\n      }\n      Output.puts_success_message(\"检查完成！\\n\")\n\n      if Workspace.filter_config.auto_exec\n        do_repos += dirty_repos\n        do_repos.uniq! { |repo| repo.name }\n      elsif no_remote_repos.length > 0 ||\n        dirty_repos.length > 0 ||\n        detached_repos.length > 0 ||\n        no_tracking_repos.length > 0 ||\n        remote_inconsist_repos.length > 0\n        remind_repos = []\n        remind_repos.push(['远程分支不存在', no_remote_repos.map { |e| e.name }]) if no_remote_repos.length > 0\n        remind_repos.push(['未追踪远程分支(建议:mgit branch -u origin/<branch>)', no_tracking_repos.map { |e| e.name }]) if no_tracking_repos.length > 0\n        remind_repos.push(['有本地改动', dirty_repos.map { |e| e.name }]) if dirty_repos.length > 0\n        remind_repos.push(['HEAD游离,当前不在任何分支上', detached_repos.map { |e| e.name }]) if detached_repos.length > 0\n        remind_repos.push(['实际url与配置不一致', remote_inconsist_repos.map { |e| e.name }]) if remote_inconsist_repos.length > 0\n        Output.interact_with_multi_selection_combined_repos(remind_repos, \"以上仓库状态异常\", ['a: 跳过并继续', 'b: 强制执行', 'c: 终止']) { |input|\n          if input == 'b'\n            do_repos += dirty_repos\n            do_repos += detached_repos\n            do_repos += no_remote_repos\n            do_repos += no_tracking_repos\n            do_repos += remote_inconsist_repos\n            do_repos.uniq! { |repo| repo.name }\n          elsif input == 'c' || input != 'a'\n            Output.puts_cancel_message\n            return\n          end\n        }\n      end\n\n      if do_repos.length != 0\n        error_repos = []\n        # 如果不带任何参数，则将pull分解为fetch+merge执行, fetch已经执行，此处执行merge。带参数则透传。\n        if argv.git_opts.length == 0\n          # 排除HEAD游离，无远程分支，未追踪远程分支的仓库，这三种仓库是无法强制执行git pull的（但是可以执行如：git pull origin master，因此透传不做此校验）\n          skip_repos = do_repos.select { |repo|\n            branch_status = repo.status_checker.branch_status\n            branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_remote] ||\n            branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking] ||\n            branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]\n          }\n          if skip_repos.length > 0\n            Output.puts_remind_block(skip_repos.map { |e| e.name }, \"以上仓库无法强制执行，已跳过。\")\n            do_repos -= skip_repos\n            if do_repos.length == 0\n              Output.puts_success_message(\"仓库均为最新，无须执行！\")\n              return\n            end\n          end\n\n          count_msg = \"，另有#{do_nothing_repos.length}个仓库无须执行\" if do_nothing_repos.length > 0\n          Output.puts_remind_block(do_repos.map { |repo| repo.name }, \"开始为以上仓库合并远程分支#{count_msg}...\")\n          _, error_repos = Workspace.execute_git_cmd_with_repos('', '', do_repos) { |repo|\n            msg = nil\n            branch = repo.status_checker.current_branch(strict_mode:false, use_cache:true)\n            tracking_branch = repo.status_checker.tracking_branch(branch, use_cache:true)\n            # 如果产生分叉，为生成的新节点提供log\n            msg = \"-m \\\"【Merge】【0.0.0】【#{branch}】合并远程分支'#{tracking_branch}'。\\\"\" if diverged_repos.include?(repo.name)\n            [\"merge\", \"#{tracking_branch} #{msg}\"]\n          }\n        else\n          count_msg = \"，另有#{do_nothing_repos.length}个仓库无须执行\" if do_nothing_repos.length > 0\n          Output.puts_remind_block(do_repos.map { |repo| repo.name }, \"开始pull以上仓库#{count_msg}...\")\n          _, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, do_repos)\n        end\n\n        if config_error.nil? && error_repos.length == 0\n          Output.puts_succeed_cmd(argv.absolute_cmd)\n          Timer.show_time_consuming_repos\n        end\n\n      else\n        Output.puts_success_message(\"仓库均为最新，无须执行！\")\n      end\n\n      # 清除中间态\n      OperationProgressManager.remove_progress(Workspace.root, __progress_type)\n    end\n\n    def simple_pull(argv)\n      Output.puts_start_cmd\n\n      # 优先pull配置仓库\n      config_repo = generate_config_repo\n      config_error = pull_config_repo(argv.cmd, argv.git_opts, config_repo)\n\n      Output.puts_processing_message(\"开始pull子仓库...\")\n      _, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, all_repos)\n\n      if config_error.nil? && error_repos.length == 0\n        Output.puts_succeed_cmd(argv.absolute_cmd)\n      elsif !config_error.nil?\n        Output.puts_fail_block([config_repo.name], \"主仓库操作失败：#{config_error}\")\n      end\n\n      # 情况中间态\n      OperationProgressManager.remove_progress(Workspace.root, __progress_type)\n    end\n\n    def pull_config_repo(cmd, opts, repo)\n      if !repo.nil?\n        branch_status = repo.status_checker.branch_status\n        if branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]\n          remind_config_repo_fail(\"主仓库\\\"#{repo.name}\\\"HEAD游离，当前不在任何分支上，无法执行！\")\n        elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]\n          remind_config_repo_fail(\"主仓库\\\"#{repo.name}\\\"未跟踪对应远程分支，无法执行！(需要执行'mgit branch -u origin/<branch>')\")\n        elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_remote]\n          remind_config_repo_fail(\"主仓库\\\"#{repo.name}\\\"远程分支不存在，无法执行！\")\n        # elsif repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]\n        #   remind_config_repo_fail(\"主仓库\\\"#{repo.name}\\\"有改动，无法执行！\")\n        else\n\n          Output.puts_processing_message(\"开始操作主仓库...\")\n\n          # 现场信息\n          exec_subrepos = all_repos(except_config:true)\n          is_all = Workspace.is_all_exec_sub_repos?(exec_subrepos)\n          context = OperationProgressContext.new(__progress_type)\n          context.cmd = cmd\n          context.opts = opts\n          context.repos = is_all ? nil : exec_subrepos.map { |e| e.name } # nil表示操作所有子仓库\n          context.branch = repo.status_checker.current_branch(use_cache:true)\n\n          # 如果不带任何参数，则将pull分解为fetch+merge执行, fetch已经执行，此处执行merge。带参数则透传。\n          if opts.length == 0\n            branch = repo.status_checker.current_branch(strict_mode:false, use_cache:true)\n            tracking_branch = repo.status_checker.tracking_branch(branch, use_cache:true)\n            msg = \"-m \\\"【Merge】【0.0.0】【#{branch}】合并远程分支'#{tracking_branch}'。\\\"\" if branch_status == Repo::Status::GIT_BRANCH_STATUS[:diverged]\n            cmd, opts = \"merge\", \"#{tracking_branch} #{msg}\"\n          end\n\n          success, output = repo.execute_git_cmd(cmd, opts)\n          if success\n            Output.puts_success_message(\"主仓库操作成功！\\n\")\n          else\n            Output.puts_fail_message(\"主仓库操作失败！\\n\")\n            config_error = output\n          end\n\n          # 刷新配置表\n          begin\n            Workspace.update_config(strict_mode:false) { |missing_repos|\n              if missing_repos.length > 0\n                success_missing_repos = Workspace.guide_to_checkout_branch(missing_repos, all_repos, append_message:\"拒绝该操作本次执行将忽略以上仓库\")\n                all_repos.concat(success_missing_repos)\n                # success_missing_repos包含新下载的和当前分支已有的新仓库，其中已有仓库包含在@all_repos内，需要去重\n                all_repos.uniq! { |repo| repo.name }\n              end\n            }\n            refresh_context(context)\n          rescue Error => e\n            if e.type == MGIT_ERROR_TYPE[:config_generate_error]\n              OperationProgressManager.trap_into_progress(Workspace.root, context)\n              show_progress_error(\"配置表生成失败\", \"#{e.msg}\")\n            end\n          end\n\n          return config_error\n        end\n      end\n    end\n\n    def remind_config_repo_fail(msg)\n      Output.puts_fail_message(msg)\n      if Workspace.filter_config.auto_exec || Output.continue_with_user_remind?(\"是否继续操作其余仓库？\")\n        return\n      else\n        Output.puts_cancel_message\n        exit\n      end\n    end\n\n    def do_abort(argv)\n      if mgit_try_to_abort?\n        Output.puts_start_cmd\n        OperationProgressManager.remove_progress(Workspace.root, __progress_type)\n        do_repos = all_repos.select { |repo| repo.status_checker.is_in_merge_progress? }\n\n        if do_repos.length > 0\n          append_message = \"，另有#{all_repos.length - do_repos.length}个仓库无须操作\" if do_repos.length < all_repos.length\n          Output.puts_processing_block(do_repos.map { |e| e.name }, \"开始操作以上仓库#{append_message}...\")\n          _, error_repos = Workspace.execute_git_cmd_with_repos('merge', '--abort', do_repos)\n          Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0\n        else\n          Output.puts_success_message(\"没有仓库需要操作！\")\n        end\n\n        return true\n      else\n        return false\n      end\n    end\n\n    def refresh_context(context)\n      exec_subrepos = all_repos(except_config:true)\n      is_all = Workspace.is_all_exec_sub_repos?(exec_subrepos)\n      context.repos = is_all ? nil : exec_subrepos.map { |e| e.name } # nil表示操作所有子仓库\n    end\n\n    def show_progress_error(summary, detail)\n      error = \"#{summary} 已进入操作中间态。\n\n原因：\n  #{detail}\n\n可选：\n  - 使用\\\"mgit pull --continue\\\"继续拉取。\n  - 使用\\\"mgit pull --abort\\\"取消拉取。\"\n      Foundation.help!(error, title:'暂停')\n    end\n\n    def enable_repo_selection\n      return true\n    end\n\n    def enable_auto_execution\n      return true\n    end\n\n    def include_lock_by_default\n      return true\n    end\n\n    def enable_continue_operation\n      return true\n    end\n\n    def enable_abort_operation\n      return true\n    end\n\n    def self.description\n      return \"从仓库或本地分支获取数据并合并。\"\n    end\n\n    def self.usage\n      return \"mgit pull [<git-pull-option>] [(--mrepo|--el-mrepo) <repo>...] [--auto-exec] [--no-check] [--include-lock] [--help]\\nmgit pull --continue\\nmgit pull --abort\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/push.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope 类似 git push\n  # 可自动生成 git gerrit 评审分支\n  # mgit push --gerrit\n  #\n  class Push < BaseCommand\n\n    # 默认是否开启gerrit相关功能\n    #\n    MGIT_PUSH_GERRIT_ENABLED = false\n\n    # 默认是否开启topic相关功能，开启后强制开启gerrit\n    MGIT_PUSH_TOPIC_ENABLED = false\n\n    OPT_LIST = {\n      :gerrit     =>  '--gerrit',\n      :topic_id   =>  '--topic'\n    }.freeze\n\n    def options\n      [\n          ARGV::Opt.new(OPT_LIST[:gerrit],\n                        info:\"开启gerrit功能，如果没有对应远程分支则推送新分支，否则推送到审查分支（refs/for/**），默认未开启\",\n                        type: :boolean),\n          ARGV::Opt.new(OPT_LIST[:topic_id],\n                        info:\"指定一组变更的分类topic，若未指定则自动生成，默认未开启，开启后强制开启Gerrit功能。mgit push --topic 12345 = git push origin HEAD:refs/for/<branch>%topic=12345\",\n                        type: :string),\n      ].concat(super)\n    end\n\n    attr_reader :topic_id\n    attr_reader :gerrit_enabled\n    def __setup_option_value(argv)\n      group_id_opt = argv.opt(OPT_LIST[:topic_id])\n      if group_id_opt\n        @topic_id = group_id_opt.value\n      elsif MGIT_PUSH_TOPIC_ENABLED\n        @topic_id = SecureRandom.uuid\n      end\n\n      @gerrit_enabled = !@topic_id.nil? || argv.opt_list.did_set_opt?(OPT_LIST[:gerrit]) || MGIT_PUSH_GERRIT_ENABLED\n    end\n\n    def execute(argv)\n      __setup_option_value(argv)\n      if argv.git_opts&.length > 0\n        raws_string = ''\n        argv.raw_opts.each do |raws|\n          raws_string += ' '\n          raws_string += raws.join(' ')\n        end\n        Foundation.help!(\"禁止使用参数 #{argv.git_opts}\\n\" + Output.remind_message(\"建议直接使用mgit #{argv.cmd}#{raws_string}\"))\n      end\n      Workspace.check_branch_consistency\n\n      Output.puts_start_cmd\n\n      # 获取远程仓库当前分支信息\n      Workspace.pre_fetch\n\n      do_repos = []\n      diverged_repos = []\n      do_nothing_repos = []\n      detached_repos = []\n      no_remote_repos = []\n      no_tracking_repos = []\n      remote_inconsist_repos = []\n      dirty_repos = []\n\n      Output.puts_processing_message(\"检查各仓库状态...\")\n      Workspace.serial_enumerate_with_progress(all_repos) { |repo|\n        Timer.start(repo.name)\n\n        url_consist = repo.url_consist?\n        branch_status = repo.status_checker.branch_status\n        remote_inconsist_repos.push(repo) if !url_consist\n\n        if branch_status == Repo::Status::GIT_BRANCH_STATUS[:diverged]\n          diverged_repos.push(repo)\n        # 仅超前且url一致的仓库直接加入到操作集\n        elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:ahead] && url_consist\n          do_repos.push(repo)\n        elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_remote]\n          no_remote_repos.push(repo)\n        elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]\n          no_tracking_repos.push(repo)\n        elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]\n          detached_repos.push(repo)\n        else\n          do_nothing_repos.push(repo.name)\n        end\n\n        if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]\n          dirty_repos.push(repo)\n        end\n\n        Timer.stop(repo.name)\n      }\n      Output.puts_success_message(\"检查完成！\\n\")\n\n      # 将没有远程分支的仓库纳入到本次操作的仓库中\n      do_repos += no_remote_repos\n      no_remote_repos = []\n\n      if diverged_repos.length > 0 ||\n        detached_repos.length > 0 ||\n        no_remote_repos.length > 0 ||\n        no_tracking_repos.length > 0 ||\n        remote_inconsist_repos.length > 0 ||\n        dirty_repos.length > 0\n        remind_repos = []\n        remind_repos.push(['远程分支不存在', no_remote_repos.map { |e| e.name }]) if no_remote_repos.length > 0\n        remind_repos.push(['未追踪远程分支(建议:mgit branch -u origin/<branch>)', no_tracking_repos.map { |e| e.name }]) if no_tracking_repos.length > 0\n        remind_repos.push(['HEAD游离,当前不在任何分支上', detached_repos.map { |e| e.name }]) if detached_repos.length > 0\n        remind_repos.push(['当前分支与远程分支分叉,需先pull本地合并', diverged_repos.map { |e| e.name }]) if diverged_repos.length > 0\n        remind_repos.push(['实际url与配置不一致', remote_inconsist_repos.map { |e| e.name }]) if remote_inconsist_repos.length > 0\n        remind_repos.push(['有本地改动', dirty_repos.map { |e| e.name }]) if dirty_repos.length > 0\n        Output.interact_with_multi_selection_combined_repos(remind_repos, \"以上仓库状态异常\", ['a: 跳过并继续', 'b: 强制执行', 'c: 终止']) { |input|\n          if input == 'b'\n            do_repos += diverged_repos\n            do_repos += detached_repos\n            do_repos += no_remote_repos\n            do_repos += no_tracking_repos\n            do_repos += remote_inconsist_repos\n            do_repos.uniq! { |repo| repo.name }\n          elsif input == 'c' || input != 'a'\n            Output.puts_cancel_message\n            return\n          end\n        }\n      end\n      if do_repos.length == 0\n        Output.puts_remind_message(\"仓库均无新提交，无须执行！\")\n        return\n      end\n      HooksManager.execute_mgit_pre_push_hook(argv.cmd, argv.pure_opts, do_repos.map { |e| e.config })\n\n      # 跳过无法处理的异常状态仓库\n      skip_repos = do_repos.select { |repo|\n        branch_status = repo.status_checker.branch_status\n        branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached] ||\n            branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]\n      }\n      Output.puts_remind_block(skip_repos.map { |e| e.name }, \"以上仓库无法强制执行，已跳过。\") if skip_repos.length > 0\n      do_repos -= skip_repos\n      if do_repos.length == 0\n        Output.puts_success_message(\"仓库均无新提交，无须执行！\")\n        return\n      end\n\n      # 找到本次操作仓库中推新分支的仓库\n      no_remote_repo_names = do_repos.select { |repo|\n        branch_status = repo.status_checker.branch_status\n        branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_remote]\n      }.map { |repo| repo.name }\n\n      count_msg = \"，另有#{do_nothing_repos.length}个仓库无须执行\" if do_nothing_repos.length > 0\n      Output.puts_remind_block(do_repos.map { |repo| repo.name }, \"开始push以上仓库#{count_msg}...\")\n\n      # ------ 执行push ------\n      total_task = do_repos.length\n      Output.update_progress(total_task, 0)\n\n      config_repo_arr = do_repos.select { |repo| repo.config.is_config_repo }\n      do_repos_without_config_repo = do_repos - config_repo_arr\n\n      sub_error_repos, sub_cr_repos = __execute_push(argv, do_repos_without_config_repo) { |progress|\n        Output.update_progress(total_task, progress)\n      }\n\n      # 保证最后操作主仓库，便于流水线进行模块注册\n      sub_task_count = do_repos_without_config_repo.length\n      config_error_repos, config_cr_repos = __execute_push(argv, config_repo_arr) { |progress|\n        Output.update_progress(total_task, progress + sub_task_count)\n      }\n      puts \"\\n\"\n      # ----------------------\n      # 显示错误仓库信息\n      error_repos = sub_error_repos.merge(config_error_repos)\n      Workspace.show_error(error_repos) if error_repos.length > 0\n\n      # 显示成功推了新分支的仓库\n      success_push_branch_repo_names = no_remote_repo_names - error_repos.keys\n      if success_push_branch_repo_names.length > 0\n        Output.puts_remind_block(success_push_branch_repo_names, \"为以上仓库推送了新分支。\")\n        puts \"\\n\"\n      end\n\n      # 显示成功仓库的评审链接\n      if gerrit_enabled\n        success_output = ''\n        all_cr_repos = sub_cr_repos.keys + config_cr_repos.keys\n        all_cr_repos.uniq!\n        all_cr_repos.each do |repo_name|\n          cr_url = sub_cr_repos[repo_name] || config_cr_repos[repo_name]\n          success_output += Output.generate_title_block(repo_name, has_separator: false) { cr_url } + \"\\n\\n\"\n        end\n\n        if success_output.length > 0\n          Output.puts_remind_message(\"以下本地提交代码评审链接，请联系仓库负责人评审后合入：\")\n          puts success_output\n        end\n\n        # 显示topic id\n        if topic_id\n          success_push_code_repo_names = do_repos.map { |e| e.name } - error_repos.keys - success_push_branch_repo_names\n          if success_push_code_repo_names.length > 0\n            Output.puts_remind_message(\"本次push的topic id：#{topic_id}\\n\") if !topic_id.nil? && topic_id.length > 0\n          end\n        end\n      end\n\n      # 显示成功信息\n      if error_repos.empty?\n        Output.puts_succeed_cmd(argv.absolute_cmd)\n        Timer.show_time_consuming_repos\n      elsif topic_id\n        # 显示失败后的操作提示，若全部成功则不显示\n        group_repo_names = error_repos.keys\n        group_repo_name_str = group_repo_names.join(' ')\n        is_all = Workspace.is_all_exec_sub_repos_by_name?(group_repo_names)\n        mrepo_str = is_all ? '' : \" --mrepo #{group_repo_name_str}\"\n        Output.puts_processing_block(group_repo_names, \"以上仓库组推送失败，请处理后用以下指令再次推送：\\n\\n    mgit push --topic #{topic_id}#{mrepo_str}\\n\")\n      end\n\n    end\n\n    private\n\n    def __execute_push(argv, do_repos)\n      mutex = Mutex.new\n      error_repos = {}\n      cr_repos = {}\n      task_count = 0\n\n      Workspace.concurrent_enumerate(do_repos) { |repo|\n        cmd, opt = __parse_cmd_and_opt(repo)\n        git_cmd = repo.git_cmd(cmd, opt)\n\n        Utils.execute_shell_cmd(git_cmd) { |stdout, stderr, status|\n          mutex.lock\n          error_msg, cr_url = __process_push_result(repo, stdout, stderr, status)\n          error_repos[repo.name] = error_msg if error_msg\n          cr_repos[repo.name] = cr_url if cr_url\n\n          task_count += 1\n          yield(task_count) if block_given?\n          mutex.unlock\n        }\n      }\n\n      [error_repos, cr_repos]\n    end\n\n    #\n    # @return [String, String] error_message， code_review_url\n    #\n    #\n    def __process_push_result(repo, stdout, stderr, status)\n      # 标记状态更新\n      repo.status_checker.refresh\n\n      # 本地成功但远程失败此时status.success? == true，解析以检测这个情况\n      repo_msg_parser = GitMessageParser.new(repo.config.url)\n      check_msg = repo_msg_parser.parse_push_msg(stderr)\n\n      # 本地和远程同时成功\n      if status.success? && check_msg.nil?\n        cr_url = repo_msg_parser.parse_code_review_url(stdout) || repo_msg_parser.parse_code_review_url(stderr) if gerrit_enabled\n      elsif status.success? && !check_msg.nil?\n        # 本地失败\n        # check_msg = error_msg\n      else\n        check_msg = stderr\n        check_msg += stdout if stdout.length > 0\n      end\n      [check_msg, cr_url]\n    end\n\n    def __parse_cmd_and_opt(repo)\n      cmd = 'push'\n      if repo.status_checker.branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_remote]\n        opt = \"-u origin #{repo.status_checker.current_branch}\"\n      else\n        opt = \"origin HEAD:#{repo.status_checker.current_branch}\"\n        if gerrit_enabled\n          opt = \"origin HEAD:refs/for/#{repo.status_checker.current_branch}\"\n          opt += \"%topic=\" + topic_id if topic_id\n        end\n      end\n      [cmd, opt]\n    end\n\n    def enable_repo_selection\n      return true\n    end\n\n    def self.description\n      return \"更新远程分支和对应的数据对象。\"\n    end\n\n    def self.usage\n      return \"mgit push [<git-push-option>|--gerrit] [(--mrepo|--el-mrepo) <repo>...] [--topic] [--help]\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/rebase.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope 类似 git rebase\n  #\n  class Rebase < BaseCommand\n\n    PROGRESS_STAGE = {\n      :new_start          => 0,\n      :did_pull_config    => 1,\n      :did_refresh_config => 2,\n      :did_pull_sub       => 3\n    }.freeze\n    PROGRESS_STAGE_KEY = 'progress_stage'\n\n    PROGRESS_AUTO = 'auto_exec'\n\n    OPT_LIST = {\n      :pull      => '--pull'\n    }.freeze\n\n    def options\n      [\n          ARGV::Opt.new(OPT_LIST[:pull], info:'可选参数，指定后在合并仓库前会拉取远程分支更新代码，如：\"mgit rabase --pull\"。', type: :boolean)\n      ].concat(super)\n    end\n\n    def execute(argv)\n      return if do_abort(argv)\n\n      check_master_rebase(argv)\n      Workspace.check_branch_consistency\n\n      Output.puts_start_cmd\n\n      config_repo = generate_config_repo\n\n      if mgit_try_to_continue?\n        # 不处于中间态禁止执行\n        Foundation.help!(\"当前并不处于操作中间态，无法进行continue操作！\") if !OperationProgressManager.is_in_progress?(Workspace.root, __progress_type)\n\n        # 读取指令缓存失败禁止执行\n        context, _ = OperationProgressManager.load_context(Workspace.root, __progress_type)\n        Foundation.help!(\"缓存指令读取失败，continue无法继续进行，请重新执行完整指令。\") if context.nil? || !context.validate?\n\n        # 分支不匹配禁止执行\n        Foundation.help!(\"当前主仓库所在分支跟上次操作时所在分支(#{context.branch})不一致，请切换后重试。\") if config_repo.status_checker.current_branch(use_cache:true) != context.branch\n\n        if !context.repos.nil?\n          Output.puts_processing_message(\"加载上次即将操作的子仓库...\")\n          Workspace.update_all_repos(context.repos)\n        end\n\n        cmd = context.cmd\n        opts = context.opts\n        config_error = continue_execute(cmd, opts, config_repo, context.other[PROGRESS_STAGE_KEY], context.other[PROGRESS_AUTO])\n        if config_error\n          Output.puts_fail_block([config_repo.name], \"主仓库操作失败：#{config_error}\")\n          return\n        end\n      else\n        # 处于中间态则提示\n        if OperationProgressManager.is_in_progress?(Workspace.root, __progress_type)\n          if Output.continue_with_user_remind?(\"当前处于操作中间态，建议取消操作并执行\\\"mgit merge --continue\\\"继续操作未完成仓库。\\n    继续执行将清除中间态并重新操作所有仓库，是否取消？\")\n            Output.puts_cancel_message\n            return\n          end\n        end\n\n        cmd = argv.cmd\n        opts = argv.git_opts\n\n        # 优先操作配置仓库\n        config_error = rebase_config_repo(cmd, opts, config_repo, argv.opt_list.did_set_opt?(OPT_LIST[:pull]))\n        if config_error\n          Output.puts_fail_block([config_repo.name], \"主仓库操作失败：#{config_error}\")\n          return\n        end\n      end\n\n      do_repos = []\n      dirty_repos = []\n      detached_repos = []\n      no_tracking_repos = []\n\n      Output.puts_processing_message(\"检查各仓库状态...\")\n      Workspace.serial_enumerate_with_progress(all_repos) { |repo|\n        next if !config_repo.nil? && repo.name == config_repo.name\n\n        status = repo.status_checker.status\n        branch_status = repo.status_checker.branch_status\n\n        if status == Repo::Status::GIT_REPO_STATUS[:clean] &&\n          branch_status != Repo::Status::GIT_BRANCH_STATUS[:detached] &&\n          branch_status != Repo::Status::GIT_BRANCH_STATUS[:no_tracking]\n          do_repos.push(repo)\n        else\n          if status == Repo::Status::GIT_REPO_STATUS[:dirty]\n            dirty_repos.push(repo)\n          end\n          if branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]\n            detached_repos.push(repo)\n          elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]\n            no_tracking_repos.push(repo)\n          end\n        end\n      }\n      Output.puts_success_message(\"检查完成！\\n\")\n\n      if dirty_repos.length > 0 || no_tracking_repos.length > 0 || detached_repos.length > 0\n        remind_repos = []\n        remind_repos.push(['有本地改动', dirty_repos.map { |e| e.name }])\n        remind_repos.push(['未追踪远程分支(建议:mgit branch -u origin/<branch>)', no_tracking_repos.map { |e| e.name }]) if no_tracking_repos.length > 0\n        remind_repos.push(['HEAD游离,当前不在任何分支上', detached_repos.map { |e| e.name }]) if detached_repos.length > 0\n        Output.interact_with_multi_selection_combined_repos(remind_repos, \"以上仓库状态异常\", ['a: 跳过并继续', 'b: 强制执行', 'c: 终止']) { |input|\n          if input == 'b'\n            do_repos += dirty_repos\n            do_repos += detached_repos\n            do_repos += no_tracking_repos\n            do_repos.uniq! { |repo| repo.name }\n          elsif input == 'c' || input != 'a'\n            Output.puts_cancel_message\n            return\n          end\n        }\n      end\n\n      if do_repos.length == 0\n        Output.puts_remind_message(\"没有仓库需要执行rebase指令！\") if config_repo.nil?\n      else\n        Output.puts_processing_message(\"开始rebase子仓库...\")\n        _, error_repos = Workspace.execute_git_cmd_with_repos(cmd, opts, do_repos)\n      end\n\n      Output.puts_succeed_cmd(\"#{cmd} #{opts}\") if config_error.nil? || error_repos.empty?\n\n      # 清除中间态\n      OperationProgressManager.remove_progress(Workspace.root, __progress_type)\n    end\n\n    # 合并主仓库\n    #\n    # @param cmd [String] 合并指令\n    #\n    # @param opts [String] 合并参数\n    #\n    # @param repo [Repo] 配置仓库对象\n    #\n    # @param exec_repos [Array<Repo>] 本次操作的所有仓库（含配置仓库）\n    #\n    def rebase_config_repo(cmd, opts, repo, auto_update)\n      return if repo.nil?\n      branch_status = repo.status_checker.branch_status\n      if branch_status == Repo::Status::GIT_BRANCH_STATUS[:detached]\n        remind_config_repo_fail(\"主仓库\\\"#{repo.name}\\\"HEAD游离，当前不在任何分支上，无法执行!\")\n      elsif branch_status == Repo::Status::GIT_BRANCH_STATUS[:no_tracking]\n        remind_config_repo_fail(\"主仓库\\\"#{repo.name}\\\"未跟踪对应远程分支，无法执行！(需要执行'mgit branch -u origin/<branch>')\")\n      elsif repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]\n        remind_config_repo_fail(\"主仓库\\\"#{repo.name}\\\"有改动，无法执行\\\"#{cmd}\\\"!\")\n      else\n        return continue_execute(cmd, opts, repo, PROGRESS_STAGE[:new_start], auto_update)\n      end\n    end\n\n    def continue_execute(cmd, opts, repo, check_point, auto_update)\n\n      # 现场信息\n      exec_subrepos = all_repos(except_config:true)\n      is_all = Workspace.is_all_exec_sub_repos?(exec_subrepos)\n      context = OperationProgressContext.new(__progress_type)\n      context.cmd = cmd\n      context.opts = opts\n      context.repos = is_all ? nil : exec_subrepos.map { |e| e.name } # nil表示操作所有子仓库\n      context.branch = repo.status_checker.current_branch(use_cache:true)\n\n      # 更新主仓库\n      update_config_repo(repo, context, auto_update) if check_point < PROGRESS_STAGE[:did_pull_config]\n\n      if check_point < PROGRESS_STAGE[:did_refresh_config]\n        # 操作主仓库\n        config_error = exec_config_repo(repo, cmd, opts)\n        return config_error if config_error\n        # 更新配置表\n        refresh_config(repo, context, auto_update)\n      end\n\n      if check_point < PROGRESS_STAGE[:did_pull_sub]\n        # 如果本次操作所有子仓库，则再次获取所有子仓库（因为配置表可能已经更新，子仓库列表也有更新，此处获取的仓库包含：已有的子仓库 + 合并后新下载仓库 + 从缓存弹出的仓库）\n        exec_subrepos = all_repos(except_config:true) if context.repos.nil?\n        # 更新子仓库\n        update_subrepos(exec_subrepos, context, auto_update)\n      end\n\n      config_error\n    end\n\n    def update_config_repo(repo, context, auto)\n      if auto || Output.continue_with_user_remind?(\"即将合并主仓库，是否先拉取远程代码更新？\")\n        Output.puts_processing_message(\"正在更新主仓库...\")\n        success, output = repo.execute_git_cmd('pull', '')\n        if !success\n          context.other = {\n            PROGRESS_STAGE_KEY => PROGRESS_STAGE[:did_pull_config],\n            PROGRESS_AUTO => auto\n          }\n          OperationProgressManager.trap_into_progress(Workspace.root, context)\n          show_progress_error(\"主仓库更新失败\", \"#{output}\")\n        else\n          Output.puts_success_message(\"更新成功！\\n\")\n        end\n      end\n    end\n\n    def exec_config_repo(repo, cmd, opts)\n      error = nil\n      Output.puts_processing_message(\"开始操作主仓库...\")\n      success, output = repo.execute_git_cmd(cmd, opts)\n      if success\n        Output.puts_success_message(\"操作成功！\\n\")\n      else\n        Output.puts_fail_message(\"操作失败！\\n\")\n        error = output\n      end\n      error\n    end\n\n    # 刷新配置表\n    def refresh_config(repo, context, auto)\n      begin\n        Workspace.update_config(strict_mode:false) { |missing_repos|\n          if missing_repos.length > 0\n            # 这里分支引导仅根据主仓库来进行，如果使用all_repos来作为引导\n            # 基准，可能不准确(因为all_repos可能包含merge分支已有的本地\n            # 仓库，而这些仓库所在分支可能五花八门，数量也可能多于处于正确\n            # 分支的仓库)。\n            success_missing_repos = Workspace.guide_to_checkout_branch(missing_repos, [repo], append_message:\"拒绝该操作本次执行将忽略以上仓库\")\n            all_repos.concat(success_missing_repos)\n            # success_missing_repos包含新下载的和当前分支已有的新仓库，其中已有仓库包含在@all_repos内，需要去重\n            all_repos.uniq! { |repo| repo.name }\n          end\n        }\n        refresh_context(context)\n      rescue Error => e\n        if e.type == MGIT_ERROR_TYPE[:config_generate_error]\n          context.other = {\n            PROGRESS_STAGE_KEY => PROGRESS_STAGE[:did_refresh_config],\n            PROGRESS_AUTO => auto\n          }\n\n          OperationProgressManager.trap_into_progress(Workspace.root, context)\n          show_progress_error(\"配置表生成失败\", \"#{e.msg}\")\n        end\n      end\n    end\n\n    def update_subrepos(subrepos, context, auto)\n      if auto || Output.continue_with_user_remind?(\"即将合并子仓库，是否先拉取远程代码更新？\")\n        Output.puts_processing_message(\"正在更新子仓库...\")\n        _, error_repos = Workspace.execute_git_cmd_with_repos('pull', '', subrepos)\n        if error_repos.length > 0\n          context.other = {\n            PROGRESS_STAGE_KEY => PROGRESS_STAGE[:did_pull_sub],\n            PROGRESS_AUTO => auto\n          }\n          OperationProgressManager.trap_into_progress(Workspace.root, context)\n          show_progress_error(\"子仓库更新失败\", \"见上述输出\")\n        else\n          Output.puts_success_message(\"更新成功！\\n\")\n        end\n      end\n    end\n\n    def refresh_context(context)\n      exec_subrepos = all_repos(except_config:true)\n      is_all = Workspace.is_all_exec_sub_repos?(exec_subrepos)\n      context.repos = is_all ? nil : exec_subrepos.map { |e| e.name } # nil表示操作所有子仓库\n    end\n\n    def remind_config_repo_fail(msg)\n      Output.puts_fail_message(msg)\n      return if Output.continue_with_user_remind?(\"是否继续操作其余仓库？\")\n      Output.puts_cancel_message\n      exit\n    end\n\n    def check_master_rebase(argv)\n      opt_arr = argv.git_opts(raw:false)\n      opt_arr.each { |opts|\n        Foundation.help!(\"当前版本不支持\\\"-i\\\"或\\\"--interactive\\\"参数，请重试。\") if ['-i','--interactive'].include?(opts.first)\n      }\n    end\n\n    def do_abort(argv)\n      return false unless argv.git_opts.include?('--abort')\n      Output.puts_start_cmd\n      OperationProgressManager.remove_progress(Workspace.root, __progress_type)\n      do_repos = all_repos.select { |repo| repo.status_checker.is_in_rebase_progress? }\n\n      if do_repos.length > 0\n        append_message = \"，另有#{all_repos.length - do_repos.length}个仓库无须操作\" if do_repos.length < all_repos.length\n        Output.puts_processing_block(do_repos.map { |e| e.name }, \"开始操作以上仓库#{append_message}...\")\n        _, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, do_repos)\n        Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0\n      else\n        Output.puts_success_message(\"没有仓库需要操作！\")\n      end\n\n      true\n    end\n\n    def show_progress_error(summary, detail)\n      error = \"#{summary} 已进入操作中间态。\n\n原因：\n  #{detail}\n\n可选：\n  - 使用\\\"mgit rebase --continue\\\"继续变基。\n  - 使用\\\"mgit rebase --abort\\\"取消变基。\"\n      Foundation.help!(error, title:'暂停')\n    end\n\n    def enable_repo_selection\n      true\n    end\n\n    def enable_continue_operation\n      true\n    end\n\n    def self.description\n      \"重新将提交应用到其他基点，该命令不执行lock的仓库。\"\n    end\n\n    def self.usage\n      \"mgit rebase [<git-rebase-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]\\nmgit rebase --continue\\nmgit rebase --abort\"\n    end\n\n    private\n\n    def __progress_type\n      OperationProgressManager::PROGRESS_TYPE[:rebase]\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/reset.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope 类似 git reset\n  #\n  class Reset < BaseCommand\n\n    def execute(argv)\n      Workspace.check_branch_consistency\n\n      Output.puts_start_cmd\n      _, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, all_repos)\n      Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0\n    end\n\n    def enable_repo_selection\n      true\n    end\n\n    def self.description\n      \"将当前HEAD指针还原到指定状态。\"\n    end\n\n    def self.usage\n      \"mgit reset [<git-reset-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/self.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # 该指令用于不带指令的输入：mgit --help，用于执行mgit的一级参数（如\"mgit --help\"的\"--help\"）\n  #\n  class Self < BaseCommand\n\n    HELP_INTRODUCTION = <<INTRO\n#{Output.info_title(\"Description:\")}\n\n     mgit是多仓库管理工具，通过将git指令作用到多个仓库，实现批量的版本管理功能\n\n     更多介绍：https://github.com/baidu/mgit\n\nINTRO\n\n    OPT_LIST = {\n      :all            =>  '--all',\n      :all_s          =>  '-a',\n      :list           =>  '--list',\n      :list_s         =>  '-l',\n      :size           =>  '--size',\n      :size_s         =>  '-s',\n      :version        =>  '--version',\n      :version_s      =>  '-v',\n      :workspace      =>  '--workspace',\n      :workspace_s    =>  '-w'\n    }.freeze\n\n    def options\n      return [\n          ARGV::Opt.new(OPT_LIST[:list], short_key:OPT_LIST[:list_s], info:\"显示MGit管理的仓库。\", type: :boolean),\n          ARGV::Opt.new(OPT_LIST[:size], short_key:OPT_LIST[:size_s], info:\"显示MGit管理的仓库的磁盘占用量。\", type: :boolean),\n          ARGV::Opt.new(OPT_LIST[:all], short_key:OPT_LIST[:all_s], info:\"指定操作所有定义在manifest内的仓库，可配合-l合并使用: \\\"mgit -al\\\"。\", type: :boolean),\n          ARGV::Opt.new(OPT_LIST[:version], short_key:OPT_LIST[:version_s], info:\"显示当前MGit版本。\", type: :boolean),\n          ARGV::Opt.new(OPT_LIST[:workspace], short_key:OPT_LIST[:workspace_s], info:\"显示当前MGit工程管理根目录(.mgit所在目录)。\", type: :boolean)\n      ].concat(super)\n    end\n\n    def validate(argv)\n      Foundation.help!(\"输入非法参数：#{argv.git_opts}。请通过\\\"mgit --help\\\"查看用法。\") if argv.git_opts.length > 0\n    end\n\n    def prepare\n      Workspace.setup_multi_repo_root\n      Workspace.setup_config\n    end\n\n    # --- 覆写，不需要预设操作 ---\n    def pre_exec\n    end\n\n    def post_exec\n    end\n\n    def usage(argv)\n      show_help(argv)\n    end\n    # -------------------------\n\n    def execute(argv)\n      # 如果存在多余（透传）指令则报错\n      extra_opt_str = argv.git_opts\n      if extra_opt_str.length > 0\n        Output.puts_fail_message(\"输入无效参数：#{extra_opt_str}\\n\")\n        show_help(argv)\n        exit\n      end\n\n      argv.enumerate_valid_opts { |opt|\n        if opt.key == OPT_LIST[:list] || opt.key == OPT_LIST[:list_s]\n          show_all_repos(argv)\n          return\n        elsif opt.key == OPT_LIST[:size] || opt.key == OPT_LIST[:size_s]\n          show_repo_size\n          return\n        elsif opt.key == OPT_LIST[:version] || opt.key == OPT_LIST[:version_s]\n          show_version\n          return\n        elsif opt.key == OPT_LIST[:workspace] || opt.key == OPT_LIST[:workspace_s]\n          show_workspace\n          return\n        end\n      }\n\n      # 无任何参数传入时显示帮助\n      show_help(argv)\n    end\n\n    def show_help(argv)\n      head_space = '    '\n      middle_space = '   '\n\n      output =  HELP_INTRODUCTION # \"#{Output.info_title(\"Description:\")}\\n\\n#{head_space}mgit是多仓库管理工具，通过将git指令作用到多个仓库，实现批量的版本管理功能。\"\n      output += \"#{Output.info_title(\"Usage:\")}\\n\\n#{head_space}$ #{Output.green_message(\"mgit <mgit_options>\")}\\n\"\n      output += \"#{head_space}$ #{Output.green_message(\"mgit <command> [<command_option>...] [<value>...]\")}\\n\\n\"\n\n      # mgit options\n      output += \"#{Output.info_title(\"MGit Option:\")}\\n\\n\"\n      divider = \", \"\n      longest_opt = argv.opt_list.opts.max_by { |e| \"#{Output.blue_message(\"#{e.key}#{divider + e.short_key if !e.short_key.nil?}\")}\".length }\n      max_opt_length = \"#{longest_opt.short_key + divider + longest_opt.key}\".length\n      mgit_option_info = ''\n      argv.opt_list.opts.each { |opt|\n        key = \"#{opt.short_key + divider + opt.key}\"\n        mgit_option_info += \"#{head_space}#{Output.blue_message(key)}#{' ' * (max_opt_length - key.length + middle_space.length)}#{argv.info(opt.key)}\\n\"\n      }\n      output += mgit_option_info + \"\\n\"\n\n      # subcommand\n      output += \"#{Output.info_title(\"Command:\")}\\n\\n\"\n      cmd_header = '+ '\n      cmd_info = ''\n\n      max_cmd_length = Output.blue_message(cmd_header + CommandManager.commands.keys.max_by { |e| e.length }.to_s).length\n      CommandManager.commands.keys.sort.each { |cmd_name|\n        next if cmd_name == self.class.cmd\n        cls_name = CommandManager.commands[cmd_name]\n        cmd_name = Output.green_message(cmd_header + cmd_name.downcase.to_s)\n        cmd_info += \"#{head_space}#{cmd_name}#{' ' * (max_cmd_length - cmd_name.length + middle_space.length)}#{cls_name.description}\\n\"\n      }\n      output += cmd_info + \"\\n\"\n      output += \"#{Output.info_title(\"Command Option:\")}\\n\\n#{head_space}请通过[ mgit <command> --help ]查看。\\n\\n\"\n      output += \"#{Output.info_title(\"Version:\")}\\n\\n#{head_space}#{MGit::VERSION}\\n\"\n\n      puts output\n    end\n\n    def show_all_repos(argv)\n      prepare\n\n      list_all = argv.opt_list.did_set_opt?(OPT_LIST[:all])\n      existing_repos, missing_repos = prepare_repos(with_excluded:list_all)\n\n      list = {}\n      if existing_repos.length > 0\n        existing_repos.sort_by { |e| e.name }.each { |light_repo|\n          dir = File.join(\"<ROOT>\", File.dirname(light_repo.path)).bold\n          list[dir] = [] if list[dir].nil?\n          list[dir].push(light_repo.name)\n        }\n      end\n\n      if missing_repos.length > 0\n        list['本地缺失'.bold] = missing_repos.sort_by { |e| e.name }.map { |e| e.name }\n      end\n\n      list_array = []\n      list.each { |dir, list|\n        list_array.push([dir.bold, list])\n      }\n\n      puts Output.generate_table_combination(list_array, separator:'|')\n      if list_all\n        message = \"共统计#{existing_repos.length + missing_repos.length}个仓库。\"\n      else\n        message = \"mgit目前共管理#{existing_repos.length + missing_repos.length}个仓库。\"\n      end\n      Output.puts_remind_message(message)\n      Output.puts_fail_message(\"有#{missing_repos.length}个仓库本地缺失!\") if missing_repos.length > 0\n    end\n\n    def show_repo_size\n      prepare\n      Workspace.setup_all_repos\n      Output.puts_processing_message(\"开始计算...\")\n      repo_size = {}\n      mutex = Mutex.new\n      task_count = 0\n      Output.update_progress(all_repos.length, task_count)\n      Workspace.concurrent_enumerate(all_repos) { |repo|\n        success, output = repo.execute(\"du -sh #{repo.path}\")\n        mutex.lock\n        if success\n          repo_size[repo.name] = output&.strip\n        end\n        task_count += 1\n        Output.update_progress(all_repos.length, task_count)\n        mutex.unlock\n      }\n      Output.puts_success_message(\"计算完成。\")\n\n      display_size = repo_size.sort_by { |k,v| k}.map { |e| e.last }\n      Output.puts_remind_block(display_size, \"共统计#{repo_size.length}个仓库。\")\n    end\n\n    def prepare_repos(with_excluded:false)\n      existing_repos = []\n      missing_repos = []\n      Workspace.config.light_repos.each { |light_repo|\n        if with_excluded || !light_repo.mgit_excluded\n          repo_exist = Repo.is_git_repo?(light_repo.abs_dest(Workspace.root))\n          if repo_exist\n            existing_repos.push(light_repo)\n          else\n            missing_repos.push(light_repo)\n          end\n        end\n      }\n      return existing_repos, missing_repos\n    end\n\n    def show_workspace\n      root = Workspace.multi_repo_root_path\n      if root.nil?\n        Output.puts_fail_message(\"当前不在任何多仓库目录下！\")\n      else\n        puts root\n      end\n    end\n\n    def show_version\n      puts MGit::VERSION\n    end\n\n    def enable_short_basic_option\n      return true\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/stash.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope 类似 git stash，但是强制标记名称\n  #\n  class Stash < BaseCommand\n\n    OPT_LIST = {\n      :push     => '--push',\n      :pop      => '--pop',\n      :apply    => '--apply',\n      :list     => '--list',\n      :clear    => '--clear',\n    }.freeze\n\n    def options\n      return [\n          ARGV::Opt.new(OPT_LIST[:push], info:'添加储藏：mgit stash --push \"stash_name\"。', type: :string),\n          ARGV::Opt.new(OPT_LIST[:pop], info:'恢复储藏：mgit stash --pop \"stash_name\"。', type: :string),\n          ARGV::Opt.new(OPT_LIST[:apply], info:'恢复储藏：mgit stash --apply \"stash_name\"。', type: :string),\n          ARGV::Opt.new(OPT_LIST[:list], info:'显示储藏列表。', type: :boolean),\n          ARGV::Opt.new(OPT_LIST[:clear], info:'清空所有储藏。', type: :boolean)\n      ].concat(super)\n    end\n\n    def validate(argv)\n      missing_msg = \"缺失必要参数\"  if argv.raw_opts.length == 0\n      illegal_msg = \"输入非法参数：#{argv.git_opts}\" if argv.git_opts.length > 0\n      conjunction = \"，同时\" if !missing_msg.nil? && !illegal_msg.nil?\n      Foundation.help!(\"#{missing_msg}#{conjunction}#{illegal_msg}，请通过\\\"mgit #{argv.cmd} --help\\\"查看用法。\") if !missing_msg.nil? || !illegal_msg.nil?\n    end\n\n    # 注意：git stash相关命令不支持指定“working tree”和“git dir”\n    # 如：git --git-dir=/path/to/.git --work-tree=/path/to/working-tree stash list，若当前不在working tree目录下，则将无法执行。\n    def execute(argv)\n      argv.enumerate_valid_opts { |opt|\n        if opt.key == OPT_LIST[:push]\n          do_stash_push(argv, opt.value)\n          break\n        elsif opt.key == OPT_LIST[:pop] || opt.key == OPT_LIST[:apply]\n          action = opt.key.gsub('--', '')\n          do_stash_pop_apply(argv, opt.value, action)\n          break\n        elsif opt.key == OPT_LIST[:list]\n          do_stash_list(argv)\n        elsif opt.key == OPT_LIST[:clear]\n          do_clear(argv)\n        end\n      }\n    end\n\n    def do_clear(argv)\n      if Output.continue_with_user_remind?(\"该操作会丢失所有的stash，确定要执行吗？\")\n        Output.puts_start_cmd\n        abs_cmd = \"git stash clear\"\n        _, error_repos = Workspace.execute_common_cmd_with_repos(abs_cmd, all_repos)\n        Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0\n      else\n        Output.puts_cancel_message\n      end\n    end\n\n    def do_stash_list(argv)\n      Output.puts_start_cmd\n\n      error_repos = {}\n      all_repos.each { |repo|\n        cmd = \"git -C \\\"#{repo.path}\\\" stash list\"\n        success, output = repo.execute(cmd)\n        if success\n          puts Output.generate_title_block(repo.name) {\n            output\n          } + \"\\n\" if output.length > 0\n        else\n          error_repos[repo.name] = output\n        end\n      }\n      if error_repos.length > 0\n        Workspace.show_error(error_repos)\n      else\n        Output.puts_succeed_cmd(argv.absolute_cmd)\n      end\n    end\n\n    def do_stash_pop_apply(argv, stash_name, action)\n      do_repos = []\n      mutex = Mutex.new\n      Output.puts_processing_message(\"检查仓库状态...\")\n      all_repos.each { |repo|\n        stash_list = repo_stash_list_msg(repo)\n        next if stash_list.nil?\n\n        stash_id = stash_include_name(stash_list, stash_name)\n        next if stash_id.nil?\n        mutex.lock\n        do_repos.push(repo)\n        mutex.unlock\n      }\n      Output.puts_success_message(\"检查完成！\\n\")\n\n      Output.puts_start_cmd\n      if do_repos.length == 0\n        Output.puts_nothing_to_do_cmd\n      else\n        _, error_repos = Workspace.execute_common_cmd_with_repos('', do_repos) { |repo|\n          stash_list = repo_stash_list_msg(repo)\n          stash_id = stash_include_name(stash_list, stash_name)\n          \"git stash #{action} #{stash_id}\"\n        }\n        Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0\n      end\n    end\n\n    def do_stash_push(argv, stash_name)\n      do_repos = []\n      remind_repos = []\n      mutex = Mutex.new\n      Output.puts_processing_message(\"检查仓库状态...\")\n      all_repos.each { |repo|\n        next if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:clean]\n        next if repo.status_checker.dirty_zone == Repo::Status::GIT_REPO_STATUS_DIRTY_ZONE[:special]\n\n        stash_list = repo_stash_list_msg(repo)\n        stash_id = stash_include_name(stash_list, stash_name)\n        mutex.lock\n        if stash_id.nil?\n          do_repos.push(repo)\n        else\n          remind_repos.push(repo.name)\n        end\n        mutex.unlock\n      }\n      Output.puts_success_message(\"检查完成！\\n\")\n\n      if remind_repos.length > 0\n        Output.puts_remind_block(remind_repos, \"以上仓库当前分支已经存在stash名称：#{stash_name}，请换一个名称或者使用\\\"mgit stash --list\\\"查看详情。\")\n        Output.puts_fail_cmd(argv.absolute_cmd)\n      elsif do_repos.empty?\n        Output.puts_remind_message(\"所有仓库均是clean状态或者文件未跟踪，无需执行\")\n      else\n        Output.puts_start_cmd\n        abs_cmd = \"git stash save -u #{stash_name}\"\n        _, error_repos = Workspace.execute_common_cmd_with_repos(abs_cmd, do_repos)\n        Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0\n      end\n    end\n\n    # 获取当前的 stash list 字符串,nil,标识当前没有stash\n    def repo_stash_list_msg(repo)\n      success, output = repo.execute(\"git -C \\\"#{repo.path}\\\" stash list\")\n      return output if success && output.length > 0\n    end\n\n    # 查询stash_list 是否包含某一个保存的stash\n    # 不做分支判断，因为在保存的stashlist中，分支只保留了/之后的内容\n    #\n    def stash_include_name(stash_list, stash_name)\n      return if stash_list.nil?\n\n      stash_list_array = stash_list.split(\"\\n\")\n\n      find_stash_id = nil\n      stash_list_array.each do |line|\n        regex = /(stash@{\\d+}):.*:\\s(.*)$/\n        next unless line.match(regex)\n        match_stash_name = $2\n        next unless match_stash_name == stash_name\n        find_stash_id = $1\n        break\n      end\n      find_stash_id\n    end\n\n    def enable_repo_selection\n      true\n    end\n\n    def self.description\n      \"使用git stash将当前工作区改动暂时存放起来。\"\n    end\n\n    def self.usage\n      \"mgit stash [<option> <value>...] [(--mrepo|--el-mrepo) <repo>...] [--help]\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/status.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope 类似 git status\n  #\n  class Status < BaseCommand\n    def execute(argv)\n      Output.puts_processing_message(\"正在检查各仓库状态...\")\n\n      status_info = {}\n      mutex = Mutex.new\n      mutex_branch = Mutex.new\n      mutex_modification = Mutex.new\n      mutex_other = Mutex.new\n\n      branch_notice = []\n      modification_notice = []\n      other_notice = []\n      in_progress_notice = []\n\n      task_count = 0\n      repo_combo = all_repos + locked_repos\n      Output.update_progress(repo_combo.length, task_count)\n      Workspace.concurrent_enumerate(repo_combo) { |repo|\n        status_msg = ''\n        info = []\n\n        # 非锁定仓库进行常规分支检查\n        if !locked_repos.include?(repo) &&\n          repo.status_checker.branch_status != Repo::Status::GIT_BRANCH_STATUS[:up_to_date] &&\n          repo.status_checker.branch_status != Repo::Status::GIT_BRANCH_STATUS[:no_remote]\n            info.push(['分支', [repo.status_checker.branch_message]])\n\n            mutex_branch.lock\n            branch_notice.push(repo.name)\n            mutex_branch.unlock\n        end\n\n        # 检查工作区状态\n        if repo.status_checker.status != Repo::Status::GIT_REPO_STATUS[:clean]\n          info += repo.status_checker.message\n          mutex_modification.lock\n          modification_notice.push(repo.name)\n          mutex_modification.unlock\n        end\n\n        # 检查url是否一致\n        if !repo.url_consist?\n          info.push(['其他', ['仓库实际url与当前配置不一致']])\n          mutex_other.lock\n          other_notice.push(repo.name)\n          mutex_other.unlock\n        end\n\n        # 生成表格\n        status_msg = Output.generate_table_combination(info) + \"\\n\\n\" if info.length > 0\n\n        # 压缩状态信息\n        mutex.lock\n        if status_msg.length > 0\n          status_info[status_msg] = {'repo_names' => [], 'info' => info} if status_info[status_msg].nil?\n          status_info[status_msg]['repo_names'].push(repo.name)\n        end\n        task_count += 1\n        Output.update_progress(repo_combo.length, task_count)\n        mutex.unlock\n      }\n\n      status_info.each_with_index { |(status_msg, item), index|\n        info = item['info']\n        repo_names = item['repo_names']\n        Output.puts_remind_block(repo_names, \"以上仓库状态：\")\n        MGit::Loger.info(info)\n        status_msg += \"\\n\" if index != status_info.length - 1\n        puts status_msg\n      }\n\n      OperationProgressManager::PROGRESS_TYPE.each { |type, type_str|\n        if OperationProgressManager.is_in_progress?(Workspace.root, type_str)\n          in_progress_notice.push(type.to_s)\n        end\n      }\n\n      summary = []\n      if branch_notice.length > 0\n        summary.push([\"分支提醒(#{branch_notice.length})\",branch_notice])\n      end\n\n      if modification_notice.length > 0\n        summary.push([\"改动提醒(#{modification_notice.length})\",modification_notice])\n      end\n\n      if other_notice.length > 0\n        summary.push([\"其他警告(#{other_notice.length})\", other_notice])\n      end\n\n      if in_progress_notice.length > 0\n        summary.push([\"处于中间态的操作\",in_progress_notice])\n      end\n\n      if summary.length > 0\n        puts \"\\n\"\n        puts Output.generate_table_combination(summary, title: \"状态小结\", separator: \"|\")\n        MGit::Loger.info('状态小结')\n        MGit::Loger.info(summary)\n      end\n\n      Output.puts_success_message(\"所查询仓库均无改动！\") if status_info.keys.length == 0\n\n      Output.puts_succeed_cmd(argv.absolute_cmd)\n    end\n\n    def validate(argv)\n      Foundation.help!(\"输入非法参数：#{argv.git_opts}。请通过\\\"mgit #{argv.cmd} --help\\\"查看用法。\") if argv.git_opts.length > 0\n    end\n\n    def enable_repo_selection\n      true\n    end\n\n    def enable_short_basic_option\n      true\n    end\n\n    def self.description\n      \"输出所有仓库的状态。包括：\\\"分支\\\"，\\\"暂存区\\\"，\\\"工作区\\\"，\\\"特殊（未跟踪和被忽略）\\\"，\\\"冲突\\\"。\"\n    end\n\n    def self.usage\n      \"mgit status [(-m|-e) <repo>...] [-h]\"\n    end\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/sync.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope 同步所有管理的仓库到工作区，可能从远端拉取，可能从本地dump，所以sync后的工作区不一定是分支的最新节点\n  # 可通过 mgit sync --pull 进行同步后pull到最新节点\n  #\n  class Sync < BaseCommand\n\n    OPT_LIST = {\n      :new_repo    =>  '--new-repo',\n      :new_repo_s  =>  '-n',\n      :all         =>  '--all',\n      :all_s       =>  '-a',\n      :clone       =>  '--clone',\n      :clone_s     =>  '-c',\n      :pull        =>  '--pull',\n      :pull_s      =>  '-p',\n      :url         =>  '--url',\n      :url_s       =>  '-u',\n    }.freeze\n\n    # --- 覆写前后hook，不需要预设操作 ---\n    def pre_exec\n      MGit::DurationRecorder.start\n      Workspace.setup_multi_repo_root\n      # 配置log\n      MGit::Loger.config(Workspace.root)\n      MGit::Loger.info(\"~~~ #{@argv.absolute_cmd} ~~~\")\n      Workspace.setup_config\n    end\n\n    def post_exec\n      # 打点结束\n      duration = MGit::DurationRecorder.end\n      MGit::Loger.info(\"~~~ #{@argv.absolute_cmd}, 耗时：#{duration} s ~~~\")\n    end\n    # --------------------------------\n\n    def options\n      return [\n          ARGV::Opt.new(OPT_LIST[:new_repo],\n                        short_key:OPT_LIST[:new_repo_s],\n                        info:\"下载配置表中指定被mgit管理，但本地不存在的仓库，已有仓库不做任何处理，使用：mgit sync -n。\",\n                        type: :boolean),\n          ARGV::Opt.new(OPT_LIST[:all],\n                        short_key:OPT_LIST[:all_s],\n                        info:'对所有(包含不被mgit管理的)仓库操作:1.如果本地缺失则下载。2.如果本地存在且被锁定则同步到锁定状态。注意，如果需要下载代码，需要配置仓库URL，否则跳过，使用：mgit sync -a。',\n                        type: :boolean),\n          ARGV::Opt.new(OPT_LIST[:clone], short_key:OPT_LIST[:clone_s], info:'下载一组仓库(包含不被mgit管理的仓库)，如: mgit sync -c repo1 repo2。'),\n          ARGV::Opt.new(OPT_LIST[:pull],\n                        short_key:OPT_LIST[:pull_s],\n                        info:'同步本地仓库后执行pull操作更新，配合其他指令使用，如: mgit sync -a -p。',\n                        type: :boolean),\n          ARGV::Opt.new(OPT_LIST[:url],\n                        short_key:OPT_LIST[:url_s],\n                        info:'校验并同步URL与配置不一致的仓库，如: mgit sync -u。',\n                        type: :boolean)\n      ].concat(super)\n    end\n\n    def validate(argv)\n      opt_all = argv.opt(OPT_LIST[:all])\n      opt_clone = argv.opt(OPT_LIST[:clone])\n      opt_new = argv.opt(OPT_LIST[:new_repo])\n      invalid = [opt_all, opt_clone, opt_new].select { |e| !e.nil? }.length > 1\n      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\n    end\n\n    def execute(argv)\n      Output.puts_start_cmd\n\n      if argv.opt_list.did_set_opt?(OPT_LIST[:all])\n        setup_all_sync_reops(argv)\n      elsif argv.opt_list.did_set_opt?(OPT_LIST[:new_repo])\n        setup_new_reops\n      elsif argv.opt_list.did_set_opt?(OPT_LIST[:url])\n        setup_config_url_repos\n      elsif argv.opt_list.did_set_opt?(OPT_LIST[:clone])\n        return if !setup_download_reops(argv.opt(OPT_LIST[:clone]).value)\n      elsif argv.git_opts.length > 0\n        return if !setup_specified_repos(argv)\n      else\n        setup_normal_reops(argv)\n      end\n\n      if (@sync_repos.length + @update_repos.length + @download_repos.length) == 0\n        Output.puts_success_message(\"没有仓库需要同步！\")\n        return\n      end\n\n      error_repos = {}\n      if @sync_repos.length > 0\n        Workspace.concurrent_enumerate_with_progress_bar(@sync_repos, \"正在同步(锁定)以上仓库...\") { |light_repo|\n          repo = Repo.generate_strictly(Workspace.root, light_repo)\n          error_message = Repo::SyncHelper.sync_exist_repo(repo, light_repo)\n          if !error_message.nil?\n            Lock.mutex_exec { error_repos[light_repo.name] = error_message }\n          end\n        }\n      end\n\n      if @update_repos.length > 0\n        Workspace.concurrent_enumerate_with_progress_bar(@update_repos, \"正在更新以上仓库...\") { |light_repo|\n          repo = Repo.generate_strictly(Workspace.root, light_repo)\n          success, output = repo.execute_git_cmd('pull', '')\n          if !success\n            Lock.mutex_exec { error_repos[light_repo.name] = output }\n          end\n        }\n      end\n\n      if @download_repos.length > 0\n        Workspace.sync_new_repos(@download_repos)\n      end\n\n      if error_repos.length > 0\n        Workspace.show_error(error_repos)\n      else\n        Output.puts_succeed_cmd(argv.absolute_cmd)\n      end\n    end\n\n    def prepare_repo_category\n      @sync_repos = []\n      @update_repos = []\n      @download_repos = []\n    end\n\n    # M/NM: 被/不被mgit管理\n    # E/NE: 仓库本地存在/不存在\n    # L/NL: 仓库被/不被锁定\n    # U/NU: 仓库有/没有远程地址\n\n    # mgit sync repo1 repo2 ... [--pull]\n    # 针对一组指定仓库进行：下载仓库：NE|U，同步仓库：E|L，更新仓库: E|U（可选）\n    def setup_specified_repos(argv)\n      prepare_repo_category\n      should_pull = argv.opt_list.did_set_opt?(OPT_LIST[:pull])\n      valid_repos, error_repo_names = check_valid_repos(parse_repo_name(argv))\n      valid_repos.each { |light_repo|\n        repo_exist = Dir.exist?(light_repo.abs_dest(Workspace.root))\n        if !repo_exist\n          if !light_repo.url.nil?\n            @download_repos.push(light_repo)\n          else\n            error_repo_names.push(light_repo.name)\n          end\n        else\n          if light_repo.lock\n            @sync_repos.push(light_repo)\n          end\n\n          if should_pull\n            if !light_repo.url.nil?\n              @update_repos.push(light_repo)\n            else\n              error_repo_names.push(light_repo.name)\n            end\n          end\n        end\n      }\n\n      should_continue = true\n      error_repos = []\n      error_repos.push(['配置表中未定义(或未指定\"remote-path\")', error_repo_names]) if error_repo_names.length > 0\n      Output.interact_with_multi_selection_combined_repos(error_repos, \"以上仓库状态异常\", ['a: 跳过并继续', 'b: 终止']) { |input|\n        if input == 'b'\n          Output.puts_cancel_message\n          should_continue = false\n        end\n      } if error_repos.length > 0\n      return should_continue\n    end\n\n    # mgit sync [--pull]\n    # 下载仓库：M|NE|U, 同步仓库：M|E|L, 更新仓库：M|E|U（可选）\n    def setup_normal_reops(argv)\n      prepare_repo_category\n      should_pull = argv.opt_list.did_set_opt?(OPT_LIST[:pull])\n      Workspace.config.light_repos.each { |light_repo|\n        if !light_repo.mgit_excluded\n          repo_exist = Dir.exist?(light_repo.abs_dest(Workspace.root))\n          if !repo_exist && !light_repo.url.nil?\n            @download_repos.push(light_repo)\n          elsif repo_exist && light_repo.lock\n            @sync_repos.push(light_repo)\n          end\n\n          if repo_exist && should_pull && !light_repo.url.nil?\n              @update_repos.push(light_repo)\n          end\n        end\n      }\n    end\n\n    # mgit sync --new\n    # 下载仓库：M|NE|U\n    def setup_new_reops\n      prepare_repo_category\n      Workspace.config.light_repos.each { |light_repo|\n        if !light_repo.mgit_excluded\n          repo_exist = Dir.exist?(light_repo.abs_dest(Workspace.root))\n          @download_repos.push(light_repo) if !repo_exist && !light_repo.url.nil?\n        end\n      }\n    end\n\n    # mgit sync --all [--pull]\n    # 下载仓库：NE|U，同步仓库：E|L，更新仓库: E|NL|U（可选）\n    def setup_all_sync_reops(argv)\n      prepare_repo_category\n      Workspace.config.light_repos.each { |light_repo|\n        repo_exist = Dir.exist?(light_repo.abs_dest(Workspace.root))\n        if !repo_exist && !light_repo.url.nil?\n          @download_repos.push(light_repo)\n        elsif repo_exist && light_repo.lock\n          @sync_repos.push(light_repo)\n        elsif repo_exist && !light_repo.url.nil? && argv.opt_list.did_set_opt?(OPT_LIST[:pull])\n          @update_repos.push(light_repo)\n        end\n      }\n    end\n\n    # mgit sync --clone repo1 repo2 ...\n    # 下载仓库：NE|U(指定)\n    def setup_download_reops(repo_names)\n      prepare_repo_category\n      existing_repos = []\n      valid_repos, error_repo_names = check_valid_repos(repo_names)\n\n      valid_repos.each { |light_repo|\n        repo_exist = Dir.exist?(light_repo.abs_dest(Workspace.root))\n        if !repo_exist\n          if !light_repo.url.nil?\n            @download_repos.push(light_repo)\n          else\n            error_repo_names.push(light_repo.name)\n          end\n        else\n          error_repo_names.push(light_repo.name) if light_repo.url.nil?\n          existing_repos.push(light_repo.name)\n        end\n      }\n\n      should_continue = true\n      error_repos = []\n      error_repos.push(['配置表中未定义(或未指定\"remote-path\")', error_repo_names]) if error_repo_names.length > 0\n      error_repos.push(['本地已经存在', existing_repos]) if existing_repos.length > 0\n      Output.interact_with_multi_selection_combined_repos(error_repos, \"以上仓库状态异常\", ['a: 跳过并继续', 'b: 终止']) { |input|\n        if input == 'b'\n          Output.puts_cancel_message\n          should_continue = false\n        end\n      } if error_repos.length > 0\n      return should_continue\n    end\n\n    # mgit sync -u\n    # 同步仓库：E | url不一致\n    def setup_config_url_repos()\n      prepare_repo_category\n\n      warning_repos = []\n      missing_repos = []\n      Workspace.config.light_repos.each { |light_repo|\n        if !light_repo.mgit_excluded\n          repo, _ = Repo.generate_softly(Workspace.root, light_repo)\n          if !repo.nil?\n            original_url = repo.status_checker.default_url\n            target_url = light_repo.url\n            warning_repos.push(light_repo) if !Utils.url_consist?(original_url, target_url)\n          else\n            missing_repos.push(light_repo)\n          end\n        end\n      }\n\n      Output.puts_remind_block(missing_repos.map { |repo| repo.name }, \"以上仓库本地缺失，无法校验URL，请执行\\\"mgit sync -n\\\"重新下载后重试！\") if missing_repos.length > 0\n\n      if warning_repos.length > 0\n        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    继续强制设置可能导致仓库出错，是否继续？\")\n          @sync_repos = warning_repos\n        else\n          Output.puts_cancel_message\n          exit\n        end\n      end\n    end\n\n    def check_valid_repos(repo_names)\n      specified_repos = repo_names.map { |name| name.downcase }\n      all_valid_repos = Workspace.config.light_repos.map { |light_repo| light_repo.name.downcase }\n      error_repo_names = specified_repos - all_valid_repos\n      valid_repo_names = specified_repos - error_repo_names\n      valid_repos = Workspace.config.light_repos.select { |light_repo|\n        valid_repo_names.include?(light_repo.name.downcase)\n      }\n      [valid_repos, error_repo_names]\n    end\n\n\n    def parse_repo_name(argv)\n      return if argv.git_opts.nil?\n\n      repos = argv.git_opts.split(' ')\n      extra_opts = repos.select { |e| argv.is_option?(e) }\n      Foundation.help!(\"输入非法参数：#{extra_opts.join('，')}。请通过\\\"mgit #{argv.cmd} --help\\\"查看用法。\") if extra_opts.length > 0\n      Foundation.help!(\"未输入查询仓库名！请使用这种形式查询：mgit info repo1 repo2 ...\") if repos.length == 0\n      repos.map { |e| e.downcase }\n    end\n\n    def enable_short_basic_option\n      true\n    end\n\n    def self.description\n      \"根据配置表(从远端或本地)同步仓库到工作区，包括被锁定仓库，已经在工作的不作处理（默认不执行pull）。\"\n    end\n\n    def self.usage\n      \"mgit sync [-a|-n|-c] [<repo>...] [-p] [-o] [-h]\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command/tag.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # @!scope 类似 git tag\n  #\n  class Tag < BaseCommand\n\n    def execute(argv)\n      Workspace.check_branch_consistency\n\n      Output.puts_start_cmd\n\n      exec_repos = all_repos + locked_repos\n\n      if argv.git_opts.empty?\n        _, error_repos = Workspace.execute_git_cmd_with_repos(argv.cmd, argv.git_opts, exec_repos)\n        Output.puts_succeed_cmd(argv.absolute_cmd) if error_repos.length == 0\n        return\n      end\n\n      error_repos = {}\n      no_tag_repos = []\n      exec_repos.each { |repo|\n        success, output = repo.execute_git_cmd(argv.cmd, argv.git_opts)\n        if success\n          tags = output.split(\"\\n\")\n          if tags.length > 0\n            puts Output.generate_title_block(repo.name) {\n              Output.generate_table(tags, separator:\"\")\n            }\n          else\n            no_tag_repos.push(repo.name)\n          end\n        else\n          error_repos[repo.name] = output\n        end\n      }\n\n      if no_tag_repos.length > 0\n        puts \"\\n\"\n        Output.puts_remind_block(no_tag_repos, \"以上仓库尚未创建tag！\")\n      end\n\n      if error_repos.length > 0\n        Workspace.show_error(error_repos)\n      else\n        Output.puts_succeed_cmd(argv.absolute_cmd)\n      end\n\n    end\n\n    def enable_repo_selection\n      true\n    end\n\n    def self.description\n      \"增删查或验证标签。增加标签示例：mgit tag -a 'v0.0.1' -m 'Tag description message'\"\n    end\n\n    def self.usage\n      \"mgit tag [<git-tag-option>] [(--mrepo|--el-mrepo) <repo>...] [--help]\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/command_manager.rb",
    "content": "\nmodule MGit\n  class CommandManager\n    class << self\n      # cmd generate\n      #\n      def commands\n        @commands ||= {}\n      end\n\n      def register_command(cmd, cls)\n        commands[cmd] = cls\n      end\n\n      def [](cmd)\n        class_with_command(cmd)\n      end\n\n      def class_with_command(cmd)\n        commands[cmd]\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/error.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  MGIT_ERROR_TYPE = {\n    :config_name_error => 'config_name_error',\n    :config_generate_error => 'config_generate_error'\n  }.freeze\n\n  class Error < StandardError\n    attr_reader :msg\n    attr_reader :type\n\n    def initialize(msg, type:nil)\n      @msg = msg\n      @type = type\n    end\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/foundation/constants.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  module Constants\n    PROJECT_DIR = {\n        :hooks          => '.mgit/hooks',\n        :source_config  => '.mgit/source-config',\n        :source_git     => '.mgit/source-git',\n        :snapshot       => '.mgit/snapshot',\n        :log_dir        => '.mgit/logs'\n    }.freeze\n\n    HOOK_NAME = {\n        :pre_hook            => 'pre_hook.rb',\n        :post_hook           => 'post_hook.rb',\n        :manifest_hook       => 'manifest_hook.rb',\n        :post_download_hook  => 'post_download_hook.rb',\n        :pre_push_hook       => 'pre_push_hook.rb',\n        :pre_exec_hook       => 'pre_exec_hook.rb'\n    }.freeze\n\n    MGIT_CONFIG_PATH = \".mgit/config.yml\"\n\n    CONFIG_FILE_NAME = {\n        :manifest       => 'manifest.json',\n        :manifest_cache => '.manifest_cache.json',\n        :local_manifest => 'local_manifest.json'\n    }.freeze\n\n    # 全局配置\n    CONFIG_KEY = {\n        # [String] 包含仓库的文件夹相对.mgit目录路径，完整路径如：<.mgit所在路径>/dest/<repo_name>，与abs-dest同时指定时无效\n        :dest           => 'dest',\n        # [Boolean] 是否将所有仓库排除mgit管理\n        :mgit_excluded  => 'mgit-excluded',\n        # [String] 远程仓库根目录，完整URL：remote/remote_path\n        :remote         => 'remote',\n        # [String] 远程仓库相对目录，完整URL：remote/remote_path\n        :repositories   => 'repositories',\n        # [String] mgit版本\n        :version        => 'version',\n    }.freeze\n\n    # 仓库配置\n    REPO_CONFIG_KEY = {\n        # [String] 仓库完整路径\n        :abs_dest       => 'abs-dest',\n        # [Boolean] 是否是配置仓库\n        :config_repo    => 'config-repo',\n        # [String] 包含仓库的文件夹相对.mgit目录路径，完整路径如：<.mgit所在路径>/dest/<repo_name>，与abs-dest同时指定时无效\n        :dest           => 'dest',\n        # [Boolean] 是否是占位仓库，占位操作不会让mgit进行常规操作（隐含指定mgit_excluded为true），标记为占位的仓库组装器组装时不需要使用\n        :dummy          => 'dummy',\n        # [Json] 锁定状态，见REPO_CONFIG_LOCK_KEY\n        :lock           => 'lock',\n        # [Boolean] 是否排除mgit管理，被排除的仓库不会让mgit进行常规操作，若未标记dummy，则为不被mgit管理，但组装器需要的仓库\n        :mgit_excluded  => 'mgit-excluded',\n        # [String] 远程仓库根目录，完整URL：remote/remote_path\n        :remote         => 'remote',\n        # [String] 远程仓库相对目录，完整URL：remote/remote_path\n        :remote_path    => 'remote-path'\n    }.freeze\n\n    # 必须全局字段\n    REQUIRED_CONFIG_KEY = [\n        CONFIG_KEY[:remote],\n        CONFIG_KEY[:version],\n        CONFIG_KEY[:dest],\n        CONFIG_KEY[:repositories]\n    ].freeze\n\n    # 必须仓库配置字段\n    REQUIRED_REPO_CONFIG_KEY = [\n        REPO_CONFIG_KEY[:remote_path],\n    ].freeze\n\n    REPO_CONFIG_LOCK_KEY = {\n        :branch         => 'branch',\n        :commit_id      => 'commit-id',\n        :tag            => 'tag'\n    }.freeze\n\n    SNAPSHOT_KEY = {\n        :time_stamp     => 'time-stamp',\n        :message        => 'message',\n        :snapshot       => 'snapshot'\n    }.freeze\n\n    CONFIG_CACHE_KEY = {\n        :hash           => 'hash',\n        :cache          => 'cache'\n    }.freeze\n\n    # 定义字段用于调用git（shell）指令前export，便于仓库git hook区分来进行其余操作\n    MGIT_EXPORT_INFO = {\n        # 是否是MGit操作的git指令\n        :MGIT_TRIGGERRED  =>  1,\n        # 本次MGit操作的唯一标示\n        :MGIT_OPT_ID      =>  SecureRandom.uuid\n    }.freeze\n\n    # 临时中央仓库名\n    CENTRAL_REPO = \"Central\".freeze\n\n    INIT_CACHE_DIR_NAME = \"pmet_tini_tigm\".freeze\n  end\nend\n"
  },
  {
    "path": "lib/m-git/foundation/dir.rb",
    "content": "\nclass Dir\n\n  # 检查传入路径是不是git仓库\n  #\n  # @param path [String] 仓库路径\n  #\n  def self.is_git_repo?(path)\n    return false unless File.directory?(path)\n    git_dir = File.join(path, '.git')\n    File.directory?(git_dir)\n  end\n\n  def self.is_in_git_repo?(path)\n    check_path = path\n    result = is_git_repo?(check_path)\n    while !result\n      check_path = File.dirname(check_path)\n      break if check_path == '/'\n      result = is_git_repo?(check_path)\n    end\n    result\n  end\n\nend"
  },
  {
    "path": "lib/m-git/foundation/duration_recorder.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  # 计时器\n  #\n  class DurationRecorder\n\n    DEFAULT_DURATION_KEY = 'default_duration_key'\n\n    module Status\n      # 停止态\n      IDLE = 0\n      # 暂停态\n      PAUSE = 1\n      # 计时中\n      RUNNING = 2\n    end\n\n    include Status\n\n    @@status = {}\n    @@duration = {}\n    @@time_stamp = {}\n    @@lock = Mutex.new\n\n    def self.start(duration_key:DEFAULT_DURATION_KEY, use_lock:false)\n      mutex_exec(use_lock) {\n        status = IDLE if @@status[duration_key].nil?\n        if status == IDLE\n          @@duration[duration_key] = 0.0\n          @@time_stamp[duration_key] = Time.new.to_f\n          @@status[duration_key] = RUNNING\n        else\n          puts '需要停止计时后重新开始计时'\n        end\n      }\n    end\n\n    def self.pause(duration_key:DEFAULT_DURATION_KEY, use_lock:false)\n      mutex_exec(use_lock) {\n        if @@status[duration_key] == RUNNING\n          current = Time.new.to_f\n          @@duration[duration_key] += (current - @@time_stamp[duration_key])\n          @@time_stamp.delete(duration_key)\n          @@status[duration_key] = PAUSE\n        end\n      }\n    end\n\n    def self.resume(duration_key:DEFAULT_DURATION_KEY, use_lock:false)\n      mutex_exec(use_lock) {\n        if @@status[duration_key] == PAUSE\n          @@time_stamp[duration_key] = Time.new.to_f\n          @@status[duration_key] = RUNNING\n        end\n      }\n    end\n\n    def self.end(duration_key:DEFAULT_DURATION_KEY, use_lock:false)\n      mutex_exec(use_lock) {\n        if @@status[duration_key] != IDLE\n          current = Time.new.to_f\n          @@duration[duration_key] += (current - @@time_stamp[duration_key])\n          @@time_stamp.delete(duration_key)\n          @@status[duration_key] = IDLE\n          return @@duration[duration_key]\n        else\n          puts '没有需要停止的计时'\n        end\n      }\n    end\n\n    private\n\n    # 多线程执行保护\n    #\n    # @param use_lock [Boolean] 执行是否加锁\n    #\n    def self.mutex_exec(use_lock)\n      if use_lock\n        @@lock.lock\n        yield if block_given?\n        @@lock.unlock\n      else\n        yield if block_given?\n      end\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/foundation/git_message_parser.rb",
    "content": "\nmodule MGit\n  # Git interact message parse with git-server\n  #\n  class GitMessageParser\n\n    def initialize(url)\n      @url = url\n    end\n\n    # @return [String] error message\n    # @return [nil] none error\n    #\n    def parse_fetch_msg(input)\n      parse_pull_msg(input)\n    end\n\n    # @return [String] error message\n    # @return [nil] none error\n    #\n    def parse_pull_msg(input)\n      __default_parse_msg(input)\n    end\n\n    # @return [String] error message\n    # @return [nil] none error\n    #\n    def parse_push_msg(input)\n      __default_parse_msg(input)\n    end\n\n    # @return [String] codereview的url地址\n    # 默认无解析\n    def parse_code_review_url(input)\n      nil\n    end\n\n    def __default_parse_msg(msg)\n      return if msg.nil? || msg.empty?\n\n      key_word = 'error:'\n      error_line = msg.split(\"\\n\").find {|line|\n        # 解析 \"error: failed to push some refs...\"\n        line.include?(key_word)\n      }\n      msg if error_line\n    end\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/foundation/lock.rb",
    "content": "\nmodule MGit\n  class Lock\n    class << self\n\n      # @!scope 互斥执行锁\n      # @example\n      # mutex_exec do\n      #   exec..\n      # end\n      def mutex_exec\n        @mutex = Mutex.new if @mutex.nil?\n        @mutex.lock\n        yield if block_given?\n        @mutex.unlock\n      end\n\n      # @!scope 互斥显示锁\n      # @example\n      # mutex_puts do\n      #   exec..\n      # end\n      def mutex_puts\n        @mutex_puts = Mutex.new if @mutex_puts.nil?\n        @mutex_puts.lock\n        yield if block_given?\n        @mutex_puts.unlock\n      end\n\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/foundation/loger.rb",
    "content": "#coding=utf-8\n\nrequire 'logger'\nrequire 'fileutils'\nmodule MGit\n\n  # 日志模块配置\n  class Loger\n\n    MGIT_LOG_FILE_NAME = 'mgit.log'\n\n    #\n    # 配置Log模块\n    #\n    # @param root [String] 工程根目录\n    #\n    def self.config(root)\n      # 是否开启log\n      begin\n        log_enable = MGit::MGitConfig.query_with_key(root, :logenable)\n      rescue MGitClass::Error => e\n        log_enable = TRUE\n      end\n      MGit::Loger.set_log_enable(log_enable)\n\n      # 配置log路径\n      log_dir = File.join(root, MGit::Constants::PROJECT_DIR[:log_dir])\n      FileUtils.mkdir_p(log_dir) if !Dir.exist?(log_dir)\n      file_path = File.join(log_dir, MGIT_LOG_FILE_NAME)\n      MGit::Loger.set_log_file(file_path)\n\n      # 配置log的level\n      begin\n        log_level = MGit::MGitConfig.query_with_key(root, :loglevel)\n      rescue MGitClass::Error => e\n        log_level = 1\n      end\n      MGit::Loger.set_log_level(log_level)\n\n    end\n\n  end\n\n\n  # 日志模块\n  class Loger\n    DEFAULT_SHIFT_SIZE = 1048576  #每个文件1M\n    DEFAULT_SHIFT_AGE = 10 # 保留10个文件\n    DEFAULT_LOG_FILE = \"./mgit.log\"\n\n    #\n    # 设置mgit log是否开启 默认开启\n    #\n    # @param log_enable [Boolean] 是否开启log日志\n    #\n    def self.set_log_enable(log_enable)\n      @log_enable = log_enable\n    end\n\n    #\n    # 设置mgit log的打印等级\n    #\n    # @param level [Enumerable]: Logger::DEBUG | Logger::INFO | Logger::ERROR | Logger::FATAL\n    #\n    def self.set_log_level(level)\n      self.logger.level = level\n    end\n\n    #\n    # 设置mgit log的文件名， 默认路径在工作区的 .mgit/logs/mgit.log\n    #\n    def self.set_log_file(file_path)\n      @log_file = file_path\n    end\n\n    #\n    # 打印 DEBUG类型的log\n    #\n    def self.debug(message)\n      self.logger.debug(message) if @log_enable\n    end\n\n    #\n    # 打印 DEBUG类型的log\n    #\n    def self.info(message)\n      self.logger.info(message) if @log_enable\n    end\n\n    #\n    # 打印 WARN类型的log\n    #\n    def self.warn(message)\n      self.logger.warn(message) if @log_enable\n    end\n\n    #\n    # 打印 ERROR类型的log\n    #\n    def self.error(message)\n      self.logger.error(message) if @log_enable\n    end\n\n    #\n    # 打印 FATAL类型的log\n    #\n    def self.fatal(message)\n      self.logger.fatal(message) if @log_enable\n    end\n\n    private\n    def self.logger\n      unless @logger\n        @log_enable ||= TRUE\n        @log_level ||= Logger::INFO\n        @log_file ||= DEFAULT_LOG_FILE\n        @logger = Logger.new(@log_file, shift_age = DEFAULT_SHIFT_AGE, shift_size = DEFAULT_SHIFT_SIZE)\n        @logger.level = @log_level\n        @logger.datetime_format = '%Y-%m-%d %H:%M:%S'\n        @logger.formatter = proc do | severity, datetime, progname, msg|\n          \"#{datetime} - #{severity} - : #{msg}\\n\"\n        end\n      end\n      @logger\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/foundation/mgit_config.rb",
    "content": "#coding=utf-8\n\nrequire_relative 'utils'\n\nmodule MGit\n  class MGitConfig\n\n    CONFIG_KEY = {\n        :managegit  => {\n            :info     => '是否将.git实体托管给MGit，托管后仓库内的.git目录是软链 (true/false)',\n            :type     => 'Boolean',\n            :default  => true\n        },\n        :maxconcurrentcount => {\n            :info     => 'MGit操作的最大并发数? (Integer)',\n            :type     => 'Integer',\n            :default  => MGit::Utils.logical_cpu_num\n        },\n        :syncworkspace => {\n            :info     => '是否按照配置表同步工作区? (true/false)',\n            :type     => 'Boolean',\n            :default  => true\n        },\n        :savecache => {\n            :info     => '同步工作区时回收的仓库是否保留到缓存中? (true/false)',\n            :type     => 'Boolean',\n            :default  => false\n        },\n        :logenable => {\n            :info     => '是否开启日志打印，操作日志会收集到.mgit/log目录下? (true/false)',\n            :type     => 'Boolean',\n            :default  => true\n        },\n        :loglevel => {\n            :info     => '日志的打印级别, DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, FATAL = 4',\n            :type     => 'Integer',\n            :default  => 1\n        }\n    }\n\n    class << self\n\n      # 查询配置\n      #\n      # @param root [String] 工程根目录\n      #\n      # @raise [MGit::Error] 异常错误\n      #\n      def query(root)\n        config, error = __load_file(root)\n        if !error.nil?\n          raise Error.new(error)\n          return\n        elsif block_given?\n          # 如果文件存在但无内容，此时读取到的数据类型是FalseClass，此处统一规范化\n          config = {} if !config.is_a?(Hash)\n          yield(config)\n        end\n      end\n\n      # Description of #query_with_key\n      #\n      # @param root [String] 工程根目录\n      #\n      # @param key_symbol [Symbol] 符号key值\n      #\n      # @return [Object] 配置值\n      #\n      # @raise [MGit::Error] 异常错误\n      def query_with_key(root, key_symbol)\n        query(root) { |config|\n          if !config[key_symbol.to_s].nil?\n            return config[key_symbol.to_s]\n          elsif !CONFIG_KEY[key_symbol].nil?\n            return CONFIG_KEY[key_symbol][:default]\n          else\n            return nil\n          end\n        }\n      end\n\n      # 更新配置\n      #\n      # @param root [String] 工程根目录\n      #\n      # @raise [MGit::Error] 异常错误\n      #\n      def update(root)\n        # 加载配置\n        config, error = __load_file(root)\n        if !error.nil?\n          raise Error.new(error)\n          return\n        end\n\n        # 如果文件存在但无内容，此时读取到的数据类型是FalseClass，此处统一规范化\n        config = {} if !config.is_a?(Hash)\n\n        # 更新\n        yield(config) if block_given?\n\n        # 更新完后校验格式\n        if !config.is_a?(Hash)\n          raise Error.new(\"工具配置更新数据格式错误，更新失败！\")\n          return\n        end\n\n        # 写回配置\n        error = write_to_file(root, config)\n        if !error.nil?\n          raise Error.new(error)\n          return\n        end\n      end\n\n      # 列出所有配置\n      #\n      # @param root [String] 工程根目录\n      #\n      def dump_config(root)\n        query(root) { |config|\n          CONFIG_KEY.each_with_index { |(key, value_dict), index|\n            line = \"#{Output.blue_message(\"[#{value_dict[:info]}]\")}\\n#{key.to_s}: \"\n            set_value = config[key.to_s]\n            if !set_value.nil?\n              line += \"#{set_value}\\n\\n\"\n            else\n              line += \"#{value_dict[:default]}\\n\\n\"\n            end\n            puts line\n          }\n        }\n      end\n\n      # 验证一个value的值是否符合key的类型\n      #\n      # @param root [String] 工程根目录\n      #\n      # @param key [String] 字符串key值\n      #\n      # @param value [String] 字符串格式的值\n      #\n      # @return [Object] key值对应的合法类型value值\n      #\n      def to_suitable_value_for_key(root, key, value)\n        return unless CONFIG_KEY.keys.include?(key.to_sym)\n        return nil if !value.is_a?(String)\n\n        key_dict = CONFIG_KEY[key.to_sym]\n        set_value = nil\n\n        # 是否是数字\n        if key_dict[:type] == 'Integer' && value.to_i.to_s == value\n          set_value = value.to_i\n\n          # 是否是true\n        elsif key_dict[:type] == 'Boolean' && value.to_s.downcase == 'true'\n          set_value = true\n\n          # 是否是false\n        elsif key_dict[:type] == 'Boolean' && value.to_s.downcase == 'false'\n          set_value = false\n\n          # 是否是其他字符串\n        elsif key_dict[:type] == 'String' && value.is_a?(String)\n          set_value = value\n        end\n\n        set_value\n      end\n\n      private\n\n      # 加载mgit配置文件\n      #\n      # @param root [String] 工程根目录\n      #\n      # @return [(Hash, Boolean)] (配置内容，错误信息)\n      #\n      def __load_file(root)\n        config_path = File.join(root, Constants::MGIT_CONFIG_PATH)\n        if File.exists?(config_path)\n          begin\n            return YAML.load_file(config_path), nil\n          rescue => e\n            return nil, \"工具配置文件\\\"#{File.basename(config_path)}\\\"读取失败，原因：\\n#{e.message}\"\n          end\n        end\n\n        [nil, nil]\n      end\n\n      # 将配置写回文件\n      #\n      # @param root [String] 工程根目录\n      #\n      # @param content [Hash] 新配置\n      #\n      # @return [String] 错误信息\n      #\n      def write_to_file(root, content)\n        config_path = File.join(root, Constants::MGIT_CONFIG_PATH)\n        begin\n          FileUtils.rm_f(config_path) if File.exist?(config_path)\n\n          dir_name = File.dirname(config_path)\n          FileUtils.mkdir_p(dir_name) if !Dir.exist?(dir_name)\n\n          file = File.new(config_path, 'w')\n          if !file.nil?\n            file.write(content.to_yaml)\n            file.close\n          end\n          return nil\n        rescue => e\n          return \"工具配置文件\\\"#{File.basename(config_path)}\\\"写入失败，原因：\\n#{e.message}\"\n        end\n      end\n    end\n\n  end\nend\n"
  },
  {
    "path": "lib/m-git/foundation/operation_progress_manager.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  # 本类用于缓存/加载操作中间态\n  class OperationProgressContext\n\n    # 现场信息关键字段\n    CONTEXT_CMD = 'cmd'\n    CONTEXT_OPTS = 'opts'\n    CONTEXT_BRANCH = 'branch'\n    CONTEXT_REPOS = 'repos'\n    CONTEXT_OTHER = 'other'\n\n    attr_accessor :type     # String: 本次进入中间态的操作类型，可自定义，如'merge_in_progress','rebase_in_progress'等。该字段用于索引缓存的中间态信息，需要唯一。\n    attr_accessor :cmd      # String: 本次执行指令，如'merge'，'checkout'等\n    attr_accessor :opts     # String: 本次执行参数，如'--no-ff'，'--ff'等\n    attr_accessor :branch   # String: 仓库当前分支\n    attr_accessor :repos    # [String]: 本次执行哪些仓库的名称数组\n    attr_accessor :other    # Object: 其他信息，可自定义\n\n    def initialize(type)\n      self.type = type\n      self.other = {}\n    end\n\n    # 将中间态对象序列化为Hash\n    def serialize\n      return {\n          CONTEXT_CMD         => self.cmd,\n          CONTEXT_OPTS        => self.opts,\n          CONTEXT_BRANCH      => self.branch,\n          CONTEXT_REPOS       => self.repos,\n          CONTEXT_OTHER       => self.other\n      }\n    end\n\n    # 反序列化\n    #\n    # @param dict [Hash] 中间态Hash\n    #\n    def deserialize(dict)\n      self.cmd = dict[CONTEXT_CMD]\n      self.opts = dict[CONTEXT_OPTS]\n      self.branch = dict[CONTEXT_BRANCH]\n      self.repos = dict[CONTEXT_REPOS]\n      self.other = dict[CONTEXT_OTHER]\n    end\n\n    # 校验中间态是否合法，仓库可缺省，若缺省则表示所有仓库\n    #\n    # @return [Boolean] 是否合法\n    #\n    def validate?\n      return !self.cmd.nil? && !self.opts.nil? && !self.branch.nil?\n    end\n  end\n\n  class OperationProgressManager\n\n    PROGRESS_TYPE = {\n        :merge  =>  'merge_in_progress',\n        :rebase =>  'rebase_in_progress',\n        :pull   =>  'pull_in_progress'\n    }.freeze\n\n    class << self\n\n      # 进入中间态\n      #\n      # @param root [String] 多仓库根目录\n      #\n      # @param context [OperationProgressContext] 中间态对象\n      #\n      def trap_into_progress(root, context)\n        begin\n          MGitConfig.update(root) { |config|\n            config[context.type] = context.serialize\n          }\n        rescue Error => e\n          Output.puts_fail_message(e.msg)\n        end\n      end\n\n      # 删除中间态\n      #\n      # @param root [String] 多仓库根目录\n      #\n      # @param type [String] 自定义Key值，用于索引中间态信息\n      #\n      def remove_progress(root, type)\n        begin\n          MGitConfig.update(root) { |config|\n            config.delete(type)\n          }\n        rescue Error => e\n          Output.puts_fail_message(e.msg)\n        end\n      end\n\n      # 是否处于中间态中\n      def is_in_progress?(root, type)\n        is_in_progress = false\n        begin\n          MGitConfig.query(root) { |config|\n            is_in_progress = !config[type].nil?\n          }\n        rescue Error => e\n          Output.puts_fail_message(e.msg)\n        end\n        return is_in_progress\n      end\n\n      # 加载中间态上下文\n      #\n      # @param root [String] 多仓库根目录\n      #\n      # @param type [String] 自定义Key值，用于索引中间态信息\n      #\n      # @return [OperationProgressContext，String] 中间态对象；错误信息\n      #\n      def load_context(root, type)\n        context = nil\n        error = nil\n        begin\n          MGitConfig.query(root) { |config|\n            dict = config[type]\n            context = OperationProgressContext.new(type)\n            context.deserialize(dict)\n          }\n        rescue Error => e\n          Output.puts_fail_message(e.msg)\n          error = e.msg\n        end\n        return context, error\n      end\n\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/foundation/timer.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  # 计时器，用于统计指令执行耗时\n  class Timer\n\n    @@time_stamp = {}\n    @@duration = {}\n    @@lock = Mutex.new\n\n    class << self\n\n      # 开始计时\n      #\n      # @param repo_name [String] 仓库名\n      #\n      # @param use_lock [Boolean] default: false 是否加锁\n      #\n      def start(repo_name, use_lock:false)\n        return if repo_name.nil?\n        mutex_exec(use_lock) {\n          @@time_stamp[repo_name] = Time.new if @@time_stamp[repo_name].nil?\n          @@duration[repo_name] = 0 if @@duration[repo_name].nil?\n        }\n      end\n\n      # 停止计时\n      #\n      # @param repo_name [String] 仓库名\n      #\n      # @param use_lock [Boolean] default: false 是否加锁\n      #\n      def stop(repo_name, use_lock:false)\n        return if repo_name.nil?\n        mutex_exec(use_lock) {\n          if !@@time_stamp[repo_name].nil? && !@@duration[repo_name].nil?\n            @@duration[repo_name] += Time.new.to_f - @@time_stamp[repo_name].to_f\n            @@time_stamp[repo_name] = nil\n          end\n        }\n      end\n\n      # 显示最耗时仓库\n      #\n      # @param threshold [Type] default: 5 耗时提示阈值，时间超过该阈值则将仓库纳入提醒集合\n      #\n      def show_time_consuming_repos(threshold:5)\n        repos = []\n        @@duration.sort_by { |repo_name,seconds| seconds }.reverse.first(5).each { |info|\n          repo_name = info.first\n          seconds = info.last\n          repos.push(\"[#{seconds.round(2)}s]#{repo_name}\") if seconds > threshold\n        }\n        Output.puts_remind_block(repos, \"以上为最耗时且耗时超过#{threshold}s的仓库,请自行关注影响速度的原因。\") if repos.length > 0\n      end\n\n      # 多线程执行保护\n      #\n      # @param use_lock [Boolean] 执行是否加锁\n      #\n      def mutex_exec(use_lock)\n        if use_lock\n          @@lock.lock\n          yield if block_given?\n          @@lock.unlock\n        else\n          yield if block_given?\n        end\n      end\n\n    end\n\n  end\nend\n"
  },
  {
    "path": "lib/m-git/foundation/utils.rb",
    "content": "#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          begin\n            num = `sysctl -n hw.logicalcpu`\n            num = `cat /proc/cpuinfo | grep \"processor\" | wc -l` unless $?.success?\n            @logical_cpu_num = num\n          rescue Exception => _\n            @logical_cpu_num = \"5\"\n          end\n        end\n        @logical_cpu_num.to_i\n      end\n\n      # ----- Shell指令相关 -----\n\n      # 执行shell指令\n      def execute_shell_cmd(cmd)\n        begin\n          (stdout, stderr, status) = Open3.capture3(cmd)\n        rescue => e\n          puts \"\\n\"\n          Foundation.help!(\"指令 \\\"#{cmd}\\\" 执行异常：#{e.message}\")\n        end\n        yield(stdout, stderr, status) if block_given?\n        self\n      end\n\n      # ----- 目录相关 -----\n\n      # 改变当前路径\n      def change_dir(dir)\n        return if dir == Dir.pwd\n        begin\n          Dir.chdir(dir)\n        rescue => e\n          raise \"目录切换失败：#{e.message}\"\n        end\n      end\n\n      # 在某路径下执行代码\n      def execute_under_dir(dir)\n        origin_dir = Dir.pwd\n        change_dir(dir)\n        yield() if block_given?\n        change_dir(origin_dir)\n      end\n\n      # 计算相对目录\n      #\n      # @param dir_a [String] 目录A，如‘/a/b/A’\n      #\n      # @param dir_b [String] 目录B，如‘/a/c/B’\n      #\n      # @return [String] A目录下文件相对B目录的路径，如‘../../c/B’\n      #\n      def relative_dir(dir_a, dir_b, realpath: true)\n        if realpath\n          (Pathname.new(dir_a).realpath.relative_path_from(Pathname.new(dir_b).realpath)).to_s\n        else\n          (Pathname.new(dir_a).relative_path_from(Pathname.new(dir_b))).to_s\n        end\n      end\n\n      # 扩展成完整路径\n      #\n      # @param path [String] 路径名\n      #\n      # @param base [Type] default: nil 基准路径\n      #\n      # @return [String] 扩展后的完整路径\n      #\n      def expand_path(path, base:nil)\n        pn = Pathname.new(path)\n        if pn.relative?\n          base = Dir.pwd if base.nil?\n          File.expand_path(File.join(base, path))\n        else\n          path\n        end\n      end\n\n      # 初始化缓存目录\n      #\n      # @return [String] 目录地址\n      #\n      def generate_init_cache_path(root)\n        temp_dir = \".#{Constants::INIT_CACHE_DIR_NAME}__#{Process.pid}__#{Time.new.to_i.to_s}\"\n        File.join(root,temp_dir)\n      end\n\n      # 创建软连接\n      #\n      # @param target_path [String] 目标文件（文件夹）的绝对路径\n      #\n      # @param link_path [String] 软连接所在绝对路径\n      #\n      def link(target_path, link_path)\n        target_relative_path = File.join(relative_dir(File.dirname(target_path), File.dirname(link_path)), File.basename(target_path))\n        FileUtils.symlink(target_relative_path, link_path, force:true)\n      end\n\n      # 显示下载仓库信息\n      #\n      # @param missing_light_repos [Array<LightRepo>] 缺失仓库配置对象\n      #\n      def show_clone_info(root, missing_light_repos)\n        notice_repo = []\n        clone_from_local = []\n        clone_from_remote = []\n        missing_light_repos.each { |light_repo|\n          if Dir.exist?(File.join(light_repo.git_store_dir(root), '.git'))\n            clone_from_local += [light_repo.name]\n          else\n            clone_from_remote += [light_repo.name]\n          end\n        }\n\n        notice_repo.push(['从本地导出', clone_from_local]) if clone_from_local.length > 0\n        notice_repo.push(['从远程下载', clone_from_remote]) if clone_from_remote.length > 0\n\n        puts Output.generate_table_combination(notice_repo, separator: \"|\")\n        Output.puts_processing_message('以上仓库本地缺失，处理中...')\n      end\n\n      # ----- Git相关 -----\n\n      # 在不拉取仓库的情况下，查询远程仓库是否存在某分支\n      #\n      # @return [Bool] 是否存在分支\n      #\n      def branch_exist_on_remote?(branch, git_url)\n        return false if branch.nil? || git_url.nil?\n        cmd = \"git ls-remote --heads #{git_url} | grep \\\"#{branch}\\\"\"\n        execute_shell_cmd(cmd) { |stdout, stderr, status|\n          return status.success?\n        }\n      end\n\n      # 在不拉仓库的情况下，查询当前用户是否有权限拉取代码\n      #\n      # @return [Bool] 是否有权限\n      #\n      def has_permission_of_remote?(git_url)\n        return false if git_url.nil?\n        cmd = \"git ls-remote --heads #{git_url}\"\n        execute_shell_cmd(cmd) { |stdout, stderr, status|\n          return status.success?\n        }\n      end\n\n      # 链接.git仓库实体\n      # 1、如果git实体存在，则删除，以当前仓库的.git为主\n      # 2、移动仓库的.git到git实体\n      # 3、软链仓库的.git为git实体\n      #\n      # @param source_dir [String] 源码路径\n      #\n      # @param source_git_dir [String] 存放git实体的路径\n      #\n      #\n      #\n      def link_git(source_dir, source_git_dir)\n\n        # 工作区.git\n        origin_git = File.join(source_dir, '.git')\n\n        # 本地缓存的.git\n        target_git = File.join(source_git_dir, '.git')\n\n        FileUtils.remove_dir(target_git, true) if File.exist?(target_git)\n        FileUtils.mkdir_p(source_git_dir) unless File.exist?(source_git_dir)\n\n        FileUtils.mv(origin_git, source_git_dir)\n\n        # 创建.git软链接\n        link(target_git, origin_git)\n      end\n\n      # 根据url生成git实体存放路径\n      #\n      # @param url [String] 仓库url\n      #\n      # @return [String] .git实体存放地址，生成错误返回nil\n      #\n      def generate_git_store(root, url)\n        return if url.nil?\n        git_dir = File.join(root, Constants::PROJECT_DIR[:source_git])\n        begin\n          url_obj = URI(url)\n          # 去除shceme\n          url_path = File.join(\"#{url_obj.host}#{url_obj.port.nil? ? '' : \":#{url_obj.port}\"}\", url_obj.path)\n          # 去除后缀名\n          git_relative_dir = File.join(File.dirname(url_path), File.basename(url_path, File.extname(url_path)))\n\n          File.join(git_dir, git_relative_dir)\n        rescue\n        end\n      end\n\n      # ----- 工作区同步相关 -----\n\n      # 同步工作区(缓存或弹出)\n      #\n      # @param root [String] mgit管理工程根目录\n      #\n      # @param config [Manifest] 配置对象\n      #\n      # @param recover_cache_if_cancelled [Boolean] 如果回收过程中取消操作，是否恢复缓存\n      #                                            （需要自行判断，如果方法调用前缓存已经覆盖，那么需要恢复以保障下次同步操作正常执行\n      #                                              如果调用前缓存未被覆盖，则无需恢复，此时若强行恢复会干扰下次同步操作）\n      #\n      def sync_workspace(root, config, recover_cache_if_cancelled:true)\n\n        # 若有缓存仓库，则移到工作区\n        config.light_repos.each { |light_repo|\n          #【注意】url不一致会认为是不同仓库，将缓存当前仓库，并弹出url对应缓存（若对应缓存则不弹出）\n          Workspace.sync_workspace(root, light_repo)\n        }\n\n        # 更新冗余仓库数据\n        if config.previous_extra_light_repos.nil? || config.previous_extra_light_repos.length == 0\n          config.update_previous_extra_light_repos(root)\n        end\n\n        # 若工作区有多余仓库，则缓存\n        if !config.previous_extra_light_repos.nil? && config.previous_extra_light_repos.length > 0\n\n          dirty_repos = []\n          do_repos = []\n          config.previous_extra_light_repos.each { |light_repo|\n\n            # 如果仓库是主仓库，则不操作\n            next if light_repo.is_config_repo\n\n            repo, error = Repo.generate_softly(root, light_repo)\n            if error.nil?\n              if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]\n                dirty_repos.push(repo)\n              else\n                do_repos.push(repo)\n              end\n            end\n          }\n\n          if dirty_repos.length > 0\n            if Output.continue_with_interact_repos?(dirty_repos.map { |e| e.name }, '即将回收以上仓库，但存在本地改动，继续操作将丢失改动，是否取消？') ||\n                Output.continue_with_user_remind?(\"即将丢失改动，是否取消？\")\n              # 用上次配置覆盖以恢复缓存，否则若此次缓存已被覆盖，取消后下次操作同步将失效\n              config.update_cache_with_content(root, config.previous_config) if recover_cache_if_cancelled\n              Foundation.help!('操作取消')\n            end\n          end\n\n          current_time = Time.new.strftime(\"%Y%m%d%H%M%S\")\n          (dirty_repos + do_repos).each { |repo|\n            if dirty_repos.include?(repo)\n              repo.execute_git_cmd('add', '.')\n              repo.execute_git_cmd('stash', \"save -u #{current_time}_MGit回收仓库自动stash\")\n            end\n\n            begin\n              save_to_cache = MGitConfig.query_with_key(root, :savecache)\n            rescue Error => _\n              save_to_cache = false\n            end\n\n            # 如果仓库没有被管理，则不删除，直接缓存\n            is_git_managed = Dir.exist?(File.join(repo.config.git_store_dir(root), '.git'))\n            if save_to_cache || !is_git_managed\n              Workspace.push(root, repo.config)\n            else\n              FileUtils.rm_rf(repo.path) if Dir.exist?(repo.path)\n            end\n          }\n\n        end\n      end\n\n      def pop_git_entity(root, config)\n        config.light_repos.each { |light_repo|\n          # 将托管的.git弹出到工作区\n          Workspace.pop_git_entity(root, light_repo)\n        }\n      end\n\n      def push_git_entity(root, config)\n        config.light_repos.each { |light_repo|\n          # 将工作区的.git托管给mgit\n          Workspace.push_git_entity(root, light_repo)\n        }\n      end\n\n      # 判断url是否一致\n      #\n      # @param url_a [String] url\n      #\n      # @param url_b [String] url\n      #\n      # @return [Boolean] 是否一致\n      #\n      def url_consist?(url_a, url_b)\n        # 删除冗余字符\n        temp_a = normalize_url(url_a)\n        temp_b = normalize_url(url_b)\n\n        # 同时不为nil判断内容是否相等\n        return temp_a == temp_b if !temp_a.nil? && !temp_b.nil?\n        # reutrn | temp_a | temp_b\n        # ----------------------\n        #  true  |   nil  |   nil\n        #  true  |   ''   |   nil\n        #  true  |   nil  |   ''\n        # ----------------------\n        #  false |   nil  |   xxx\n        #  false |   xxx  |   nil\n        (temp_a.nil? && temp_b.nil?) || (!temp_a.nil? && temp_a.length == 0) || (!temp_b.nil? && temp_b.length == 0)\n      end\n\n      # 规范化url，删除冗余字符\n      #\n      # @param url [String] url字符串\n      #\n      # @return [String] 规范化后的url\n      #\n      def normalize_url(url)\n        return if url.nil?\n        refined_url = url.strip.sub(/(\\/)+$/,'')\n        return refined_url if refined_url.length == 0\n        begin\n          uri = URI(refined_url)\n          return \"#{uri.scheme}://#{uri.host}#{uri.port.nil? ? '' : \":#{uri.port}\"}#{uri.path}\"\n        rescue  => _\n        end\n      end\n\n      # 安全的路径拼接\n      #\n      # @param path_a [String] 路径a\n      #\n      # @param path_b [String] 路径b\n      #\n      # @return [String] 完整路径\n      #\n      def safe_join(path_a, path_b)\n        if !path_a.nil? && path_a.length > 0 && !path_b.nil? && path_b.length > 0\n          File.join(path_a, path_b)\n        elsif !path_a.nil? && path_a.length > 0\n          path_a\n        elsif !path_b.nil? && path_b.length > 0\n          path_b\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/foundation.rb",
    "content": "#coding=utf-8\n\nrequire 'm-git/foundation/constants'\nrequire 'm-git/foundation/dir'\nrequire 'm-git/foundation/git_message_parser'\nrequire 'm-git/foundation/lock'\nrequire 'm-git/foundation/loger'\nrequire 'm-git/foundation/duration_recorder'\nrequire 'm-git/foundation/mgit_config'\nrequire 'm-git/foundation/operation_progress_manager'\nrequire 'm-git/foundation/timer'\nrequire 'm-git/foundation/utils'\n\nmodule MGit\n  module Foundation\n\n    class << self\n      # 异常终止\n      def help!(msg, title:nil)\n        Output.puts_terminate_message(msg, title:title)\n        exit\n      end\n    end\n  end\nend"
  },
  {
    "path": "lib/m-git/hooks_manager.rb",
    "content": "\nmodule MGit\n  module HooksManager\n\n# --- 执行hook ---\n\n    class << self\n      # 获取配置表前执行的hook\n      #\n      # @param strict_mode [Boolean] default: true 严格模式下出错直接终止，非严格模式下出错抛出异常\n      #\n      # @return [Type] description_of_returned_object\n      #\n      def execute_manifest_hook(strict_mode:true)\n        __execute_hook_file(Constants::HOOK_NAME[:manifest_hook], 'MGitTemplate::ManifestHook') do |cls|\n          begin\n            cls.run\n          rescue Error => e\n            if strict_mode\n              Foundation.help!(\"配置表生成失败：#{e.msg}\") if e.type == MGIT_ERROR_TYPE[:config_generate_error]\n            else\n              raise e\n            end\n          end\n        end\n      end\n\n      # mgit执行前的hook\n      def execute_mgit_pre_hook(cmd, pure_opts)\n        __execute_hook_file(Constants::HOOK_NAME[:pre_hook], 'MGitTemplate::PreHook') do |cls|\n          cls.run(cmd, pure_opts, Workspace.root)\n        end\n      end\n\n      # mgit执行后的hook\n      def execute_mgit_post_hook(cmd, pure_opts, light_repos)\n        __execute_hook_file(Constants::HOOK_NAME[:post_hook], 'MGitTemplate::PostHook') do |cls|\n          cls.run(cmd, pure_opts, Workspace.root, light_repos)\n        end\n      end\n\n      # mgit执行前的hook（用户级，此时已经完成状态检查，可以在内部获取到仓库配置对象）\n      # 可以按需插入到不同指令的执行前时机下调用，然后在方法中通过'cmd'参数判断当前执行到是哪个指令\n      # 目前仅插入到commit指令，后续可按需插入\n      def execute_mgit_pre_exec_hook(cmd, pure_opts, light_repos)\n        __execute_hook_file(Constants::HOOK_NAME[:pre_exec_hook], 'MGitTemplate::PreExecHook') do |cls|\n          cls.run(cmd, pure_opts, Workspace.root, light_repos)\n        end\n      end\n\n      # 功能类似'execute_mgit_pre_exec_hook'，但仅仅是push指令专用（内部不用判断'cmd'，cmd一定是push，可替换为'execute_mgit_pre_exec_hook'）\n      def execute_mgit_pre_push_hook(cmd, pure_opts, light_repos)\n        __execute_hook_file(Constants::HOOK_NAME[:pre_push_hook], 'MGitTemplate::PrePushHook') do |cls|\n          cls.run(cmd, pure_opts, Workspace.root, light_repos)\n        end\n      end\n\n      # 执行下载后的hook\n      #\n      # @param repo_name [String] 仓库ming\n      #\n      # @param repo_path [String] 仓库本地路径\n      #\n      # @param root [String] .mgit所在目录\n      #\n      # @param error [String] 错误信息，nil表示成功\n      #\n      # @return [Boolean] hook是否操作过仓库分支\n      #\n      def execute_post_download_hook(repo_name, repo_path)\n        changed = __execute_hook_file(Constants::HOOK_NAME[:post_download_hook], 'MGitTemplate::PostDownloadHook') do |cls|\n          cls.run(repo_name, repo_path)\n        end\n        changed == true\n      end\n\n      # 执行hook文件\n      #\n      # @param file_name [String] hook文件名\n      #\n      # @param hook_class [String] hook Class name\n      #\n      # block\n      def __execute_hook_file(file_name, hook_class)\n        file_path = File.join(Workspace.hooks_dir, file_name)\n        if File.exists?(file_path)\n          require file_path\n        end\n        if Object.const_defined?(hook_class) && hook = Object.const_get(hook_class)\n          return yield(hook) if block_given?\n        end\n      end\n    end\n\n  end\nend\n"
  },
  {
    "path": "lib/m-git/manifest/cache_manager.rb",
    "content": "\nmodule MGit\n  class Manifest\n    class CacheManager\n\n      attr_reader :path\n      attr_reader :hash_sha1\n      attr_reader :hash_data\n\n      def load_path(cache_path)\n        return unless File.exist?(cache_path)\n        begin\n          cache = JSON.parse(File.read(cache_path))\n        rescue => _\n          Output.puts_fail_message(\"配置文件缓存解析失败！将根据原配置文件进行仓库配置。\")\n        end\n\n        @path = cache_path\n        @hash_sha1 = cache[Constants::CONFIG_CACHE_KEY[:hash]]\n        @hash_data = cache[Constants::CONFIG_CACHE_KEY[:cache]]\n      end\n\n      # 缓存配置文件\n      #\n      # @param cache_path [string] 配置文件目录\n      #\n      # @param hash_sha1 [String] 配置哈希字符串\n      #\n      # @param hash_data [Hash] 配置字典\n      #\n      def self.save_to_cache(cache_path, hash_sha1, hash_data)\n        FileUtils.mkdir_p(File.dirname(cache_path))\n        File.open(cache_path, 'w') do |file|\n          file.write({\n                         Constants::CONFIG_CACHE_KEY[:hash]   => hash_sha1,\n                         Constants::CONFIG_CACHE_KEY[:cache]  => hash_data\n                     }.to_json)\n        end\n      end\n\n\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/manifest/internal.rb",
    "content": "\nrequire 'm-git/manifest/cache_manager'\n\nmodule MGit\n  class Manifest\n    module Internal\n\n\n      # 配置对象\n      #\n      # @param config_path [String] 配置文件路径或目录\n      #\n      # @param strict_mode [Boolean] 是否使用严格模式。在严格模式下，出错将终止执行。在非严格模式下，出错将抛出异常，程序有机会继续执行。\n      #\n      def __setup(config_path, strict_mode)\n        @strict_mode = strict_mode\n        if File.directory?(config_path)\n          config_dir = config_path\n          config_path = File.join(config_dir, Constants::CONFIG_FILE_NAME[:manifest])\n        else\n          config_dir = File.dirname(config_path)\n          config_path = File.join(config_dir, File.basename(config_path))\n        end\n\n        local_path = File.join(config_dir, Constants::CONFIG_FILE_NAME[:local_manifest])\n        cache_path = File.join(config_dir, Constants::CONFIG_FILE_NAME[:manifest_cache])\n\n        __load_config(config_path, local_config_path: local_path, cache_path: cache_path)\n      end\n\n      # 简单配置对象，部分属性为nil\n      #\n      # @param config_content [Hash] 配置Hash\n      #\n      # @param strict_mode [Boolean] 是否使用严格模式。在严格模式下，出错将终止执行。在非严格模式下，出错将抛出异常，程序有机会继续执行。\n      #\n      def __simple_setup(config_content, strict_mode)\n        @strict_mode = strict_mode\n\n        if config_content.is_a?(Hash)\n          @config = config_content.deep_clone\n        else\n          @config = JSON.parse(config_content)\n        end\n        lint_raw_json!(config)\n\n        @light_repos = __generate_light_repos(config)\n        @config_repo = light_repos.find { |light_repo| light_repo.is_config_repo }\n\n        # 计算配置文件哈希\n        @hash_sha1 = __generate_hash_sha1(config.to_json)\n      end\n\n\n      private\n\n      def cache_manager\n        @cache_manager ||= CacheManager.new\n      end\n\n\n      # 加载配置文件\n      #\n      def __load_config(config_path, config_content: nil, local_config_path: nil, cache_path: nil)\n        if config_content\n          config_hash = __parse_manifest_json(config_content)\n        else\n          # 校验配置文件路径\n          lint_manifest_path(config_path)\n\n          # 读取配置文件\n          config_hash = __parse_manifest(config_path)\n        end\n\n        lint_raw_json!(config_hash)\n\n        if local_config_path && File.exist?(local_config_path)\n          lint_local_manifest_path(local_config_path)\n          local_config_hash = __parse_manifest(local_config_path)\n\n          __merge_manifest_hash(config_hash, local_config_hash)\n        end\n\n        @light_repos = __generate_light_repos(config_hash)\n        @config_repo = light_repos.find { |light_repo| light_repo.is_config_repo }\n        lint_light_repos!\n\n        @path = config_path\n        @config = config_hash\n        # 计算配置文件哈希\n        @hash_sha1 = __generate_hash_sha1(config_hash.to_json)\n\n        __load_cache(cache_path)\n        if previous_config.nil? || previous_config != config_hash\n          # 更新缓存\n          @cache_path = cache_path\n          @previous_config = config\n          CacheManager.save_to_cache(cache_path, hash_sha1, config)\n        end\n      end\n\n      def __load_cache(cache_path)\n        if cache_path && File.exist?(cache_path)\n          cache_manager.load_path(cache_path)\n          @cache_path = cache_manager.path\n          @previous_config = cache_manager.hash_data\n\n          @previous_extra_light_repos = __generate_extra_light_repos(config, previous_config)\n        end\n      end\n\n      def __parse_manifest_json(raw_string)\n        begin\n          raw_json = JSON.parse(raw_string)\n        rescue => _\n          terminate!(\"manifest文件解析错误，请检查json文件格式是否正确！\")\n        end\n\n        raw_json\n      end\n\n      def __parse_manifest(path)\n        begin\n          raw_string = File.read(path)\n        rescue => e\n          terminate!(\"配置文件#{path}读取失败：#{e.message}\")\n        end\n        __parse_manifest_json(raw_string)\n      end\n\n      def __generate_light_repos(config_hash)\n        light_repos = []\n        repositories = config_hash[Constants::CONFIG_KEY[:repositories]]\n        repositories.each do |repo_name, repo_cfg|\n          light_repos << LightRepoGenerator.light_repo_with(repo_name, repo_cfg, config_hash)\n        end\n        light_repos\n      end\n\n      def __generate_extra_light_repos(current_hash, previous_hash)\n        return if previous_hash.nil?\n        extra_light_repos = []\n        repositories = current_hash[Constants::CONFIG_KEY[:repositories]]\n        previous_repos = previous_hash[Constants::CONFIG_KEY[:repositories]]\n        extra_keys = previous_repos.keys - repositories.keys\n        extra_keys.each do |repo_name|\n          extra_light_repos << LightRepoGenerator.light_repo_with(repo_name, previous_repos[repo_name], previous_hash)\n        end\n        extra_light_repos\n      end\n\n\n      # 计算配置文件的哈希\n      def __generate_hash_sha1(json_string)\n        begin\n          return Digest::SHA256.hexdigest(json_string)\n        rescue => e\n          terminate!(\"配置文件哈希计算失败：#{e.message}\")\n        end\n      end\n\n      def __merge_manifest_hash(base_hash, attach_hash)\n        dict = base_hash\n        attach_hash.each { |key, value|\n          if key == Constants::CONFIG_KEY[:repositories] && value.is_a?(Hash)\n            dict[key] = {} if dict[key].nil?\n            value.each { |repo_name, config|\n              dict[key][repo_name] = {} if dict[key][repo_name].nil?\n              if config.is_a?(Hash)\n                config.each { |r_key, r_value|\n                  dict[key][repo_name][r_key] = r_value if Constants::REPO_CONFIG_KEY.values.include?(r_key)\n                }\n              end\n            }\n          elsif Constants::CONFIG_KEY.values.include?(key)\n            dict[key] = value\n          end\n        }\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/manifest/light_repo.rb",
    "content": "#coding=utf-8\n\nrequire_relative 'light_repo_generator'\n\nmodule MGit\n  class Manifest\n    # @!scope manifest.json 配置的reponsitories 对象\n    # 该类用于配置解析后的初步处理，其字段与配置文件一致，但不保证与本地仓库状态一致。\n    # 而Repo类则会校验本地仓库，其状态与本地仓库状态一致，主要用于执行多仓库操作。\n    #\n    LightRepo = Struct.new(\n        # [String] 仓库名\n        :name,\n\n        # [String] 仓库相对mgit根目录的存放路径（完整路径如：<.mgit所在路径>/path/<repo_name>），或绝对路径（指定abs-dest）\n        # 绝对路径\n        :path,\n\n        # [String] 仓库配置分支\n        :branch,\n\n        # [String] 仓库配置commit id\n        :commit_id,\n\n        # [String] 仓库配置tag\n        :tag,\n\n        # [String] 仓库的git地址\n        :url,\n\n        # [Boolean] 是否纳入mgit管理\n        :mgit_excluded,\n\n        # [Boolean] 是否是占位仓库【2.3.0废弃】\n        :dummy,\n\n        # [Boolean] 是否是配置仓库（包含配置表的仓库），若是，则某些操作如merge，checkout等会优先操作配置仓库，确保配置表最新\n        :is_config_repo,\n\n        # [Boolean] 是否锁定仓库（即任何操作均保证仓库状态与指定配置一致）\n        :lock\n    ) do\n\n      # 根据解析内容拼接一个clone地址\n      #\n      # @param root [String] mgit根目录\n      # @param local_url [String] default: nil 如果从本地clone，可传入一个本地的xxx.git实体地址\n      #\n      # @return [String] clone地址\n      #\n      def clone_url(root, local_url:nil, clone_branch:nil)\n        url = local_url.nil? ? self.url : local_url\n\n        if !clone_branch.nil? && Utils.branch_exist_on_remote?(clone_branch, url)\n          branch_opt = \"-b #{clone_branch}\"\n        elsif !self.branch.nil? && Utils.branch_exist_on_remote?(self.branch, url)\n          branch_opt = \"-b #{self.branch}\"\n        else\n          branch_opt = ''\n        end\n\n        \"git clone #{branch_opt} -- #{url} #{abs_dest(root)}\"\n      end\n\n      # 生成绝对路径\n      #\n      # @param root [String] 多仓库根目录\n      #\n      # @return [String] 仓库绝对路径\n      #\n      def abs_dest(root)\n        Utils.expand_path(self.path, base:root)\n      end\n\n      # 生成.git存储的完整路径\n      #\n      # @param root [String] 多仓库根目录\n      #\n      # @return [String] .git存储的完整路径\n      #\n      def git_store_dir(root)\n        # 替换key值中的‘/’字符，避免和路径混淆\n        name = self.name.gsub(/\\//,':')\n\n        # 删除冗余字符\n        url = Utils.normalize_url(self.url)\n\n        git_store = Utils.generate_git_store(root, url)\n        if git_store.nil?\n          git_dir = File.join(root, Constants::PROJECT_DIR[:source_git])\n          git_store = File.join(git_dir, name)\n        end\n\n        git_store\n      end\n\n      # 生成缓存存放完整路径\n      #\n      # @param root [String] 多仓库根目录\n      #\n      # @return [String] 缓存存放完整路径\n      #\n      def cache_store_dir(root)\n        File.join(git_store_dir(root), 'cache')\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/manifest/light_repo_generator.rb",
    "content": "\nmodule MGit\n  class Manifest\n    # @!scope lightrepo生成器\n    #\n    class LightRepoGenerator\n\n      # 简单初始化，有写字段缺失，仅包含名字，相对路径，url\n      #\n      # @param name [String] 仓库名\n      #\n      # @param path [String] 仓库相对路径\n      #\n      # @param url [String] 仓库url\n      #\n      # @return [LightRepo] 配置对象\n      #\n      def self.simple_init(name, path, url)\n        LightRepo.new(name, path, nil, nil, nil, url, false, false, false, false)\n      end\n\n      def self.light_repo_with(name, config_json, parent_json)\n        light_repo = LightRepo.new(name)\n\n        light_repo.path = __parse_path(name, config_json, parent_json)\n        lock_info = config_json[Constants::REPO_CONFIG_KEY[:lock]]\n\n        light_repo.lock = lock_info && !lock_info.empty?\n        if light_repo.lock\n          light_repo.commit_id = lock_info[Constants::REPO_CONFIG_LOCK_KEY[:commit_id]]\n          light_repo.tag = lock_info[Constants::REPO_CONFIG_LOCK_KEY[:tag]]\n          light_repo.branch = lock_info[Constants::REPO_CONFIG_LOCK_KEY[:branch]]\n        end\n        light_repo.url = __parse_url(config_json, parent_json)\n\n        dummy = config_json[Constants::REPO_CONFIG_KEY[:dummy]]\n        dummy = !dummy.nil? && dummy == true\n        if dummy\n          excluded = true\n        else\n          excluded = config_json[Constants::REPO_CONFIG_KEY[:mgit_excluded]]\n          excluded = parent_json[Constants::CONFIG_KEY[:mgit_excluded]] if excluded.nil?\n          excluded = !excluded.nil? && excluded == true\n        end\n        light_repo.mgit_excluded = excluded\n        light_repo.dummy = dummy\n\n        is_config_repo = config_json[Constants::REPO_CONFIG_KEY[:config_repo]]\n        light_repo.is_config_repo = is_config_repo.nil? ? false : is_config_repo\n        light_repo\n      end\n\n      private\n\n      class << self\n        def __parse_path(repo_name, config_json, parent_json)\n          abs_path = config_json[Constants::REPO_CONFIG_KEY[:abs_dest]]\n          return abs_path if !abs_path.nil? && !abs_path.empty?\n\n          local_path = parent_json[Constants::REPO_CONFIG_KEY[:dest]]\n          # 替换key值中的‘/’字符，避免和路径混淆\n          repo_name = repo_name.gsub(/\\//,':')\n          if local_path.nil?\n            Utils.safe_join(parent_json[Constants::CONFIG_KEY[:dest]], repo_name)\n          elsif !local_path.empty?\n            Utils.safe_join(local_path, repo_name)\n          else\n            repo_name\n          end\n        end\n\n        def __parse_url(config_json, parent_json)\n          source_remote = config_json[Constants::REPO_CONFIG_KEY[:remote]]\n          remote_path = config_json[Constants::REPO_CONFIG_KEY[:remote_path]]\n          return if remote_path.nil?\n          if source_remote.nil?\n            global_remote = parent_json[Constants::CONFIG_KEY[:remote]]\n            Utils.safe_join(global_remote, remote_path)\n          else\n            Utils.safe_join(source_remote, remote_path)\n          end\n        end\n      end\n\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/manifest/linter.rb",
    "content": "\nmodule MGit\n  class Manifest\n    module Linter\n\n      # 校验配置文件路径\n      #\n      # @param path [Stirng] 配置文件路径或包含配置文件的目录\n      #\n      # @return [String] 配置文件合法路径\n      #\n      def lint_manifest_path(path)\n        manifest_name = Constants::CONFIG_FILE_NAME[:manifest]\n\n        if !File.exists?(path)\n          if File.symlink?(path)\n            terminate!(\"配置文件软链接#{path}失效，请执行\\\"mgit config -m <new_path>/manifest.json\\\"更新！\")\n          else\n            terminate!(\"配置文件#{path}不存在！\")\n          end\n        elsif File.basename(path) != manifest_name\n          terminate!(\"请指定名为#{manifest_name}的文件！\", type:MGIT_ERROR_TYPE[:config_name_error])\n        end\n      end\n\n      # 校验本地配置文件路径\n      #\n      # @param path [String] 本地配置文件路径\n      #\n      # @return [String] 合法的本地配置文件路径\n      #\n      def lint_local_manifest_path(path)\n        local_manifest_name = Constants::CONFIG_FILE_NAME[:local_manifest]\n        terminate!(\"local配置文件#{path}不存在！\") if !File.file?(path)\n        terminate!(\"请指定名为#{local_manifest_name}的文件！\", type:MGIT_ERROR_TYPE[:config_name_error]) if File.basename(path) != local_manifest_name\n      end\n\n      # @!scope 检查lightrepo的仓库url是否重复\n      #\n      def lint_light_repos!\n        repo_urls = {}\n        light_repos.each { |light_repo|\n          next if light_repo.url.nil? || light_repo.url.length == 0\n          repo_urls[light_repo.url] = [] if repo_urls[light_repo.url].nil?\n          repo_urls[light_repo.url].push(light_repo.name)\n        }\n\n        error_repos = []\n        repo_urls.each { |_, value|\n          if value.length > 1\n            error_repos += value\n          end\n        }\n\n        if error_repos.length > 0\n          puts Output.generate_table(error_repos, separator:'|')\n          Foundation.help!(\"以上仓库url配置重复，请修改后重试！\")\n        end\n      end\n\n# 解析并校验配置文件\n      def lint_raw_json!(dict)\n        required_keys = Constants::REQUIRED_CONFIG_KEY\n        missing_required_keys = required_keys - dict.keys\n        terminate!(\"配置文件中缺失必需字段：#{missing_required_keys}\") if missing_required_keys.length > 0\n\n        valid_keys = Constants::CONFIG_KEY.values\n        valid_repo_keys = Constants::REPO_CONFIG_KEY.values\n\n        dict.each { |k, v|\n          terminate!(\"配置文件中存在冗余字段：#{k}\") unless valid_keys.include?(k)\n\n          if k == Constants::CONFIG_KEY[:repositories]\n            terminate!(\"配置文件中#{k}字段下的数据应为json格式！\") if !dict[k].is_a?(Hash)\n\n            config_repos = []\n            dict[k].each { |repo_name, config|\n              terminate!(\"配置文件中#{k}.#{repo_name}字段下的数据应为json格式！\") if !config.is_a?(Hash)\n\n              # 校验值类型\n              config.each { |rk, rv|\n                if rk == Constants::REPO_CONFIG_KEY[:mgit_excluded] ||\n                    rk == Constants::REPO_CONFIG_KEY[:config_repo] ||\n                    rk == Constants::REPO_CONFIG_KEY[:dummy]\n                  terminate!(\"配置文件中#{k}.#{repo_name}.#{rk}字段下的数据应为Bool类型！\") if !rv.is_a?(TrueClass) && !rv.is_a?(FalseClass)\n                elsif rk == Constants::REPO_CONFIG_KEY[:lock]\n                  terminate!(\"配置文件中#{k}.#{repo_name}.#{rk}字段下的数据应为Json类型！\") if !rv.is_a?(Hash)\n                elsif valid_repo_keys.include?(rk)\n                  terminate!(\"配置文件中#{k}.#{repo_name}.#{rk}字段下的数据应为String类型！\") if !rv.is_a?(String)\n                end\n              }\n\n              # 如果mgit_excluded字段是false或者缺省，则纳入mgit多仓库管理，进行严格校验\n              mgit_excluded = config[Constants::REPO_CONFIG_KEY[:mgit_excluded]]\n              global_mgit_excluded = dict[Constants::CONFIG_KEY[:mgit_excluded]]\n\n              if (mgit_excluded.nil? || mgit_excluded == false) && (global_mgit_excluded.nil? || global_mgit_excluded == false)\n                # 校验仓库配置必须字段\n                valid_required_repo_keys = Constants::REQUIRED_REPO_CONFIG_KEY # 不可缺省的字段\n                missing_required_keys = valid_required_repo_keys - config.keys\n                terminate!(\"配置文件中#{k}.#{repo_name}下有缺失字段:#{missing_required_keys.join(', ')}\") if missing_required_keys.length > 0\n\n                # 校验仓库配置冗余字段\n                # extra_keys = config.keys - valid_repo_keys\n                # terminate!(\"配置文件中#{k}.#{repo_name}下有冗余字段:#{extra_keys.join(', ')}\") if extra_keys.length > 0\n\n                # 统计指定的配置仓库\n                config_repo = config[Constants::REPO_CONFIG_KEY[:config_repo]]\n                if !config_repo.nil? && config_repo == true\n                  config_repos.push(repo_name)\n                end\n\n                # 校验锁定点\n                lock_key = Constants::REPO_CONFIG_KEY[:lock]\n                lock_config = config[lock_key]\n                if !lock_config.nil?\n                  valid_lock_keys = Constants::REPO_CONFIG_LOCK_KEY.values\n\n                  # 校验锁定点配置值\n                  lock_config.each { |ck, cv|\n                    terminate!(\"配置文件中#{k}.#{repo_name}.#{lock_key}.#{ck}字段下的数据应为String类型！\") if !cv.is_a?(String)\n                  }\n\n                  # 校验锁定点配置必须字段\n                  terminate!(\"配置文件中#{k}.#{repo_name}.#{lock_key}下只能指定字段:#{valid_lock_keys.join(', ')}中的一个！\") if lock_config.keys.length != 1 || !valid_lock_keys.include?(lock_config.keys.first)\n\n                  # 校验锁定点配置冗余字段\n                  # extra_keys = lock_config.keys - valid_lock_keys\n                  # terminate!(\"配置文件中#{k}.#{repo_name}.#{lock_key}下有冗余字段:#{extra_keys.join(', ')}\") if extra_keys.length > 0\n\n                end\n              end\n            }\n\n            # 校验配置仓库字段的合法性\n            if config_repos.length > 1\n              puts Output.generate_table(config_repos)\n              terminate!(\"配置表中同时指定了以上多个仓库为配置仓库，仅允许指定最多一个！\")\n            end\n          elsif k == Constants::CONFIG_KEY[:version]\n            terminate!(\"配置文件中#{k}字段下的数据应为Integer类型！\") if !dict[k].is_a?(Integer)\n          elsif k == Constants::CONFIG_KEY[:mgit_excluded]\n            terminate!(\"配置文件中#{k}字段下的数据应为Boolean类型！\") if !dict[k].is_a?(TrueClass) && !dict[k].is_a?(FalseClass)\n          else\n            terminate!(\"配置文件中#{k}字段下的数据应为String类型！\") if !dict[k].is_a?(String)\n          end\n        }\n      end\n\n    end\n\n  end\nend\n"
  },
  {
    "path": "lib/m-git/manifest/manifest.sample",
    "content": "--- manifest.json ---\n{\n  \"remote\": \"https://github.com/baidu\",//远程仓库地址\n  \"version\":1,//配置文件版本\n  \"branch\": \"master\",//配置仓库当前分支\n  \"dest\": \"Sources\",//本地仓库相对mgit根目录存放路径\n  \"repositories\": {//源码列表，包含git仓库和非git仓库\n    \"Watch\": {\n      \"remote-path\": \"watch\"//远程仓库名，对于需要mgit管理的仓库是必需字段\n    },\n    \"BBAAccount\": {\n      \"remote-path\": \"bbaaccount\",\n      \"config_repo\": true//标志是否是包含配置表的配置仓库\n    },\n    \"Script\": {\n      \"remote-path\": \"script\",\n      \"remote\": \"https://github.com/baidu/some_script\",//可选，覆盖最外层定义\n      \"branch\": \"my_branch\",//可选，覆盖最外层定义\n      \"dest\": \"New/Dir\",//可选，覆盖最外层定义，此时源码路径为\"mgit根目录/New/Dir/Script\"\n    },\n    \"Some_Repo\": {\n      \"remote-path\": \"some_repo\",\n      \"mgit-excluded\": true//指定不进行多仓库管理，可选\n    },\n    //本地git仓库或非git仓库\n    \"New_Repo\": {\n      \"dest\": \"Some/Dir\",\n      \"mgit-excluded\": true\n    }\n  }\n}\n\n--- manifest.json cache ---\n{\n  \"hash\":\"e2d85a8b0474805365db7ac1c5961f30020675af4158c2a9407239522ff76603\",//manifest.json内容的哈希\n  \"cache\":\n  {\n    \"remote\": \"https://github.com/baidu\",//远程仓库地址\n    \"version\":1,//配置文件版本\n    \"branch\": \"master\",//配置仓库当前分支\n    \"dest\": \"Sources\",//本地仓库相对mgit根目录存放路径\n    \"repositories\": {//源码列表，包含git仓库和非git仓库\n      \"Watch\": {\n        \"remote-path\": \"watch\"//远程仓库名，对于需要mgit管理的仓库是必需字段\n      },\n      \"BBAAccount\": {\n        \"remote-path\": \"bbaaccount\",\n        \"config_repo\": true//标志是否是包含配置表的配置仓库\n      },\n      \"Script\": {\n        \"remote-path\": \"script\",\n        \"remote\": \"https://github.com/some_script\",//可选，覆盖最外层定义\n        \"branch\": \"my_branch\",//可选，覆盖最外层定义\n        \"dest\": \"New/Dir\",//可选，覆盖最外层定义，此时源码路径为\"mgit根目录/New/Dir/Script\"\n      },\n      \"Some_Repo\": {\n        \"remote-path\": \"some_repo\",\n        \"mgit-excluded\": true//指定不进行多仓库管理，可选\n      },\n      //本地git仓库或非git仓库\n      \"New_Repo\": {\n        \"dest\": \"Some/Dir\",\n        \"mgit-excluded\": true\n      }\n    }\n  }\n}"
  },
  {
    "path": "lib/m-git/manifest.rb",
    "content": "#coding=utf-8\n\nrequire 'm-git/manifest/light_repo'\nrequire 'm-git/manifest/linter'\nrequire 'm-git/manifest/internal'\n\nmodule MGit\n\n  # Sample: 'm-git/manifest/manifest.sample'\n  #\n  class Manifest\n    include Linter\n    include Internal\n\n    # [Hash] 配置内容\n    attr_reader :config\n\n    # [String] 配置表内容哈希\n    # attr_reader :config_hash\n    attr_reader :hash_sha1\n\n    # [Array<LightRepo>] 与config中‘repositories’字段对应的Manifest::LightRepo对象数组，包含git仓库和非git仓库\n    attr_reader :light_repos\n\n    # [Array<LightRepo>] 类型同light_repos，表示上次操作，但本次未操作的仓库\n    attr_reader :previous_extra_light_repos\n\n    # [LightRepo] 包含配置表的配置仓库\n    attr_reader :config_repo\n\n    # [String] 配置文件路径\n    attr_reader :path\n\n    # [String] 缓存文件路径\n    attr_reader :cache_path\n\n    # [Hash] 上次改动生成的配置缓存\n    attr_reader :previous_config\n\n    # 在mgit根目录中搜索配置文件并根据配置文件生成对应LightRepo对象。\n    #\n    # [!!!!!] 请勿随意修改该接口\n    #\n    # @param root [String] mgit工作区根目录\n    #\n    # @param mgit_managed_only [Boolean] default: false，是否只获取mgit管理的git仓库\n    #\n    # @param only_exist [Boolean] default: false，是否只获取实际存在的仓库\n    #\n    # @param exclude_dummy [Boolean] default: false，是否排除dummy仓库\n    #\n    # @return [Array<LightRepo>] 仓库列表\n    #\n    def self.generate_light_repos(root, mgit_managed_only: false, only_exist:false, exclude_dummy:false)\n      config_path = File.join(root, Constants::PROJECT_DIR[:source_config])\n      config = self.parse(config_path)\n\n      if mgit_managed_only\n        repos = config.repo_list\n      elsif exclude_dummy\n        repos = config.light_repos.select { |repo| !repo.dummy }\n      else\n        repos = config.light_repos\n      end\n\n      if only_exist\n        existing_repos = []\n        repos.each { |repo|\n          abs_path = repo.abs_dest(root)\n          # 被管理的仓库验证是否是git仓库，不被管理的仓库只验证文件夹是否存在\n          if (repo.mgit_excluded && Dir.exist?(abs_path)) || (!repo.mgit_excluded && Repo.is_git_repo?(abs_path))\n            existing_repos.push(repo)\n          end\n        }\n        repos = existing_repos\n      end\n\n      return repos\n    end\n\n    # 全流程校验，除了字段合法性校验外，还包含缓存生成/校验，local配置文件校验/合并\n    #\n    # @param config_path [String] 配置文件本地路径\n    #\n    # @param strict_mode [Boolean] default: true 如果为true，出错直接报错退出，如果为false，出错抛出异常（MGit::Error）\n    #\n    # @return [Manifest] 配置对象\n    #\n    def self.parse(config_path, strict_mode:true)\n      config = Manifest.new\n      config.__setup(config_path, strict_mode)\n      return config\n    end\n\n    # 简单校验，仅校验配置内容，无缓存生成/校验，无local配置文件校验/合并\n    #\n    # @param config_content [String/Hash] 配置表json字符串或字典(key值需为String)\n    #\n    # @param strict_mode [Boolean] default: true 如果为true，出错直接报错退出，如果为false，出错抛出异常（MGit::Error）\n    #\n    # @return [Manifest] 配置对象\n    #\n    def self.simple_parse(config_content, strict_mode:true)\n      config = Manifest.new\n      config.__simple_setup(config_content, strict_mode)\n      return config\n    end\n\n\n    # 返回所有git仓库列表\n    #\n    # @param selection [Array<String>] default: nil，需要筛选的仓库名数组\n    #\n    # @param exclusion [Array<String>] default: nil，需要排除的仓库名数组\n    #\n    # @param all [Boolean] default: false 若指定为true，则忽略mgit_excluded的字段值，只要remote url存在（即有对应远程仓库），则选取\n    #\n    # @return [Array<LightRepo>] 被mgit管理的仓库生成的LightRepo数组\n    #\n    def repo_list(selection: nil, exclusion: nil, all:false)\n      list = []\n      light_repos.each { |light_repo|\n        if !light_repo.mgit_excluded || (all && !light_repo.url.nil?)\n          # 选取指定仓库\n          if (selection.nil? && exclusion.nil?) ||\n            (!selection.nil? && selection.is_a?(Array) && selection.any? { |e| e.downcase == light_repo.name.downcase }) ||\n            (!exclusion.nil? && exclusion.is_a?(Array) && !exclusion.any? { |e| e.downcase == light_repo.name.downcase })\n            list.push(light_repo)\n          end\n        end\n      }\n      list\n    end\n\n    # 获取全局配置\n    #\n    # @param key [Symbol] 配置字段【符号】\n    #\n    # @return [Object] 对应配置字段的值，可能是字符串，也可能是字典\n    #\n    def global_config(key)\n      key_str = Constants::CONFIG_KEY[key]\n      value = config[key_str]\n      terminate!(\"无法获取多仓库配置必需字段\\\"#{key}\\\"的值!\") if value.nil? && Constants::REQUIRED_CONFIG_KEY.include?(key_str)\n      value\n    end\n\n    # 更新缓存\n    #\n    # @param root [String] 多仓库根目录\n    #\n    # @param config_content [Hash] 配置Hash\n    #\n    def update_cache_with_content(root, config_content)\n      config_dir = File.join(root, Constants::PROJECT_DIR[:source_config])\n      cache_path = File.join(config_dir, Constants::CONFIG_FILE_NAME[:manifest_cache])\n      sha1 = __generate_hash_sha1(config_content.to_json)\n      CacheManager.save_to_cache(cache_path, sha1, config_content)\n    end\n\n    # 更新（对比上次操作）冗余的轻量仓库对象\n    #\n    # @param root [String] 多仓库根目录\n    #\n    def update_previous_extra_light_repos(root)\n      cache_path = File.join(root, Constants::PROJECT_DIR[:source_config],  Constants::CONFIG_FILE_NAME[:manifest_cache])\n      __load_cache(cache_path)\n    end\n\n    def terminate!(msg, type:nil)\n      if @strict_mode\n        Foundation.help!(msg)\n      else\n        raise Error.new(msg, type:type)\n      end\n    end\n\n  end\n\n  RepoConfig = Manifest\nend\n"
  },
  {
    "path": "lib/m-git/open_api/script_download_info.rb",
    "content": "#coding=utf-8\nmodule MGit\n  class OpenApi\n\n    module DownloadResult\n      SUCCESS = 0\n      FAIL = 1\n      EXIST = 2\n    end\n\n    class ScriptDownloadInfo\n\n      # [String] 下载仓库名\n      attr_reader :repo_name\n\n      # [String] 下载仓库本地地址\n      attr_reader :repo_path\n\n      # [OpenApi::DOWNLOAD_RESULT] 下载结果\n      attr_reader :result\n\n      # [String] 执行输出，若出错，该变量保存出错信息，可能为nil\n      attr_reader :output\n\n      # [Float] 当前仓库在整个下载任务中所处进度（如10个任务，当前第5个下载完，则progress=0.5），并非单仓库下载进度。\n      attr_reader :progress\n\n      def initialize(repo_name, repo_path, result, output, progress)\n        @repo_name = repo_name\n        @repo_path = repo_path\n        @result = result\n        @output = output\n        @progress = progress\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/open_api.rb",
    "content": "#coding=utf-8\n\nrequire 'm-git/open_api/script_download_info'\n\nmodule MGit\n\n  # --- Ruby环境调用 ---\n  class OpenApi\n    class << self\n\n      # 根据配置表下载仓库，并同步工作区\n      #\n      # @param config_content [String/Hash] 配置表json字符串或字典(key值需为String)，不可为nil。\n      #\n      # @param download_root [String] 仓库下载的根目录，不可为nil。\n      #\n      # Block: (OpenApi::ScriptDownloadInfo) 下载结果对象\n      #\n      # ========= 用法 ===========\n      ### info对象：[OpenApi::ScriptDownloadInfo]\n      # MGit::OpenApi.sync_repos(json,root) { |info|\n      #   puts \"name:#{info.repo_name} \\n path:#{info.repo_path}\\n result:#{info.result}\\n error:#{info.output}\\n progress:#{info.progress}\"\n      #   if info.result == OpenApi::DOWNLOAD_RESULT::EXIST\n      #   elsif info.result == OpenApi::DOWNLOAD_RESULT::SUCCESS\n      #   elsif info.result == OpenApi::DOWNLOAD_RESULT::FAIL\n      #   end\n      # }\n      def sync_repos(config_content, download_root)\n        # 空值校验\n        error = self.validate_argv(__method__, {\"config_content\" => config_content, \"download_root\" => config_content})\n        yield(ScriptDownloadInfo.new(nil, nil, DownloadResult::FAIL, error, 0)) if block_given? && !error.nil?\n\n        begin\n          config = Manifest.simple_parse(config_content, strict_mode:false)\n        rescue Error => e\n          yield(ScriptDownloadInfo.new(nil, nil, DownloadResult::FAIL, e.msg, 0)) if block_given?\n          return\n        end\n\n        # 同步工作区仓库（压入或弹出），此时配置表缓存未覆盖，同步操作若取消无需恢复缓存。\n        Utils.sync_workspace(download_root, config, recover_cache_if_cancelled:false)\n\n        # 更新配置缓存\n        config.update_cache_with_content(download_root, config.config)\n\n        # 下载\n        self.download(config, download_root, sync_exist:true) { |download_info|\n          yield(download_info) if block_given?\n        }\n\n      end\n\n      # 根据配置表下载仓库\n      #\n      # @param config_content [String/Hash] 配置表json字符串或字典(key值需为String)，不可为nil。\n      #\n      # @param download_root [String] 仓库下载的根目录，不可为nil。\n      #\n      # @param manage_git [Boolean] 新下载的仓库是否托管.git，若为false，则在工作区保留新克隆仓库的.git，否则将.git托管到.mgit/sourct-git中，并在工作区创建其软链接\n      #\n      # Block: (OpenApi::ScriptDownloadInfo) 下载结果对象\n      #\n      # ========= 用法 ===========\n      ### info对象：[OpenApi::ScriptDownloadInfo]\n      # MGit::OpenApi.download_repos(json,root) { |info|\n      #   puts \"name:#{info.repo_name} \\n path:#{info.repo_path}\\n result:#{info.result}\\n error:#{info.output}\\n progress:#{info.progress}\"\n      #   if info.result == OpenApi::DOWNLOAD_RESULT::EXIST\n      #   elsif info.result == OpenApi::DOWNLOAD_RESULT::SUCCESS\n      #   elsif info.result == OpenApi::DOWNLOAD_RESULT::FAIL\n      #   end\n      # }\n      def download_repos(config_content, download_root, manage_git:true)\n\n        # 空值校验\n        error = self.validate_argv(__method__, {\"config_content\" => config_content, \"download_root\" => config_content})\n        yield(ScriptDownloadInfo.new(nil, nil, DownloadResult::FAIL, error, 0)) if block_given? && !error.nil?\n\n        begin\n          config = Manifest.simple_parse(config_content, strict_mode:false)\n        rescue Error => e\n          yield(ScriptDownloadInfo.new(nil, nil, DownloadResult::FAIL, e.msg, 0)) if block_given?\n          return\n        end\n\n        self.download(config, download_root, manage_git:manage_git) { |download_info|\n          yield(download_info) if block_given?\n        }\n\n      end\n\n      # 将仓库切到特定的分支或commit\n      #\n      # @param repo_name [String] 仓库名，不可为nil。\n      #\n      # @param repo_path [String] 仓库本地地址，不可为nil。\n      #\n      # @param create_branch [String] 是否创建新分支。\n      #\n      # @param branch [String] 需要切换的分支，可为nil。。\n      #                         如果非nil，若仓库有该分支则直接切换，没有则在指定commit上创建。\n      #                         如果为nil，则直接切换到指定commit。\n      #\n      # @param commit_id [String] 需要切换的commit id，不可为nil。\n      #\n      # @param allow_fetch [String] 指定在本地无指定分支或commit时是否fetch后重试。\n      #\n      # @return [(String, Boolean)] (error, did_create_new_branch)\n      #                              - error: 错误信息，若无错误，返回nile\n      #                              - did_create_new_branch: 本次checkout是否创建新分支\n      # ========= 用法 ===========\n      ### 自信切commit\n      # MGit::OpenApi.checkout(name, abs_path, base_commit:'4922620')\n      #\n      ### 自信切分支，分支确定存在\n      # MGit::OpenApi.checkout(name, abs_path, branch:'master')\n      #\n      ### 尝试切分支，没有就创建\n      # MGit::OpenApi.checkout(name, abs_path, create_branch:true, branch:'master', base_commit:'4922620')\n      def checkout(repo_name, repo_path, create_branch:false, branch:nil, base_commit:nil, allow_fetch:false)\n\n        # 空值校验\n        error = self.validate_argv(__method__, {\"repo_name\" => repo_name, \"repo_path\" => repo_path})\n        return error, false if !error.nil?\n        return 'branch和base_commit必须传入一个值！', false if branch.nil? && base_commit.nil?\n\n        # 生成仓库对象\n        if Repo.is_git_repo?(repo_path)\n          repo = Repo.new(repo_name, repo_path)\n        else\n          return \"路径位置\\\"#{repo_path}\\\"不是git仓库！\", false\n        end\n\n        error = nil\n        did_create_new_branch = false\n\n        # 如果指定可fetch，那么在本地缺失信息时执行fetch\n        if allow_fetch\n\n          # 查询分支和commit\n          should_fetch = !branch.nil? && !repo.status_checker.local_branch_exist?(branch) ||\n                          !base_commit.nil? && !repo.status_checker.commit_exist?(base_commit)\n\n          error = fetch(repo.name, repo.path) if should_fetch\n\n          return error, did_create_new_branch if !error.nil?\n        end\n\n        if !branch.nil?\n\n          # 已在指定分支则不操作\n          if repo.status_checker.current_branch(strict_mode:false) != branch\n\n            # 有本地改动禁止新建/切换分支\n            if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]\n              error = \"仓库#{repo_name}有改动，无法切换/创建分支！\"\n\n            # 有本地或对应远程分支，直接切换\n            elsif repo.status_checker.local_branch_exist?(branch) || repo.status_checker.remote_branch_exist?(branch)\n              success, output = repo.execute_git_cmd('checkout', branch)\n              error = output if !success\n\n            # 无分支，在指定commit上创建\n            elsif create_branch\n\n              if !base_commit.nil?\n                if repo.status_checker.commit_exist?(base_commit)\n                  success, output = repo.execute_git_cmd('checkout', \"-b #{branch} #{base_commit}\")\n                  if !success\n                    error = output\n                  else\n                    did_create_new_branch = true\n                  end\n                else\n                  error = \"仓库#{repo_name}创建新分支时，未找到指定基点commit！\"\n                end\n              else\n                error = \"仓库#{repo_name}创建新分支时，没有指定基点commit！\"\n              end\n\n            else\n              error = \"仓库#{repo_name}无分支#{branch}，且未指定创建！\"\n            end\n\n          end\n\n        elsif !base_commit.nil?\n\n          # 已在指定commit则不操作\n          if !repo.status_checker.current_branch(strict_mode:false).nil? ||\n            repo.status_checker.current_head(strict_mode:false) != base_commit\n\n\n            # 有本地改动禁止新建/切换分支\n            if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]\n              error = \"仓库#{repo_name}有改动，无法切换HEAD！\"\n\n            # 未指定branch，直接切到指定commit\n            elsif repo.status_checker.commit_exist?(base_commit)\n              success, output = repo.execute_git_cmd('checkout', base_commit)\n              error = output if !success\n\n            # 指定commit不存在报错\n            else\n              error = \"仓库#{repo_name}切换操作失败，指定commit不存在！\"\n            end\n\n          end\n\n        end\n\n        return error, did_create_new_branch\n      end\n\n      # 执行fetch操作\n      #\n      # @param repo_name [Stirng] 仓库名\n      #\n      # @param repo_path [String] 仓库路径\n      #\n      # @return [String] 错误信息，如果执行成功返回nil\n      #\n      def fetch(repo_name, repo_path)\n        # 空值校验\n        error = self.validate_argv(__method__, {\"repo_name\" => repo_name, \"repo_path\" => repo_path})\n        return error if !error.nil?\n\n        if Dir.exist?(repo_path) && Repo.is_git_repo?(repo_path)\n          repo = Repo.new(repo_name, repo_path)\n          success, output = repo.execute_git_cmd('fetch', '')\n          return output if !success\n        else\n          return '指定路径不存在或不是git仓库！'\n        end\n      end\n\n      # 执行pull操作\n      #\n      # @param repo_name [Stirng] 仓库名\n      #\n      # @param repo_path [String] 仓库路径\n      #\n      # @return [String] 错误信息，如果执行成功返回nil\n      #\n      def pull(repo_name, repo_path)\n        # 空值校验\n        error = self.validate_argv(__method__, {\"repo_name\" => repo_name, \"repo_path\" => repo_path})\n        return error if !error.nil?\n\n        if Dir.exist?(repo_path) && Repo.is_git_repo?(repo_path)\n          repo = Repo.new(repo_name, repo_path)\n          success, output = repo.execute_git_cmd('pull', '')\n          return output if !success\n        else\n          return '指定路径不存在或不是git仓库！'\n        end\n      end\n\n      # 检查给定仓库是否工作区脏，工作区的.git是否是软链接\n      #\n      # @param path_dict [Hash] 待校验的冗余仓库地址：{ \"name\": \"abs_path\" }\n      #\n      # @return [Hash] 冗余仓库状态信息：{ \"name\": { \"clean\": true, \"git_linked\": true , \"error\": \"error_msg\"} }\n      #                  - \"clean\": 为true，表示工作区干净，否则脏。\n      #                  - \"git_linked\": 为true，表示.git实体放在.mgit/source-git下，工作区.git仅为软链接，否则为实体。\n      #                  - \"error\": 如果校验成功则无该字段，否则校验失败，值为错误信息。注意，若出错则无\"clean\"和\"git_linked\"字段。\n      #\n      def check_extra_repos(path_dict)\n\n        # 空值校验\n        error = self.validate_argv(__method__, {\"path_dict\" => path_dict})\n        return {} if !error.nil?\n\n        output = {}\n        path_dict.each { |name, path|\n          output[name] = {}\n          # 生成仓库对象\n          if Repo.is_git_repo?(path)\n            repo = Repo.new(name, path)\n            output[name]['clean'] = repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:clean]\n            output[name]['git_linked'] = File.symlink?(File.join(path, '.git'))\n          else\n            output[name]['error'] = \"路径位置\\\"#{repo_path}\\\"不是git仓库！\"\n          end\n        }\n        return output\n      end\n\n      # 并发遍历\n      #\n      # @param array [<Object>] 遍历数组\n      #\n      # @param max_concurrent_count [Integer] 最大并发数\n      #\n      # ========= 用法 ===========\n      # concurrent_enumerate([item1, item2...]) { |item|\n      #  do something with item...\n      # }\n      def concurrent_enumerate(array, max_concurrent_count:5)\n        if array.is_a?(Array) && array.length > 0\n          array.peach(max_concurrent_count) { |item|\n            yield(item) if block_given?\n          }\n        end\n      end\n\n      # 在不拉仓库的情况下，批量查询当前用户是否有权限拉取代码\n      #\n      # @param root [String] 工程根目录\n      #\n      # @param url_list [Array<String>] 一组远程仓库url\n      #\n      # ========= 用法 ===========\n      # url_list = [\n      #   'https://github.com/baidu/baiduapp-platform/a',\n      #   'https://github.com/baidu/baiduapp-platform/b'\n      # ]\n      # MGit::OpenApi.check_permission_batch(arr) { |url, has_permission, progress|\n      #   do something...\n      # }\n      def check_permission_batch(root, url_list)\n        mutex = Mutex.new\n        task_count = 0\n        total_task = url_list.length\n        concurrent_enumerate(url_list) { |url|\n          has_permission = self.check_permission(url, root:root)\n          mutex.lock\n          task_count += 1\n          progress = Float(task_count) / total_task\n          yield(url, has_permission, progress) if block_given?\n          mutex.unlock\n        }\n      end\n\n      # 在不拉仓库的情况下，查询当前用户是否有权限拉取代码\n      #\n      # @param root [String] 工程根目录\n      #\n      # @param url [String] 远程仓库url\n      #\n      # @return [Boolean] 是否有权限\n      #\n      # ========= 用法 ===========\n      # url = 'https://github.com/baidu/baiduapp-platform/a'\n      # result = MGit::OpenApi.check_permission(url)\n      def check_permission(url, root:nil)\n        if !root.nil?\n          git_store = Utils.generate_git_store(root, url)\n          if !git_store.nil? && Dir.exist?(git_store)\n            return true\n          end\n        end\n\n        return Utils.has_permission_of_remote?(url)\n      end\n\n      # ----- Util -----\n      # 校验参数合法性\n      #\n      # @param method_name [String] 方法名\n      #\n      # @param args [Hash] 参数数组\n      #\n      # @return [String] 错误信息，正常返回nil\n      #\n      def validate_argv(method_name, args)\n        args.each { |k,v|\n          if v.nil?\n            return \"MGit API调用错误: MGit::OpenApi.#{method_name}()的#{k}参数不能为空！\"\n          end\n        }\n        return nil\n      end\n\n      # 根据配置对象下载仓库\n      #\n      # @param config_content [String/Hash] 配置表json字符串或字典(key值需为String)，不可为nil。\n      #\n      # @param download_root [String] 仓库下载的根目录，不可为nil。\n      #\n      # @param manage_git [Boolean] 新下载的仓库是否托管.git，若为false，则在工作区保留新克隆仓库的.git，否则将.git托管到.mgit/sourct-git中，并在工作区创建其软链接\n      #\n      # ========= 用法 ===========\n      # self.download(config, download_root) { |download_info|\n      #   do something with download_info....\n      # }\n      def download(config, download_root, manage_git:true, sync_exist: false)\n        task_count = 0\n        total_task = config.light_repos.length\n        mutex = Mutex.new\n        concurrent_enumerate(config.light_repos) { |light_repo|\n          name = light_repo.name\n          path = light_repo.abs_dest(download_root)\n          result = nil\n          output = nil\n          progress = 0\n\n          if Dir.exist?(path) && Repo.is_git_repo?(path)\n            if sync_exist\n              repo, error = Repo.generate_softly(download_root, light_repo)\n              error = Repo::SyncHelper.sync_exist_repo(repo, repo.config) if !repo.nil?\n            end\n            result = DownloadResult::EXIST\n          elsif Utils.has_permission_of_remote?(light_repo.url)\n            error, repo = Repo::SyncHelper.sync_new_repo(light_repo, download_root, link_git:manage_git)\n            if !error.nil?\n              result = DownloadResult::FAIL\n              output = error\n            else\n              result = DownloadResult::SUCCESS\n            end\n          else\n            result = DownloadResult::FAIL\n            output = \"当前用户没有该仓库的克隆权限：#{light_repo.name}(#{light_repo.url})！\"\n          end\n\n          mutex.lock\n          task_count += 1\n          progress = Float(task_count) / total_task\n          yield(ScriptDownloadInfo.new(name, path, result, output, progress)) if block_given?\n          mutex.unlock\n        }\n      end\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/output/output.rb",
    "content": "#coding=utf-8\n\nrequire 'm-git/foundation'\n\nmodule MGit\n  module Output\n    class << self\n\n# --- 简单输出 ---\n\n      def puts_cancel_message\n        puts_fail_message(\"执行取消！\")\n      end\n\n      def puts_succeed_cmd(cmd)\n        puts_success_message(\"指令[ mgit #{cmd} ]执行成功！\")\n      end\n\n      def puts_start_cmd\n        puts_processing_message(\"开始执行...\")\n      end\n\n      def puts_fail_cmd(cmd)\n        puts_fail_message(\"指令[ mgit #{cmd} ]执行失败！\")\n      end\n\n      def puts_nothing_to_do_cmd\n        puts_remind_message(\"没有仓库需要执行指令！\")\n      end\n\n      def puts_success_message(str)\n        MGit::Loger.info(str.strip)\n        puts success_message(str)\n      end\n\n      def puts_remind_message(str)\n        MGit::Loger.info(str)\n        puts remind_message(str)\n      end\n\n      def puts_terminate_message(str, title:nil)\n        MGit::Loger.error(\"#{title}: #{str}\")\n        puts terminate_message(str, title:title)\n      end\n\n      def puts_fail_message(str)\n        MGit::Loger.error(str)\n        puts fail_message(str)\n      end\n\n      def puts_processing_message(str)\n        MGit::Loger.info(str)\n        puts processing_message(str)\n      end\n\n      def puts_fail_block(list, bottom_summary)\n        puts generate_fail_block(list, bottom_summary)\n      end\n\n      def puts_remind_block(list, bottom_summary)\n        puts generate_remind_block(list, bottom_summary)\n      end\n\n      def puts_processing_block(list, bottom_summary)\n        puts generate_processing_block(list, bottom_summary)\n      end\n\n      def puts_fail_combined_block(list_array, bottom_summary, title:nil)\n        puts generate_fail_combined_block(list_array, bottom_summary, title:title)\n      end\n\n      def puts_in_pager(output)\n        begin\n          pager = TTY::Pager.new\n          pager.page(output)\n        rescue => _\n        end\n      end\n\n# --- 交互式输出 ---\n\n      def continue_with_user_remind?(msg)\n        puts blue_message(\"[?] #{msg} Y/n\")\n        MGit::DurationRecorder.pause\n        input = nil\n        loop do\n          input = STDIN.gets.chomp.downcase\n          if input == 'y' || input == 'yes' || input == 'n' || input == 'no'\n            break\n          end\n          puts blue_message(\"[?] 输入不合法,#{msg} Y/n\")\n        end\n        MGit::DurationRecorder.resume\n        return input == 'y' || input == 'yes'\n      end\n\n      def continue_with_interact_repos?(repos, msg)\n        output = generate_table(repos, separator:'|') + \"\\n\"\n        output += blue_message(\"[?] #{msg} Y/n\")\n        puts output\n        MGit::DurationRecorder.pause\n        input = nil\n        loop do\n          input = STDIN.gets.chomp.downcase\n          if input == 'y' || input == 'yes' || input == 'n' || input == 'no'\n            break\n          end\n          puts blue_message(\"[?] 输入不合法,#{msg} Y/n\")\n        end\n        MGit::DurationRecorder.resume\n        return input == 'y' || input == 'yes'\n      end\n\n      def continue_with_combined_interact_repos?(repos_array, msg, title:nil)\n        output = generate_table_combination(repos_array, title:title, separator:'|') + \"\\n\"\n        output += blue_message(\"[?] #{msg} Y/n\")\n        puts output\n        MGit::DurationRecorder.pause\n        input = nil\n        loop do\n          input = STDIN.gets.chomp.downcase\n          if input == 'y' || input == 'yes' || input == 'n' || input == 'no'\n            break\n          end\n          puts blue_message(\"[?] 输入不合法,#{msg} Y/n\")\n        end\n        MGit::DurationRecorder.resume\n        return input == 'y' || input == 'yes'\n      end\n\n# 显示一组复合表格并显示多选项操作\n#\n# @param list_array [Array<Array>] 包含表格内容的数组，其中每个元素为数组，表示一张表格内容，list_array内容为：【【title1, list1】,【title2, list2】...】, title<String>为标题，list<Array>为单张表格元素数组，所有内容会被渲染为多张表格然后合并为一张\n#\n# @param msg [string] 交互消息\n#\n# @param selection [Array] 选型数组，如:【'a: 跳过并继续', 'b: 强制执行', 'c: 终止'】\n#\n      def interact_with_multi_selection_combined_repos(list_array, msg, selection)\n        puts generate_table_combination(list_array, separator:'|')\n        puts blue_message(\"[?] #{msg}，请选择操作：\\n#{selection.join(\"\\n\")}\")\n        MGit::DurationRecorder.pause\n        input = STDIN.gets.chomp\n        MGit::DurationRecorder.resume\n        yield(input) if block_given?\n      end\n\n# --- 特定消息生成器 ---\n\n      def processing_message(str)\n        return yellow_message(\"[~] #{str}\")\n      end\n\n      def remind_message(str)\n        return blue_message(\"[!] #{str}\")\n      end\n\n      def success_message(str)\n        return green_message(\"[✔] #{str}\")\n      end\n\n      def fail_message(str)\n        return red_message(\"[✘] #{str}\")\n      end\n\n      def terminate_message(str, title:nil)\n        header = \"执行终止\"\n        header = title if !title.nil?\n        return red_message(\"[✘✘✘ #{header} ✘✘✘] #{str}\")\n      end\n\n# --- 有色输出生成器 ---\n\n# 绿色提示信息\n      def green_message(str)\n        return \"\\033[32m#{str}\\033[0m\"\n      end\n\n# 青色提示信息\n      def blue_message(str)\n        return \"\\033[36m#{str}\\033[0m\"\n      end\n\n# 紫红色提示信息\n      def red_message(str)\n        return \"\\033[31m#{str}\\033[0m\"\n      end\n\n# 黄色提示信息\n      def yellow_message(str)\n        return \"\\033[33m#{str}\\033[0m\"\n      end\n\n      def info_title(str)\n        return \"\\033[4m#{str}\\033[0m\"\n      end\n\n# --- 格式化输出生成器 ---\n\n      def generate_fail_combined_block(list_array, bottom_summary, title:nil)\n        return '' if list_array.nil? || list_array.length == 0\n        output = generate_table_combination(list_array, title: title, separator:'|') + \"\\n\"\n        output += fail_message(bottom_summary)\n        return output\n      end\n\n      def generate_remind_block(list, bottom_summary)\n        msg = generate_block(list, remind_message(bottom_summary))\n        MGit::Loger.info(bottom_summary)\n        return msg\n      end\n\n      def generate_fail_block(list, bottom_summary)\n        msg = generate_block(list, fail_message(bottom_summary))\n        MGit::Loger.info(bottom_summary)\n        return msg\n      end\n\n      def generate_processing_block(list, bottom_summary)\n        msg = generate_block(list, processing_message(bottom_summary))\n        MGit::Loger.info(bottom_summary)\n        return msg\n      end\n\n      def generate_block(list, bottom_summary)\n        return '' if list.nil? || list.length == 0\n        output = generate_table(list, separator:'|') + \"\\n\"\n        output += bottom_summary\n        MGit::Loger.info(list)\n        return output\n      end\n\n      def generate_title_block(title, has_separator:true)\n        title = \"--- #{title} ---\"\n        separator = ''\n        separator = \"\\n\" + '=' * string_length_by_ascii(title) if has_separator\n        output = blue_message(title + separator) + \"\\n\"\n        output += yield(output)\n        return output\n      end\n\n# 生成合成表格(将多个表格融合为一个)\n#\n# @param list_array [Array<Array>] 包含表格内容的数组，其中每个元素为数组，表示一张表格内容，list_array内容为：【【title1, list1】,【title2, list2】...】, title<String>为标题，list<Array>为单张表格元素数组，所有内容会被渲染为多张表格然后合并为一张\n#\n# @param title [String] default: nil 表格标题\n#\n# @param separator [String] default: '' 表格内元素分割线\n#\n# @return [String] 生成的合成表格\n#\n      def generate_table_combination(list_array, title:nil, separator:'')\n        table_width = -1\n        head_separator = \"| \"\n        middle_separator = \" #{separator} \"\n        tail_separator = \" |\"\n        head_tail_padding = head_separator.length + tail_separator.length\n\n        list_array.each { |list|\n          items = list.last\n          max_table_width, _, _ = calculate_table_info(items, head_separator, middle_separator, tail_separator, title:list.first)\n          table_width = max_table_width if !max_table_width.nil? && max_table_width > table_width\n        }\n\n        output = ''\n        if !title.nil? && table_width > 0\n\n          title_length = string_length_by_ascii(title)\n          table_width = title_length + head_tail_padding if table_width < title_length + head_tail_padding\n\n          space = secure(table_width - head_tail_padding - title_length) / 2\n          head_line = head_separator + ' ' * space +  title\n          head_line = head_line + ' ' * secure(table_width - string_length_by_ascii(head_line) - tail_separator.length) + tail_separator + \"\\n\"\n\n          output += '-' * table_width + \"\\n\"\n          output += head_line\n        end\n\n        list_array.each_with_index { |list, idx|\n          sub_title = list.first if !list.first.nil? && list.first != ''\n          output += generate_table(list.last, title:sub_title, separator:separator, fixed_width:table_width, hide_footer_line:idx != list_array.length - 1)\n        }\n        return output\n      end\n\n# 生成表格\n#\n# @param list [Array] 包含表格中显示内容\n#\n# @param title [String] default: nil 表格标题\n#\n# @param separator [String] default: '' 表格内部分割线\n#\n# @param fixed_width [Number] default: -1 可指定表格宽度，若指定宽度大于计算所得最大宽度，则使用该指定宽度\n#\n# @param hide_footer_line [Boolean] default: false 隐藏底部分割线\n#\n# @return [String] 已生成的表格字符串\n#\n      def generate_table(list, title:nil, separator:'', fixed_width:-1, hide_footer_line:false)\n        return '' if list.nil? || list.length == 0\n\n        output = ''\n        head_separator = \"| \"\n        middle_separator = \" #{separator} \"\n        tail_separator = \" |\"\n        head_tail_padding = head_separator.length + tail_separator.length\n\n        max_table_width, column, max_meta_display_length_by_ascii = calculate_table_info(list, head_separator, middle_separator, tail_separator, title:title, fixed_width:fixed_width)\n\n        if !max_table_width.nil? && !column.nil? && !max_meta_display_length_by_ascii.nil?\n          if !title.nil?\n            title_length = string_length_by_ascii(title)\n            title = head_separator + title + ' ' * secure(max_table_width - title_length - head_tail_padding) + tail_separator\n\n            max_table_width = title_length + head_tail_padding if max_table_width < title_length + head_tail_padding\n            output += '-' * max_table_width + \"\\n\"\n            output += title + \"\\n\"\n\n            # 处理标题下的分割线\n            output += head_separator + '-' * title_length + ' ' * secure(max_table_width - head_tail_padding - title_length) + tail_separator + \"\\n\"\n          else\n            output += '-' * max_table_width + \"\\n\"\n          end\n\n          list.each_slice(column).to_a.each { |row|\n            line = head_separator\n            row.each_with_index { |item, idx|\n              # 最大显示宽度由纯ascii字符个数度量，而输出时中文占2个ascii字符宽度\n              # ljust方法以字符做计算，一个汉字会被认为是一个字符，但占了2单位宽度，需要把显示宽度根据汉字个数做压缩，如:\n              # 最长显示字符（纯ascii字符）：'abcdef1234', 字符长度10，占10个ascii字符显示宽度\n              # 需要输出字符串：'[删除]abc'，字符长度7，但有2个汉字，占9个ascii字符显示宽度\n              # 因此，ljust接受宽度字符个数：10 - 2 = 8\n              # 即对于'[删除]abc'这样的字符串，8个字符对应的ascii字符显示宽度（'[删除]abc ', 注意8个字符宽度此时有一个空格）和'abcdef1234'显示宽度等长\n              line += item.ljust(max_meta_display_length_by_char(item, max_meta_display_length_by_ascii))\n              line += middle_separator if idx != row.length - 1\n            }\n            last_line_space = ' ' * secure(max_table_width - string_length_by_ascii(line) - tail_separator.length)\n            line += last_line_space + tail_separator\n            output += line + \"\\n\"\n          }\n          output += '-' * max_table_width if !hide_footer_line\n        else\n          if list.length > 1\n            output = list.join(\"\\n\")\n          else\n            output = list.first + \"\\n\"\n          end\n        end\n\n        return output\n      end\n\n# 计算表格信息\n#\n# @param list [Array] 包含表格中显示内容\n#\n# @param head_separator [String] 表格头部分割线\n#\n# @param middle_separator [String] 表格中部分割线\n#\n# @param tail_separator [String] 表格尾部分割线\n#\n# @param fixed_width [Number] default: -1 可指定表格宽度，若指定宽度大于计算所得最大宽度，则使用该指定宽度\n#\n# @return [Objtct...] 返回表格最大宽度，表格列数，表格中一个项目显示的最大长度\n#\n      def calculate_table_info(list, head_separator, middle_separator, tail_separator, title:nil, fixed_width:-1)\n        return nil if list.nil? || list.length == 0\n\n        head_tail_padding = head_separator.length + tail_separator.length\n\n        max_meta_display_length_by_ascii = -1\n        list.each { |item|\n          display_length = string_length_by_ascii(item)\n          if max_meta_display_length_by_ascii < display_length\n            max_meta_display_length_by_ascii = display_length\n          end\n        }\n\n        # 终端宽度。\n        # -1：减去最后一行\"\\n\"\n        screen_width = `tput cols`.to_i - 1\n        if screen_width > 0 && screen_width > max_meta_display_length_by_ascii + head_tail_padding\n          # 定义：\n          # n：列数\n          # a：单个item最大长度\n          # b：分隔字符长度\n          # l：表格宽度减去前后padding的长度\n          # 如：| [abc]def | [def]abcd | [zz]asdf |\n          # 有：  |<------------ l ------------>|\n          #                 |<- a -->|\n          # n=3，a=9，b=3(' | '.length == 3)，l=31\n\n          # 计算l\n          raw_length = screen_width - head_tail_padding\n\n          # 计算列数：n * a + (n - 1) * b = l => n = (l + b) / (a + b)\n          column = (raw_length + middle_separator.length) / (max_meta_display_length_by_ascii + middle_separator.length)\n\n          # 如果计算得到的列数小于item个数，那么取item个数为最大列数\n          column = list.length if column > list.length\n\n          # 计算一个item的平均长度“al”，由\n          # 1. n * a + (n - 1) * b = l\n          # 2. al = l / n\n          # => al = (n * (a + b) - b) / n\n          average_length = (column * (max_meta_display_length_by_ascii + middle_separator.length) - middle_separator.length) / column.to_f\n\n          # 表格最大宽度为：n * al + head_tail_padding\n          max_table_width = (column * average_length + head_tail_padding).ceil\n\n          # 如果title最宽，则使用title宽度\n          if !title.nil? && title.is_a?(String)\n            title_length = string_length_by_ascii(head_separator + title + tail_separator)\n            max_table_width = title_length if max_table_width < title_length && title_length < screen_width\n          end\n\n          # 如果指定了一个合理的宽度，则使用该宽度\n          max_table_width = fixed_width if max_table_width < fixed_width && fixed_width <= screen_width\n\n          return max_table_width, column, max_meta_display_length_by_ascii\n        else\n          return nil\n        end\n      end\n\n# 输出时中文占2个字符宽度，根据字符串中的汉字个数重新计算字符串长度（以纯ascii字符作度量）\n      def string_length_by_ascii(str)\n        chinese_chars = str.scan(/\\p{Han}/)\n        length = str.length + chinese_chars.length\n        return length\n      end\n\n# 输出时中文占2个字符宽度，根据字符串中的汉字个数重新计算最大显示字符长度（以包括汉字在内的字符作度量）\n      def max_meta_display_length_by_char(str, max_meta_display_length_by_ascii)\n        chinese_chars = str.scan(/\\p{Han}/)\n        return max_meta_display_length_by_ascii - chinese_chars.length\n      end\n\n# 保证数字大于0\n      def secure(num)\n        return num > 0 ? num : 0\n      end\n\n# --- 进度条 ---\n\n      def update_progress(totaltasks, finishtasks)\n        totalmark = 30\n        progress = totaltasks > 0 ? (finishtasks * 100.0 / totaltasks).round : 100\n        progress_str = ''\n        pre_num = (totalmark / 100.0 * progress).round\n        progress_str += '#' * pre_num\n        progress_str += ' ' * (totalmark - pre_num)\n        bar = \"\\r[#{progress_str}] #{progress}%\"\n        bar += \"\\n\" if progress == 100\n        print bar\n      end\n    end\n  end\nend"
  },
  {
    "path": "lib/m-git/plugin/completion.bash",
    "content": "_mgit_completion() {\n  COMPREPLY=()\n\n  local completions word argc\n  word=\"${COMP_WORDS[COMP_CWORD]}\"\n  argc=${#COMP_WORDS[@]}\n\n  if [[ $argc < 2 ]]; then\n    return\n  elif [[ $argc == 2 ]]; then\n    completions=\"$(mgit script --commands)\"\n  elif [[ $argc > 2 ]]; then\n    completions=\"$(mgit script --list --all --commands ${COMP_WORDS[1]})\"\n  fi\n\n  COMPREPLY=( $(compgen -W \"$completions\" -- \"$word\") )\n}\n\ncomplete -F _mgit_completion mgit\n"
  },
  {
    "path": "lib/m-git/plugin/completion.zsh",
    "content": "_mgit_completion() {\n\n  if [[ ${#words[@]} == 2 ]]; then\n    # Complete command\n    completions=\"$(mgit script --commands)\"\n    reply=( \"${(ps:\\n:)completions}\" )\n  elif [[ ${#words[@]} > 2 ]]; then\n    # Complete repo name and command's options\n    # Array index starts at 1\n    local cmd\n    cmd=$words[2]\n    completions=\"$(mgit script --all --list --commands $cmd)\"\n    reply=( \"${(ps:\\n:)completions}\" )\n  fi\n}\n\ncompctl -K _mgit_completion mgit\n"
  },
  {
    "path": "lib/m-git/plugin/init.sh",
    "content": "if [ -n \"$BASH_VERSION\" ]; then\n  plugin_root=\"$(dirname \"${BASH_SOURCE[0]}\")\"\n  source \"$plugin_root/completion.bash\"\n\nelif [ -n \"$ZSH_VERSION\" ]; then\n  plugin_root=\"$(dirname \"$0\")\"\n  source \"$plugin_root/completion.zsh\"\nfi\n"
  },
  {
    "path": "lib/m-git/plugin_manager.rb",
    "content": "\nmodule MGit\n  class PluginManager\n\n    # @!scope 加载插件\n    # 1. 先加载本地源码插件\n    # 2. 搜索加载gem插件\n    # 3. 处理加载注入的插件\n    #\n    def self.setup\n      lib_dir = File.dirname(__FILE__)\n      plugins_dir = File.join(File.dirname(File.dirname(lib_dir)), 'plugins')\n      load_local_plugin_dir('mgit', plugins_dir)\n      load_local_plugin_dir('m-git', plugins_dir)\n      load_gem_plugins('mgit')\n      load_gem_plugins('m-git')\n\n      inject_flag = '--inject='.freeze\n      inject_arg = ::ARGV.find { |arg| arg.start_with?(inject_flag) }\n      if inject_arg\n        ::ARGV.delete(inject_arg)\n        inject_file = inject_arg[inject_flag.length..-1]\n        if !inject_file.start_with?('~') && !inject_file.start_with?('/')\n          inject_file = File.join(Dir.pwd, inject_file)\n        end\n        inject_file = File.expand_path(inject_file)\n        if File.exist?(inject_file)\n          if File.file?(inject_file)\n            require inject_file\n          elsif File.directory?(inject_file)\n            load_local_plugins('mgit', inject_file)\n            load_local_plugins('m-git', inject_file)\n          end\n        end\n      end\n    end\n\n    # 加载本地的plugin优先，然后加载gem的plugin\n    # [Hash{String=> [String]}]\n    #\n    def self.loaded_plugins\n      @loaded_plugins ||= {}\n    end\n\n    # 加载插件集合目录，该目录下每个文件夹遍历加载一次\n    #\n    def self.load_local_plugin_dir(plugin_prefix, plugins_dir)\n      Dir.foreach(plugins_dir) do |file|\n        next if file == '.' || file == '..' || file == '.DS_Store'\n        plugin_root = File.join(plugins_dir, file)\n        next unless File.directory?(plugin_root)\n        load_local_plugins(plugin_prefix, plugin_root, file)\n      end if Dir.exist?(plugins_dir)\n    end\n\n    # 加载单个本地插件\n    #\n    def self.load_local_plugins(plugin_prefix, plugin_root, with_name = nil)\n      with_name ||= plugin_root\n      glob = \"#{plugin_prefix}_plugin#{Gem.suffix_pattern}\"\n      glob = File.join(plugin_root, '**', glob)\n      plugin_files = Dir[glob].map { |f| f.untaint }\n      return if loaded_plugins[with_name] || plugin_files.nil? || plugin_files.empty?\n      safe_activate_plugin_files(with_name, plugin_files)\n      loaded_plugins[with_name] = plugin_files\n    end\n\n    # 加载已安装的gem插件\n    #\n    def self.load_gem_plugins(plugin_prefix)\n      glob = \"#{plugin_prefix}_plugin#{Gem.suffix_pattern}\"\n      gem_plugins = Gem::Specification.latest_specs.map do |spec|\n        matches = spec.matches_for_glob(glob)\n        [spec, matches] unless matches.empty?\n      end.compact\n\n      gem_plugins.map do |spec, paths|\n        next if loaded_plugins[spec.name]\n        safe_activate_gem(spec, paths)\n        loaded_plugins[spec.full_name] = paths\n      end\n    end\n\n    def self.safe_activate_gem(spec, paths)\n      spec.activate\n      paths.each { |path| require(path) }\n      true\n    rescue Exception => exception # rubocop:disable RescueException\n      message = \"\\n---------------------------------------------\"\n      message << \"\\n加载插件失败 `#{spec.full_name}`.\\n\"\n      message << \"\\n#{exception.class} - #{exception.message}\"\n      message << \"\\n#{exception.backtrace.join(\"\\n\")}\"\n      message << \"\\n---------------------------------------------\\n\"\n      warn message.ansi.yellow\n      false\n    end\n\n    def self.safe_activate_plugin_files(plugin_name, paths)\n      paths.each { |path| require(path) }\n      true\n    rescue Exception => exception\n      message = \"\\n---------------------------------------------\"\n      message << \"\\n加载插件失败 `#{plugin_name}`.\\n\"\n      message << \"\\n#{exception.class} - #{exception.message}\"\n      message << \"\\n#{exception.backtrace.join(\"\\n\")}\"\n      message << \"\\n---------------------------------------------\\n\"\n      warn message.ansi.yellow\n      false\n    end\n\n  end\nend\n"
  },
  {
    "path": "lib/m-git/repo/status.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  class Repo\n    class Status\n\n      # https://git-scm.com/docs/git-status\n      # status格式：XY PATH, X表示暂存区状态，Y表示工作区状态，在merge冲突的情况下，XY为冲突两边状态，未跟踪文件为??，忽略文件为!!。\n      # 如：\n      # MM some_file\n      #  M some_file\n      # M  some_file\n      #\n      # 其中：\n      # ' ' = unmodified\n      # M = modified\n      # A = added\n      # D = deleted\n      # R = renamed\n      # C = copied\n      # U = updated but unmerged\n\n      # 具体规则：\n      # X          Y     Meaning\n      # -------------------------------------------------\n      #   [AMD]   not updated\n      # M        [ MD]   updated in index\n      # A        [ MD]   added to index\n      # D                deleted from index\n      # R        [ MD]   renamed in index\n      # C        [ MD]   copied in index\n      # [MARC]           index and work tree matches\n      # [ MARC]     M    work tree changed since index\n      # [ MARC]     D    deleted in work tree\n      # [ D]        R    renamed in work tree\n      # [ D]        C    copied in work tree\n      # -------------------------------------------------\n      # D           D    unmerged, both deleted\n      # A           U    unmerged, added by us\n      # U           D    unmerged, deleted by them\n      # U           A    unmerged, added by them\n      # D           U    unmerged, deleted by us\n      # A           A    unmerged, both added\n      # U           U    unmerged, both modified\n      # -------------------------------------------------\n      # ?           ?    untracked\n      # !           !    ignored\n      # -------------------------------------------------\n\n      FILE_STATUS = {\n          :unmodified     =>  ' ',\n          :modified       =>  'M',\n          :added          =>  'A',\n          :deleted        =>  'D',\n          :renamed        =>  'R',\n          :copied         =>  'C'\n      }.freeze\n\n      FILE_STATUS_CONFLICT = {\n          :both_deleted   => 'DD',\n          :we_added       => 'AU',\n          :they_deleted   => 'UD',\n          :they_added     => 'UA',\n          :we_deleted     => 'DU',\n          :both_added     => 'AA',\n          :both_modified  => 'UU'\n      }.freeze\n\n      FILE_STATUS_SPECIAL = {\n          :untracked      => '??',\n          :ignored        => '!!'\n      }.freeze\n\n      FILE_STATUS_MESSAGE = {\n          FILE_STATUS[:unmodified].to_s               =>  nil,\n          FILE_STATUS[:modified].to_s                 => '[已修改]',\n          FILE_STATUS[:added].to_s                    => '[已添加]',\n          FILE_STATUS[:deleted].to_s                  => '[已删除]',\n          FILE_STATUS[:renamed].to_s                  => '[重命名]',\n          FILE_STATUS[:copied].to_s                   => '[已拷贝]',\n          FILE_STATUS_CONFLICT[:both_deleted].to_s    => '[删除|删除]',\n          FILE_STATUS_CONFLICT[:we_added].to_s        => '[添加|修改]',\n          FILE_STATUS_CONFLICT[:they_deleted].to_s    => '[修改|删除]',\n          FILE_STATUS_CONFLICT[:they_added].to_s      => '[修改|添加]',\n          FILE_STATUS_CONFLICT[:we_deleted].to_s      => '[删除|修改]',\n          FILE_STATUS_CONFLICT[:both_added].to_s      => '[添加|添加]',\n          FILE_STATUS_CONFLICT[:both_modified].to_s   => '[修改|修改]',\n          FILE_STATUS_SPECIAL[:untracked].to_s        => '[未跟踪]',\n          FILE_STATUS_SPECIAL[:ignored].to_s          => '[被忽略]'\n      }.freeze\n\n      STATUS_TYPE = {\n          :normal         => 1,\n          :conflicts      => 2,\n          :special        => 3\n      }.freeze\n\n      GIT_REPO_STATUS = {\n          :clean          =>  'clean',\n          :dirty          =>  'dirty'\n      }.freeze\n\n      GIT_REPO_STATUS_DIRTY_ZONE = {\n          :index          =>  1,      # 暂存区\n          :work_tree      =>  1 << 1, # 工作区\n          :special        =>  1 << 2  # 未跟踪和被ignore\n      }.freeze\n\n      GIT_BRANCH_STATUS = {\n          :ahead          =>  'ahead',\n          :behind         =>  'behind',\n          :detached       =>  'detached',\n          :diverged       =>  'diverged',\n          :no_remote      =>  'no_remote',\n          :no_tracking    =>  'no_tracking',\n          :up_to_date     =>  'up_to_date'\n      }.freeze\n\n      def initialize(path)\n        @path = path\n        @status, @message = nil, nil\n        @branch_status, @branch_message, @dirty_zone = nil, nil, nil\n      end\n\n      def status\n        check_repo_status if @status.nil?\n        return @status\n      end\n\n      def message\n        check_repo_status if @message.nil?\n        return @message\n      end\n\n      def dirty_zone\n        check_repo_status if @dirty_zone.nil?\n        return @dirty_zone\n      end\n\n      def branch_status\n        check_branch_status if @branch_status.nil?\n        return @branch_status\n      end\n\n      def branch_message\n        check_branch_status if @branch_message.nil?\n        return @branch_message\n      end\n\n      # 是否处于merge中间态\n      def is_in_merge_progress?\n        cmd = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" merge HEAD\"\n        Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|\n          return !status.success?\n        }\n      end\n\n      # 是否处于rebase中间态\n      def is_in_rebase_progress?\n        cmd = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" rebase HEAD\"\n        Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|\n          return !status.success?\n        }\n      end\n\n      # 当前分支是否是某个分支的祖先\n      def is_ancestor_of_branch?(branch)\n        c_branch = current_branch\n        cmd  = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" merge-base --is-ancestor #{c_branch} #{branch}\"\n        cmd2 = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" rev-parse --verify #{branch}\"\n        cmd3 = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" rev-parse --verify #{c_branch}\"\n\n        is_ancestor = false\n        Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|\n          is_ancestor = status.success?\n        }\n\n        # 当两个分支指向同一个commit的时候，“merge-base --is-ancestor”指令依然返回true，这里判断如果是这样当情况，就返回false\n        if is_ancestor\n          branch_hash = nil\n          Utils.execute_shell_cmd(cmd2) { |stdout, stderr, status|\n            branch_hash = stdout.chomp if status.success?\n          }\n\n          c_branch_hash = nil\n          Utils.execute_shell_cmd(cmd3) { |stdout, stderr, status|\n            c_branch_hash = stdout.chomp if status.success?\n          }\n          return !branch_hash.nil? && !c_branch_hash.nil? && branch_hash != c_branch_hash\n        else\n          return false\n        end\n      end\n\n      # 查询追踪的远程分支\n      #\n      # @param branch [String] 查询分支\n      #\n      # @param use_cache [Boolean] default: false，是否使用缓存\n      #\n      # @return [String] 追踪的远程分支\n      #\n      def tracking_branch(branch, use_cache:false)\n        return @tracking_branch if use_cache && !@tracking_branch.nil?\n\n        cmd = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" rev-parse --abbrev-ref #{branch}@{u}\"\n        Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|\n          if status.success?\n            @tracking_branch = stdout.chomp\n            return @tracking_branch\n          end\n          return nil\n        }\n      end\n\n      # 查询当前分支\n      #\n      # @param strict_mode [Boolean] default: true，是否是严格模式。在严格模式下，失败即终止。在非严格模式下，失败返回nil。\n      #\n      # @param use_cache [Boolean] default: false，是否使用缓存\n      #\n      # @return [String] 当前分支，查询失败或游离返回nil\n      #\n      def current_branch(strict_mode:true, use_cache:false)\n        return @current_branch if use_cache && !@current_branch.nil?\n\n        cmd = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" symbolic-ref --short -q HEAD\"\n        Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|\n          if status.success?\n            @current_branch = stdout.chomp\n            return @current_branch\n          elsif strict_mode\n            Foundation.help!(\"仓库#{File.basename(@path)}当前分支查询失败：当前HEAD不指向任何分支！\")\n          else\n            return nil\n          end\n        }\n      end\n\n      # 查询当前HEAD指向的commit\n      #\n      # @param strict_mode [Boolean] default: true，是否是严格模式。在严格模式下，失败即终止。在非严格模式下，失败返回nil。\n      #\n      # @return [String] commit id\n      #\n      def current_head(strict_mode:true)\n        cmd = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" rev-parse --short HEAD\"\n        Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|\n          if status.success?\n            return stdout.chomp\n          elsif strict_mode\n            Foundation.help!(\"仓库#{File.basename(@path)}HEAD指向查询失败：#{stderr}\")\n          else\n            return nil\n          end\n        }\n      end\n\n      # 指定分支本地是否存在\n      def local_branch_exist?(branch)\n        return has_branch?(branch, false)\n      end\n\n      # 指定分支是否存在对应远程分支（origin）\n      def remote_branch_exist?(branch)\n        return has_branch?(branch, true)\n      end\n\n      # commit是否存在\n      def commit_exist?(commit)\n        cmd = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" cat-file -t #{commit}\"\n        Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|\n          return status.success?\n        }\n      end\n\n      # 查询仓库url\n      def default_url\n        cmd = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" config remote.origin.url\"\n        Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|\n          if status.success?\n            return stdout.chomp\n          else\n            return nil\n          end\n        }\n      end\n\n      # 清空所有缓存内容\n      def refresh\n        @status, @message = nil, nil\n        @branch_status, @branch_message = nil, nil\n        @dirty_zone = 0\n      end\n\n      private\n\n      # 拼接.git在工作区的路径\n      def git_dir\n        return File.join(@path, '.git')\n      end\n\n      # 返回工作区路径\n      def work_tree\n        return @path\n      end\n\n      # 分支是否存在\n      #\n      # @param branch [String] 查询分支\n      #\n      # @param is_remote [Boolean] 是否查询远程，若是，则查询origin/<branch>，否则仅查询<branch>\n      #\n      def has_branch?(branch, is_remote)\n        return false if branch.nil?\n\n        # 如果检查分支是当前分支（格式为\"* current_branch\"），后续检查会失效，因此直接返回true\n        return true if branch == current_branch(strict_mode:false) && !is_remote\n\n        padding = \"  \" # 终端输出的分支名前有两个空格\n        cmd = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" branch #{is_remote ? '-r ' : ''}| grep -xi \\\"#{padding}#{is_remote ? 'origin/' : ''}#{branch}\\\"\"\n        Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|\n          return status.success?\n        }\n      end\n\n      # 查询仓库状态\n      def check_repo_status\n        cmd = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" status --porcelain\"\n        Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|\n          if status.success?\n            if stdout.length > 0\n              @status, @message, @dirty_zone = parse_change(stdout.split(\"\\n\"))\n            else\n              @status = GIT_REPO_STATUS[:clean]\n              @message = [[\"仓库状态\", ['无改动']]]\n            end\n          else\n            Foundation.help!(\"仓库#{File.basename(@path)}状态查询失败：#{stderr}\")\n          end\n        }\n      end\n\n      # 查询分支状态\n      def check_branch_status\n        branch = current_branch(strict_mode:false)\n        remote_branch = tracking_branch(branch)\n        is_tracking = !remote_branch.nil?\n\n        # 当前HEAD不指向任何分支\n        if branch.nil?\n          @branch_status = GIT_BRANCH_STATUS[:detached]\n          @branch_message = \"当前HEAD处于游离状态\"\n          # 当前已经追踪远程分支\n        elsif is_tracking\n          cmd1 = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" rev-list #{branch}..#{remote_branch}\"\n          cmd2 = \"git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{work_tree}\\\" rev-list #{remote_branch}..#{branch}\"\n          stdout1, stdout2 = nil, nil\n\n          Utils.execute_shell_cmd(cmd1) { |stdout, stderr, status|\n            if status.success?\n              stdout1 = stdout\n            else\n              Foundation.help!(\"检查仓库clean细节时，执行#{cmd1}命令失败：#{stderr}\")\n            end\n          }.execute_shell_cmd(cmd2) { |stdout, stderr, status|\n            if status.success?\n              stdout2 = stdout\n            else\n              Foundation.help!(\"检查仓库clean细节时，执行#{cmd2}命令失败：#{stderr}\")\n            end\n          }\n\n          if stdout1.length == 0 && stdout2.length == 0\n            @branch_status = GIT_BRANCH_STATUS[:up_to_date]\n            @branch_message = \"当前分支与远程分支[同步]\"\n          elsif stdout1.length == 0 && stdout2.length > 0\n            @branch_status = GIT_BRANCH_STATUS[:ahead]\n            # @branch_message = \"当前分支超前远程分支[#{stdout2.split(\"\\n\").length}]个提交\"\n            @branch_message = \"当前分支[超前]远程分支\"\n          elsif stdout1.length > 0 && stdout2.length == 0\n            @branch_status = GIT_BRANCH_STATUS[:behind]\n            # @branch_message = \"当前分支落后远程分支[#{stdout1.split(\"\\n\").length}]个提交\"\n            @branch_message = \"当前分支[落后]远程分支\"\n          elsif stdout1.length > 0 && stdout2.length > 0\n            @branch_status = GIT_BRANCH_STATUS[:diverged]\n            @branch_message = \"当前分支与远程分支产生[分叉]\"\n          end\n        elsif has_branch?(branch, true)\n          # 有默认远程分支，但尚未追踪\n          @branch_status = GIT_BRANCH_STATUS[:no_tracking]\n          @branch_message = \"未追踪远程分支\\\"origin/#{branch}\\\"\"\n        else\n          # 无默认远程分支\n          @branch_status = GIT_BRANCH_STATUS[:no_remote]\n          @branch_message = \"对应远程分支不存在\"\n        end\n      end\n\n      # 解析状态\n      #\n      # @param list [Array<String>] 状态行数组（通过git status -s输出）\n      #\n      # @return [GIT_REPO_STATUS，String，GIT_REPO_STATUS_DIRTY_ZONE] 状态；描述信息；脏区域\n      #\n      def parse_change(list)\n        index_message, work_tree_message, conflict_message, special_message = [], [], [], []\n        list.each { |line|\n          index_status = line[0]\n          work_tree_status = line[1]\n          combined_status = index_status + work_tree_status\n          changed_file = line[3..-1]\n\n          change_message = convert_file_status(STATUS_TYPE[:conflicts], combined_status)\n          if !change_message.nil?\n            conflict_message.push(change_message + changed_file)\n          end\n\n          change_message = convert_file_status(STATUS_TYPE[:special], combined_status)\n          if !change_message.nil?\n            special_message.push(change_message + changed_file)\n          end\n\n          change_message = convert_file_status(STATUS_TYPE[:normal], index_status)\n          if !change_message.nil?\n            index_message.push(change_message + changed_file)\n          end\n\n          change_message = convert_file_status(STATUS_TYPE[:normal], work_tree_status)\n          if !change_message.nil?\n            work_tree_message.push(change_message + changed_file)\n          end\n        }\n\n        output = []\n        dirty_zone = 0\n        if index_message.length > 0\n          output.push(['暂存区', index_message])\n          dirty_zone |= GIT_REPO_STATUS_DIRTY_ZONE[:index]\n        end\n\n        if work_tree_message.length > 0\n          output.push(['工作区', work_tree_message])\n          dirty_zone |= GIT_REPO_STATUS_DIRTY_ZONE[:work_tree]\n        end\n\n        if conflict_message.length > 0\n          output.push(['冲突[我方|对方]', conflict_message])\n          dirty_zone |= GIT_REPO_STATUS_DIRTY_ZONE[:work_tree]\n        end\n\n        if special_message.length > 0\n          output.push(['特殊', special_message])\n          dirty_zone |= GIT_REPO_STATUS_DIRTY_ZONE[:special]\n        end\n\n        status = GIT_REPO_STATUS[:dirty]\n        return status, output, dirty_zone\n      end\n\n      # 转化文件状态\n      #\n      # @param type [STATUS_TYPE] 状态类型\n      #\n      # @param status [String] 文件状态\n      #\n      # @return [FILE_STATUS_MESSAGE] 文件状态描述\n      #\n      def convert_file_status(type, status)\n        if type == STATUS_TYPE[:normal]\n          return FILE_STATUS_MESSAGE[status.to_s] if FILE_STATUS.values.include?(status)\n        elsif type == STATUS_TYPE[:conflicts]\n          return FILE_STATUS_MESSAGE[status.to_s] if FILE_STATUS_CONFLICT.values.include?(status)\n        elsif type == STATUS_TYPE[:special]\n          return FILE_STATUS_MESSAGE[status.to_s] if FILE_STATUS_SPECIAL.values.include?(status)\n        end\n        return nil\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/repo/sync_helper.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n\n  class Repo\n    class SyncHelper\n\n      class << self\n\n        # 同步新仓库\n        #\n        # @param repo [MGigClass::Repo] Repo对象\n        #\n        # @param light_repo [Manifest::LightRepo] LightRepo对象\n        #\n        # @param link_git [Boolean] 下载新仓库的时候是否托管.git实体并在工作区创建其软链接\n        #\n        # @return [String, Repo] String：执行结果，成功返回nil，错误返回错误信息，Repo：成功返回nil，错误返回新生成的repo对象\n        #\n        def sync_new_repo(light_repo, root, link_git:true)\n          def __sync_local_repo(light_repo, root, link_git:true)\n            output, repo = nil, nil\n            git_entity = File.join(light_repo.git_store_dir(root), '.git')\n            # 先git clone -- local_git_repo，再软链git_entity，再checkout分支\n            clone_url = \"git clone -- #{git_entity} #{light_repo.abs_dest(root)}\"\n            Utils.execute_shell_cmd(clone_url) { |stdout, stderr, status|\n              if status.success?\n                repo = Repo.generate_strictly(root, light_repo)\n                repo_git = File.join(repo.path, '.git')\n                manage_git = MGitConfig.query_with_key(root, :managegit)\n                if manage_git && link_git\n                  FileUtils.rm_rf(repo_git)\n                  Utils.link(git_entity, repo_git)\n                end\n                msg = ''\n                # 如果从本地clone的话，remote url是指向本地的，需要更新\n                error_message = sync_remote_url(repo, light_repo)\n                msg += error_message + \"\\n\" if !error_message.nil?\n                # 本地仓库可能太旧，执行pull更新代码和新分支\n                success, error_message = repo.execute_git_cmd('fetch', '')\n                if !success && !error_message.nil? && error_message.length > 0\n                  msg += \"由于存在本地仓库源，已从本地克隆，但代码更新失败，请自行fetch最新代码。原因：\\n\" + error_message + \"\\n\"\n                end\n\n                # 同步锁定点\n                error_message = sync_lock_point(repo, light_repo)\n                msg += error_message if !error_message.nil?\n\n                output = msg.length > 0 ? msg : nil\n              else\n                output = \"同步仓库\\\"#{light_repo.name}\\\"时clone失败，如果远程仓库不存在，请在配置文件中删除该仓库并重试。原因：\\n#{stderr}\"\n              end\n            }\n            [output, repo]\n          end\n\n          def __sync_remote_repo(light_repo, root, link_git:true)\n            #\n            output, repo = nil, nil\n            clone_url = light_repo.clone_url(root)\n\n            Utils.execute_shell_cmd(clone_url) { |stdout, stderr, status|\n              if status.success?\n                repo = Repo.generate_strictly(root, light_repo)\n                begin\n                  # 查询配置看是否需要托管.git实体, 根据mgit config -s managegit false配置。\n                  # 若是，那么.git实体会放在.mgit/source-git/文件夹下\n                  manage_git = MGitConfig.query_with_key(root, :managegit)\n                  Utils.link_git(repo.path, light_repo.git_store_dir(root)) if manage_git && link_git\n                rescue Error => _\n                end\n                msg = ''\n                # 同步锁定点\n                error_message = sync_lock_point(repo, light_repo)\n                msg += error_message if !error_message.nil?\n\n                output = msg.length > 0 ? msg : nil\n              else\n                output = \"同步仓库\\\"#{light_repo.name}\\\"时clone失败，如果远程仓库不存在，请在配置文件中删除该仓库并重试。原因：\\n#{stderr}\"\n              end\n            }\n\n            [output, repo]\n          end\n\n          git_entity = File.join(light_repo.git_store_dir(root), '.git')\n          if File.exist?(git_entity)\n            __sync_local_repo(light_repo, root, link_git: link_git)\n          else\n            __sync_remote_repo(light_repo, root, link_git: link_git)\n          end\n        end\n\n        # 同步已有仓库\n        #\n        # @param repo [Repo] Repo对象\n        #\n        # @param light_repo [Manifest::LightRepo] LightRepo对象\n        #\n        # @return [string] 执行结果，成功返回nil，错误返回错误信息\n        #\n        def sync_exist_repo(repo, light_repo)\n          msg = ''\n\n          if light_repo.lock\n            # 同步锁定点\n            error_message = sync_lock_point(repo, light_repo)\n            msg += error_message + \"\\n\" if !error_message.nil?\n          end\n\n          # 同步remote url\n          error_message = sync_remote_url(repo, light_repo)\n          msg += error_message + \"\\n\" if !error_message.nil?\n\n          return msg.length > 0 ? msg : nil\n        end\n\n        # 同步锁定点\n        #\n        # @param repo [Repo] Repo对象\n        #\n        # @param light_repo [Manifest::LightRepo] LightRepo对象\n        #\n        # @return [string] 执行结果，成功返回nil，错误返回错误信息\n        #\n        def sync_lock_point(repo, light_repo)\n          if repo.status_checker.status == Status::GIT_REPO_STATUS[:dirty]\n            return \"#{light_repo.name}有本地改动，无法锁定，请自行清空修改后重试!\"\n          end\n\n          if !light_repo.commit_id.nil?\n            return sync_commit_id(repo, light_repo)\n          elsif !light_repo.tag.nil?\n            return sync_tag(repo, light_repo)\n          elsif !light_repo.branch.nil?\n            return sync_branch(repo, light_repo)\n          end\n        end\n\n        # 同步tag\n        # @param repo [Repo] Repo对象\n        #\n        # @param light_repo [Manifest::LightRepo] LightRepo对象\n        #\n        # @return [string] 执行结果，成功返回nil，错误返回错误信息\n        #\n        def sync_tag(repo, light_repo)\n          if !light_repo.tag.nil?\n            success, output = repo.execute_git_cmd('checkout', light_repo.tag)\n            return output if !success\n          else\n            return \"\\\"#{repo.path}\\\"的仓库配置未指定tag!\"\n          end\n        end\n\n        # 同步commit id\n        # @param repo [Repo] Repo对象\n        #\n        # @param light_repo [Manifest::LightRepo] LightRepo对象\n        #\n        # @return [string] 执行结果，成功返回nil，错误返回错误信息\n        #\n        def sync_commit_id(repo, light_repo)\n          if !light_repo.commit_id.nil?\n            success, output = repo.execute_git_cmd('checkout', light_repo.commit_id)\n            return output if !success\n          else\n            return \"\\\"#{repo.path}\\\"的仓库配置未指定commit id!\"\n          end\n        end\n\n        # 同步分支\n        #\n        # @param repo [Repo] Repo对象\n        #\n        # @param light_repo [Manifest::LightRepo] LightRepo对象\n        #\n        # @return [string] 执行结果，成功返回nil，错误返回错误信息\n        #\n        def sync_branch(repo, light_repo)\n\n          current_branch = repo.status_checker.current_branch(strict_mode:false)\n          local_branch_exist = repo.status_checker.local_branch_exist?(light_repo.branch)\n          remote_branch_exist = repo.status_checker.remote_branch_exist?(light_repo.branch)\n          is_dirty = repo.status_checker.status == Status::GIT_REPO_STATUS[:dirty]\n\n          # 当前已在目标切换分支则不操作\n          if current_branch == light_repo.branch\n            return nil\n\n            # 本地或远程存在目标分支则切换\n          elsif local_branch_exist || remote_branch_exist || Utils.branch_exist_on_remote?(light_repo.branch, light_repo.url)\n\n            # 本地无目标分支则先拉取\n            if !local_branch_exist && !remote_branch_exist\n              success, error = repo.execute_git_cmd('fetch', '')\n              return error if !success\n            end\n\n            if !is_dirty\n              success, output = repo.execute_git_cmd('checkout', light_repo.branch)\n              return output if !success\n            else\n              return \"本地有改动, 无法切换到分支\\\"#{light_repo.branch}\\\", 请处理后重试!\"\n            end\n\n          else\n            return \"仓库分支\\\"#{light_repo.branch}\\\"不存在，请检查是否拼写错误！\"\n          end\n\n        end\n\n        # 同步remote url\n        #\n        # @param repo [Repo] Repo对象\n        #\n        # @param light_repo [Manifest::LightRepo] LightRepo对象\n        #\n        # @return [Boolean] 执行结果，成功返回nil，错误返回错误信息\n        #\n        def sync_remote_url(repo, light_repo)\n          return nil if light_repo.url.nil?\n\n          success, output = repo.execute_git_cmd('remote', \"set-url origin #{light_repo.url}\")\n          return success ? nil : output\n        end\n\n        # 生成本地裸库路径\n        #\n        # @param repo_name [String] 仓库名\n        #\n        # @return [String] 本地裸库路径\n        #\n        def local_bare_git_url(path)\n          if File.exist?(path)\n            return path\n          else\n            return nil\n          end\n        end\n\n        # 删除失效的仓库目录\n        #\n        # @param repo_abs_path [String] 仓库完整路径\n        #\n        def delete_legacy_repo(repo_abs_path)\n          FileUtils.remove_dir(repo_abs_path, true) if File.exist?(repo_abs_path)\n        end\n\n      end\n\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/repo.rb",
    "content": "#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    attr_reader :name\n\n    # 仓库实体完整路径\n    attr_reader :path\n\n    # 仓库状态检查器\n    attr_reader :status_checker\n\n    # 配置文件中的设置：Manifest::LightRepo\n    attr_reader :config\n\n    def initialize(name, path, config:nil)\n      @name = name\n      @path = path\n      @config = config\n      @status_checker = Status.new(path)\n    end\n\n    # 根据传入的绝对路径生成repo对象，如果传入路径不对应git仓库，则返回nil\n    # return [(Repo, String)] (repo, error_message)\n    def self.generate_softly(root, config)\n      abs_path = config.abs_dest(root)\n      if self.is_git_repo?(abs_path)\n        repo = Repo.new(config.name, config.abs_dest(root), config:config)\n        return repo, nil\n      else\n        return nil, \"路径位置\\\"#{abs_path}\\\"不是git仓库！\"\n      end\n    end\n\n    # 根据传入的绝对路径生成repo对象，如果传入路径不对应git仓库，则抛出异常\n    def self.generate_strictly(root, config)\n      abs_path = config.abs_dest(root)\n      if self.is_git_repo?(abs_path)\n        return Repo.new(config.name, config.abs_dest(root), config:config)\n      elsif File.directory?(abs_path)\n        Foundation.help!(\"路径位置\\\"#{abs_path}\\\"不是git仓库！请先确认并手动删除该文件夹，然后执行\\\"mgit sync -n\\\"重新下载。\")\n      else\n        # （注意，如果不希望被同步仓库的.git实体被mgit管理，请执行\\\"mgit sync -n -o\\\"，该方式将不会把.git实体放置到.mgit/souce-git中，更适合开发中途接入mgit的用户）\n        Foundation.help!(\"路径位置\\\"#{abs_path}\\\"不是git仓库！请执行\\\"mgit sync -n\\\"重新下载。\")\n      end\n    end\n\n    def self.check_git_dest(root, config)\n      abs_path = config.abs_dest(root)\n      if self.is_git_repo?(abs_path)\n        true\n      elsif File.directory?(abs_path)\n        Foundation.help!(\"路径位置\\\"#{abs_path}\\\"不是git仓库！请先确认并手动删除该文件夹，然后执行\\\"mgit sync -n\\\"重新下载。\")\n      else\n        false\n      end\n    end\n\n    # 检查传入路径是不是git仓库\n    #\n    # @param path [String] 仓库路径\n    #\n    def self.is_git_repo?(path)\n      Dir.is_git_repo?(path)\n    end\n\n    # 对仓库执行shell指令的入口\n    #\n    # @param abs_cmd [String] 完整指令\n    #\n    # @return [Boolean,String] 是否成功；输出结果\n    #\n    def execute(abs_cmd)\n      Timer.start(name, use_lock:true)\n      Utils.execute_shell_cmd(abs_cmd) { |stdout, stderr, status|\n        # 标记状态更新\n        @status_checker.refresh\n\n        Timer.stop(name, use_lock:true)\n        if status.success?\n          output = stdout.nil? || stdout.length == 0 ? stderr : stdout\n          return true, output\n        else\n          output = stderr.nil? || stderr.length == 0 ? stdout : stderr\n          return false, output\n        end\n      }\n    end\n\n    # 对仓库执行git指令的入口\n    def execute_git_cmd(cmd, opts)\n      return execute(git_cmd(cmd, opts))\n    end\n\n    # 对git指令进行加工，指定正确的执行目录\n    def git_cmd(cmd, opts)\n      git_dir = File.join(@path, '.git')\n\n      # 组装导出变量\n      export_pair = nil\n      Constants::MGIT_EXPORT_INFO.each { |k,v|\n        if !export_pair.nil?\n          export_pair += \" #{k.to_s}=#{v}\"\n        else\n          export_pair = \"export #{k.to_s}=#{v}\"\n        end\n      }\n      export_pair += \" && \" if !export_pair.nil?\n\n      return \"#{export_pair}git --git-dir=\\\"#{git_dir}\\\" --work-tree=\\\"#{@path}\\\" #{cmd} #{opts}\"\n    end\n\n    # 判断实际url和配置url是否一致\n    #\n    # @return [Boolean] 是否一致\n    #\n    def url_consist?\n      if !self.config.nil?\n        return Utils.url_consist?(self.status_checker.default_url, self.config.url)\n      else\n        return true\n      end\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/m-git/template/local_manifest.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  module Template\n    module_function\n    def default_template\n      return \"{\n  \\\"#{Constants::CONFIG_KEY[:repositories]}\\\": {\n\n  }\n}\n\"\n    end\n\n    def local_config_template(config_repo_name)\n      return \"{\n  \\\"#{Constants::CONFIG_KEY[:mgit_excluded]}\\\": true,\n  \\\"#{Constants::CONFIG_KEY[:repositories]}\\\": {\n    \\\"#{config_repo_name}\\\": {\n      \\\"#{Constants::REPO_CONFIG_KEY[:mgit_excluded]}\\\": false\n    }\n  }\n}\n\"\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/template/manifest_hook.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  module Template\n    MANIFEST_HOOK_TEMPLATE = '\n#coding=utf-8\n\nmodule MGitTemplate\n\n  class ManifestHook\n\n    # hook接口，用于生成manifest.json文件。文件本地地址必须设置为<PROJ_ROOT>/.mgit/source-config/manifest.json\n    #\n    # 若解析失败，可抛出异常：\n    #\n    #     raise MGit::Error.new(\"失败原因...\", type: MGit::MGIT_ERROR_TYPE[:config_generate_error])\n    #\n    # 异常抛出后程序终止\n    def self.run()\n\n    end\n\n  end\n\nend\n  '\n  end\nend\n"
  },
  {
    "path": "lib/m-git/template/post_download_hook.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  module Template\n    POST_DOWNLOAD_HOOK_TEMPLATE = '\n#coding=utf-8\n\nmodule MGitTemplate\n\n  class PostDownloadHook\n\n    # hook接口，单个仓库下载完成后调用\n    #\n    # @param name [String] 下载仓库名\n    #\n    # @param path [String] 下载仓库的本地绝对路径\n    #\n    # @return [Boolean] 是否改动仓库HEAD\n    #\n    def self.run(name, path)\n\n    end\n\n  end\n\nend\n  '\n  end\nend\n"
  },
  {
    "path": "lib/m-git/template/post_hook.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  module Template\n    POST_CUSTOMIZED_HOOK_TEMPLATE = '\n#coding=utf-8\n\nmodule MGitTemplate\n\n  class PostHook\n\n    # hook接口，用于接受本次指令执行后的数据\n    #\n    # @param cmd [String] 本次执行指令\n    #\n    # @param opts [String] 本次执行指令参数\n    #\n    # @param mgit_root [String] mgit根目录\n    #\n    # @param exec_repos [Array<Manifest::LightRepo>] 本次执行指令的LightRepo数组\n    #\n    def self.run(cmd, opts, mgit_root, exec_repos)\n\n    end\n\n  end\n\nend\n  '\n  end\nend\n"
  },
  {
    "path": "lib/m-git/template/pre_exec_hook.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  module Template\n    PRE_CUSTOMIZED_EXEC_HOOK_TEMPLATE = '\n#coding=utf-8\n\nmodule MGitTemplate\n\n  class PreExecHook\n\n    # hook接口，用于接受本次指令执行前的数据\n    #\n    # @param cmd [String] 本次执行指令\n    #\n    # @param opts [String] 本次执行指令参数\n    #\n    # @param mgit_root [String] mgit根目录\n    #\n    # @param exec_repos [Array<Manifest::LightRepo>] 本次执行指令的LightRepo数组\n    #\n    def self.run(cmd, opts, mgit_root, exec_repos)\n\n    end\n\n  end\n\nend\n  '\n  end\nend\n"
  },
  {
    "path": "lib/m-git/template/pre_hook.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  module Template\n    PRE_CUSTOMIZED_HOOK_TEMPLATE = '\n#coding=utf-8\n\nmodule MGitTemplate\n\n  class PreHook\n\n    # hook接口，用于接受本次指令执行前的数据\n    #\n    # @param cmd [String] 本次执行指令\n    #\n    # @param opts [String] 本次执行指令参数\n    #\n    # @param mgit_root [String] mgit根目录\n    #\n    def self.run(cmd, opts, mgit_root)\n\n    end\n\n  end\n\nend\n  '\n  end\nend\n"
  },
  {
    "path": "lib/m-git/template/pre_push_hook.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  module Template\n\n    PRE_CUSTOMIZED_PUSH_HOOK_TEMPLATE = '\n#coding=utf-8\n\nmodule MGitTemplate\n\n  class PrePushHook\n\n    # hook接口，用于接受push指令执行后的数据\n    #\n    # @param cmd [String] 本次执行指令\n    #\n    # @param opts [String] 本次执行指令参数\n    #\n    # @param mgit_root [String] mgit根目录\n    #\n    # @param exec_repos [Array<Manifest::LightRepo>] 本次执行指令的LightRepo数组\n    #\n    def self.run(cmd, opts, mgit_root, exec_repos)\n\n    end\n\n  end\n\nend\n  '\n  end\nend\n"
  },
  {
    "path": "lib/m-git/template.rb",
    "content": "#coding=utf-8\n\nrequire 'm-git/template/local_manifest'\nrequire 'm-git/template/manifest_hook'\nrequire 'm-git/template/post_download_hook'\nrequire 'm-git/template/post_hook'\nrequire 'm-git/template/pre_exec_hook'\nrequire 'm-git/template/pre_hook'\nrequire 'm-git/template/pre_push_hook'"
  },
  {
    "path": "lib/m-git/version.rb",
    "content": "\nmodule MGit\n  VERSION = \"2.5.5\".freeze\nend\n\n\n"
  },
  {
    "path": "lib/m-git/workspace/path_helper.rb",
    "content": "\nmodule MGit\n  class Workspace\n\n    # .mgit 目录下的文件路径\n    #\n    module PathHelper\n\n      # .mgit/config.yml\n      #\n      def config_file\n        File.join(root, Constants::MGIT_CONFIG_PATH)\n      end\n\n      # .mgit/hooks\n      #\n      def hooks_dir\n        File.join(root, Constants::PROJECT_DIR[:hooks])\n      end\n\n      # .mgit/snapshot\n      #\n      def snapshot_dir\n        File.join(root, Constants::PROJECT_DIR[:snapshot])\n      end\n\n      # .mgit/source-config\n      #\n      def source_config_dir\n        File.join(root, Constants::PROJECT_DIR[:source_config])\n      end\n\n      # .mgit/source-git\n      def source_git_dir\n        File.join(root, Constants::PROJECT_DIR[:source_git])\n      end\n      ########################### manifest ##########################\n\n      def manifest_path\n        manifest_name = Constants::CONFIG_FILE_NAME[:manifest]\n        File.join(source_config_dir, manifest_name)\n      end\n\n      def local_manifest_path\n        manifest_name = Constants::CONFIG_FILE_NAME[:local_manifest]\n        File.join(source_config_dir, manifest_name)\n      end\n\n      def cache_manifest_path\n        file_name = Constants::CONFIG_FILE_NAME[:manifest_cache]\n        File.join(source_config_dir, file_name)\n      end\n\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/workspace/workspace_helper.rb",
    "content": "#coding=utf-8\n\nmodule MGit\n  class Workspace\n    module WorkspaceHelper\n      # 弹出托管的.git实体\n      #\n      # @param root [String] mgit工程根目录\n      #\n      # @param light_repo [Manifest::LightRepo] 操作仓库的配置repo\n      #\n      def pop_git_entity(root, light_repo)\n        # 仓库工作区目录\n        repo_dir = light_repo.abs_dest(root)\n\n        # 仓库工作区.git软链接路径\n        workspace_git_link_path = File.join(repo_dir, '.git')\n\n        # 仓库.git实体托管路径\n        git_entity_path = File.join(light_repo.git_store_dir(root), '.git')\n\n        if Dir.exist?(git_entity_path) && Dir.exist?(workspace_git_link_path) && File.symlink?(workspace_git_link_path)\n          # 仓库工作区.git软链接指向路径\n          abs_link_point = File.join(repo_dir, File.readlink(workspace_git_link_path))\n\n          # 若指向路径就是.git实体托管路径，则删除工作区软链接，并把.git弹出\n          if Pathname.new(abs_link_point).realpath.to_s == git_entity_path\n            FileUtils.rm_f(workspace_git_link_path)\n            FileUtils.mv(git_entity_path, repo_dir)\n          end\n        end\n      end\n\n      # 压入（托管）.git实体\n      #\n      # @param root [String] mgit工程根目录\n      #\n      # @param light_repo [Manifest::LightRepo] 操作仓库的配置repo\n      #\n      def push_git_entity(root, light_repo)\n        # 仓库工作区目录\n        repo_dir = light_repo.abs_dest(root)\n        git_store_dir = light_repo.git_store_dir(root)\n\n        # 仓库工作区.git实体路径\n        workspace_git_entity_path = File.join(repo_dir, '.git')\n\n        # 仓库.git实体托管路径\n        cache_git_entity_path = File.join(git_store_dir, '.git')\n\n        # 工作区.git存在，且mgit没有已存在的托管的.git\n        if Dir.exist?(workspace_git_entity_path) && !Dir.exist?(cache_git_entity_path)\n          # 移动并链接\n          Utils.link_git(repo_dir, git_store_dir)\n        end\n      end\n\n      # 将缓存的仓库移动到工作区\n      #\n      # @param root [String] mgit工程根目录\n      #\n      # @param light_repo [Manifest::LightRepo] 操作仓库的配置repo\n      #\n      def pop(root, light_repo)\n        cache_path = File.join(light_repo.cache_store_dir(root), light_repo.name)\n        workspace_path = light_repo.abs_dest(root)\n        workspace_dir = File.dirname(workspace_path)\n        return if invalid_move?(cache_path, workspace_dir)\n\n        if Dir.exist?(cache_path) && !Dir.exist?(workspace_path)\n\n          # 工作区目录不存在则创建\n          FileUtils.mkdir_p(workspace_dir) if !Dir.exist?(workspace_dir)\n\n          begin\n            # 【注意】 FileUtils.mv(a,b) 如果b路径的basename不存在，那么自动创建b并将【a文件夹内的所有文件】拷贝到b(b/<content of a>)，如果basename存在，那么直接把a文件夹整个移动到b下(b/a/<content of a>)。\n            FileUtils.mv(cache_path, workspace_dir)\n          rescue => _\n          end\n        end\n      end\n\n      # 将工作区的仓库缓存起来\n      #\n      # @param root [String] mgit工程根目录\n      #\n      # @param light_repo [Manifest::LightRepo] 操作仓库的配置repo\n      #\n      def push(root, light_repo)\n        cache_dir = light_repo.cache_store_dir(root)\n        workspace_path = light_repo.abs_dest(root)\n        return if invalid_move?(workspace_path, cache_dir)\n\n        cache_path = File.join(cache_dir, light_repo.name)\n        if Dir.exist?(workspace_path)\n          # 缓存存在则删除缓存\n          FileUtils.rm_rf(cache_path) if Dir.exist?(cache_path)\n\n          # 缓存目录不存在则创建\n          FileUtils.mkdir_p(cache_dir) if !Dir.exist?(cache_dir)\n\n          begin\n            FileUtils.mv(workspace_path, cache_dir)\n          rescue => _\n          end\n        end\n      end\n\n      # 将工作区的仓库a替换为b\n      #\n      # @param root [String] mgit工程根目录\n      #\n      # @param light_repo_a [Manifest::LightRepo] 缓存仓库的配置repo\n      #\n      # @param light_repo_b [Manifest::LightRepo] 弹出仓库的配置repo\n      #\n      def replace(root, light_repo_a, light_repo_b)\n        push(root, light_repo_a)\n        pop(root, light_repo_b)\n      end\n\n      # 根据工作区仓库url和配置url来替换仓库\n      #\n      # @param root [String] mgit工程根目录\n      #\n      # @param light_repo [Manifest::LightRepo] 操作仓库的配置repo\n      #\n      def sync_workspace(root, light_repo)\n        name = light_repo.name\n        path = light_repo.abs_dest(root)\n\n        # 若工作区存在该仓库，且url与配置不匹配，则压入缓存，此时如果配置的url有对应缓存，则将其弹出\n        if Repo.is_git_repo?(path)\n          repo = Repo.new(name, path)\n          url = repo.status_checker.default_url\n          if !Utils.url_consist?(url, light_repo.url)\n            repo_config = Manifest::LightRepoGenerator.simple_init(name, path, url)\n            replace(root, repo_config, light_repo)\n          end\n\n          # 若工作区不存在该仓库，则弹出配置url对应缓存（如有缓存的话）\n        else\n          pop(root, light_repo)\n        end\n      end\n\n      # 判断是否可以移动目录，若目标目录包含源目录，则无法移动\n      #\n      # @param from_path [String] 源目录\n      #\n      # @param to_path [String] 目标目录\n      #\n      def invalid_move?(from_path, to_path)\n        return Pathname(to_path).cleanpath.to_s.include?(Pathname(from_path).cleanpath.to_s)\n      end\n\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git/workspace.rb",
    "content": "\nrequire 'm-git/workspace/workspace_helper'\nrequire 'm-git/workspace/path_helper'\n\nmodule MGit\n  class Workspace\n    # @!attribute 仓库过滤器，过滤执行的仓库\n    #\n    RepoFilterConfig = Struct.new(:auto_exec, :include_lock, :select_repos, :exclude_repos)\n\n    class << self\n\n      include WorkspaceHelper\n      include PathHelper\n\n      attr_reader :root\n\n      attr_reader :config\n\n      def filter_config\n        @filter_config ||= RepoFilterConfig.new\n      end\n\n      # 配置mgit根目录\n      def setup_multi_repo_root(initial_root=nil)\n        if initial_root\n          @root = initial_root\n          return\n        end\n        @root = multi_repo_root_path\n        if @root.nil?\n          Foundation.help!('该目录不是多仓库目录!!!')\n        end\n      end\n\n      # 解析配置文件（完成后可使用@config获取配置对象）\n      #\n      # @param strict_mode [Boolean] default: true 严格模式下，出错直接终止，非严格模式下，出错则抛出异常\n      #\n      def setup_config(strict_mode:true)\n        # 记录旧config哈希\n        hash_sha1 = config.hash_sha1 if !config.nil?\n\n        # 调用manifest_hook\n        HooksManager.execute_manifest_hook(strict_mode:strict_mode)\n\n        # 解析config\n        @config = Manifest.parse(source_config_dir)\n\n        # 是否更新了配置表\n        did_update = hash_sha1.nil? || hash_sha1 != config.hash_sha1\n\n        # --- 同步工作区 ---\n        begin\n          # 从配置中读取\n          should_sync_workspace = MGitConfig.query_with_key(root, :syncworkspace)\n          if should_sync_workspace\n            # 同步工作区仓库（缓存或弹出）\n            Utils.sync_workspace(root, config)\n          else\n            # 若禁止同步的话，则将缓存弹出（若有的话）\n            config.light_repos.each { |light_repo|\n              if !Dir.exist?(light_repo.abs_dest(root))\n                pop(root, light_repo)\n              end\n            }\n          end\n        rescue Error => _\n          Output.puts_fail_message(\"MGit配置读取失败，跳过工作区同步！\")\n        end\n\n        # ------------------\n\n        # --- 同步.git实体 ---\n        begin\n          # 从配置中读取\n          manage_git = MGitConfig.query_with_key(root, :managegit)\n          if !manage_git\n            Utils.pop_git_entity(root, config)\n          else\n            # 当前逻辑是如果配置了托管.git的话，此时不压入.git，而是只把新下载仓库的.git压入\n            # 后续如果有需要的话可以打开下面这个注释，这样在每次执行mgit指令时都会根据配置压入.git，起到同步的作用\n            # Workspace.push_git_entity(@root, @config)\n          end\n        rescue Error => _\n          Output.puts_fail_message(\"MGit配置读取失败，跳过.git同步！\")\n        end\n        # ------------------\n        did_update\n      end\n\n      # 更新配置解析结果，并同步缺失仓库\n      def update_config(strict_mode:true)\n        Output.puts_processing_message(\"检查多仓库配置信息...\")\n        if setup_config(strict_mode:strict_mode)\n          Output.puts_success_message(\"配置信息已更新！\\n\")\n        else\n          # 配置表未更新，直接返回\n          Output.puts_success_message(\"配置信息已为最新！\\n\")\n          return\n        end\n\n        origin_all_repo_names = all_repos.map { |e| e.name }\n        @all_repos, @exec_light_repos = nil, nil\n        missing_repos = []\n        missing_light_repos = setup_all_repos(strict_mode:false)\n        if missing_light_repos.length > 0\n\n          Utils.show_clone_info(root, missing_light_repos)\n          mutex = Mutex.new\n          error_repos = {}\n          task_count = 0\n          Output.update_progress(missing_light_repos.length, task_count)\n          concurrent_enumerate(missing_light_repos) { |light_repo|\n            error_message, _ = Repo::SyncHelper.sync_new_repo(light_repo, root)\n            mutex.lock\n            if error_message.nil?\n              missing_repos.push(Repo.generate_strictly(root, light_repo))\n            else\n              error_repos[light_repo.name] = error_message\n            end\n            task_count += 1\n            Output.update_progress(missing_light_repos.length, task_count)\n            mutex.unlock\n          }\n          if error_repos.length > 0\n            show_error(error_repos, action:'下载操作')\n            #（注意，如果不希望被下载仓库的.git实体被mgit管理，请执行\\\"mgit sync -n -o\\\"，该方式将不会把.git实体放置到.mgit/souce-git中，更适合开发中途接入mgit的用户）\n            Foundation.help!(\"请检查原因并执行\\\"mgit sync -n\\\"重新下载。\")\n          else\n            Output.puts_success_message(\"下载成功！\\n\")\n          end\n        end\n\n        # 加入将当前分支上本地已有的新仓库\n        current_branch_exist_new_repos = all_repos.select { |repo| !origin_all_repo_names.include?(repo.name) }\n        missing_repos += current_branch_exist_new_repos\n\n        # 新仓库当前分支可能并不是所需分支，可以再进一步操作\n        yield(missing_repos) if block_given? && missing_repos.length > 0\n      end\n\n      # 配置实体仓库对象（完成后可通过all_repos方法或@all_repos属性获取所有可执行仓库对象）\n      #\n      # @param strict_mode [Boolean] default: true 严格模式下，出错直接终止，非严格模式下，出错则抛出异常\n      #\n      def setup_all_repos(strict_mode: true)\n        repos = []\n        locked_repos = []\n        missing_light_repos = []\n\n        need_sync_repos = []\n        exec_light_repos.each do |light_repo|\n          need_sync_repos << light_repo unless Repo.check_git_dest(root, light_repo)\n        end\n\n        if need_sync_repos.length > 0\n          sync_new_repos(need_sync_repos)\n        end\n\n        exec_light_repos.each { |light_repo|\n\n          if strict_mode\n            repo = Repo.generate_strictly(root, light_repo)\n          else\n            repo, _ = Repo.generate_softly(root, light_repo)\n          end\n\n          if !repo.nil?\n            # 同步被锁仓库，不加入到本次执行中\n            if repo.config.lock\n              locked_repos.push(repo)\n            else\n              repos.push(repo)\n            end\n          else\n            missing_light_repos.push(light_repo)\n          end\n        }\n\n        # 同步锁定仓库\n        sync_locked_repos(locked_repos)\n\n        @locked_repos = locked_repos\n        @all_repos = repos\n        @all_repos += locked_repos if filter_config.include_lock\n\n        missing_light_repos\n      end\n\n      # 获取所有仓库\n      def all_repos(except_config:false)\n        setup_all_repos if @all_repos.nil?\n\n        if except_config\n          @all_repos.select { |e| !e.config.is_config_repo }\n        else\n          @all_repos\n        end\n      end\n\n      def locked_repos\n        setup_all_repos if @locked_repos.nil?\n        @locked_repos\n      end\n\n      # 提供一组light repo，更新repo对象\n      def update_all_repos(update_repos_names)\n        if update_repos_names.is_a?(Array)\n          update_light_repos = config.repo_list(selection:update_repos_names)\n          @exec_light_repos = update_light_repos\n          @all_repos = nil\n          setup_all_repos\n        end\n      end\n\n      # 抽取本次需要执行指令的仓库对应的LightRepo\n      #\n      # @return [Array<LightRepo>] 本次需要执行指令的LightRepo数组\n      #\n      def exec_light_repos\n        if @exec_light_repos.nil?\n          mrepo_opt = filter_config.select_repos\n          exclude_mrepo_opt = filter_config.exclude_repos\n\n          selected_repos = mrepo_opt.value if !mrepo_opt.nil?\n          excluded_repos = exclude_mrepo_opt.value if !exclude_mrepo_opt.nil?\n\n          # 校验参数是否正确\n          check_repo_names = []\n          check_repo_names.concat(selected_repos) if selected_repos\n          check_repo_names.concat(excluded_repos) if excluded_repos\n          unless check_repo_names.empty?\n            light_repo_names = config.light_repos.map(&:name)\n            extra_names = check_repo_names - light_repo_names\n            Foundation.help!(\"指定的仓库名称#{extra_names}不存在，请检查命令指定的参数\") unless extra_names.empty?\n          end\n\n          @exec_light_repos = config.repo_list(selection:selected_repos, exclusion:excluded_repos)\n        end\n        @exec_light_repos\n      end\n\n      # -----------------------------------------------\n      # 校验mgit根目录\n      def multi_repo_root_path\n        dir = Dir.pwd\n        while File.dirname(dir) != dir do\n          Dir.foreach(dir) do |filename|\n            next unless File.directory?(File.join(dir, filename))\n            return dir if filename == '.mgit'\n          end\n          dir = File.dirname(dir)\n        end\n        nil\n      end\n\n      # 生成配置仓库的Repo对象\n      #\n      # @return [Repo] 配置仓库的Repo对象\n      #\n      def generate_config_repo\n        config_light_repo = exec_light_repos.find { |light_repo| light_repo.is_config_repo == true }\n        if !config_light_repo.nil?\n          repo, _ = Repo.generate_softly(root, config_light_repo)\n          return repo\n        else\n          return nil\n        end\n      end\n\n\n      def concurrent_enumerate_with_progress_bar(light_repos, message, &exec_handler)\n        Output.puts_processing_block(light_repos.map { |e| e.name }, message)\n        concurrent_enumerate_with_progress_bar_pure(light_repos, &exec_handler)\n      end\n\n      def concurrent_enumerate_with_progress_bar_pure(light_repos, &exec_handler)\n        task_count = 0\n        Output.update_progress(light_repos.length, task_count)\n        concurrent_enumerate(light_repos) { |light_repo|\n          exec_handler.call(light_repo) if exec_handler\n          Lock.mutex_puts {\n            task_count += 1\n            Output.update_progress(light_repos.length, task_count)\n          }\n        }\n      end\n\n      def sync_new_repos(repos)\n        return if repos.length == 0\n\n        error_repos = {}\n\n        Utils.show_clone_info(root, repos)\n        concurrent_enumerate_with_progress_bar_pure(repos) { |light_repo|\n          error_message, _ = Repo::SyncHelper.sync_new_repo(light_repo, root)\n          if !error_message.nil?\n            Lock.mutex_exec { error_repos[light_repo.name] = error_message }\n          end\n        }\n\n        # 执行下载后hook\n        repos_need_to_guide = []\n        concurrent_enumerate(repos) { |light_repo|\n          if error_repos[light_repo.name].nil? && # 下载未出错\n              !light_repo.lock && # 不是锁定仓库\n              !HooksManager.execute_post_download_hook(light_repo.name, light_repo.abs_dest(root)) # hook没有修改HEAD\n            repos_need_to_guide.push(light_repo)\n          end\n        }\n\n        # 引导分支切换\n        if repos_need_to_guide.length > 0\n          existing_repos = []\n          missing_repos = []\n          config.repo_list.each { |light_repo|\n            repo, _ = Repo.generate_softly(root, light_repo)\n            if !repo.nil?\n              if repos_need_to_guide.include?(light_repo)\n                missing_repos.push(repo)\n              else\n                existing_repos.push(repo)\n              end\n            end\n          }\n\n          repo_combo = missing_repos + existing_repos\n          if has_diff_branch?(repo_combo)\n            # 提示切换新下载仓库\n            guide_to_checkout_branch(missing_repos, existing_repos)\n            # 切换完成后如果所出分支不一致，给出提示\n            Output.puts_remind_message(\"注意，当前所有仓库并不处于统一分支，可通过\\\"mgit branch --compact\\\"查看。\") if has_diff_branch?(repo_combo)\n          end\n        end\n\n        if error_repos.length > 0\n          show_error(error_repos, action:'锁定')\n        end\n      end\n\n      # 同步锁定仓库\n      def sync_locked_repos(repos)\n        return if repos.length == 0\n\n        Output.puts_processing_message(\"正在锁定#{repos.length}个仓库...\")\n        mutex = Mutex.new\n        error_repos = {}\n        concurrent_enumerate(repos) { |repo|\n          error_message = Repo::SyncHelper.sync_exist_repo(repo, repo.config)\n          if !error_message.nil?\n            mutex.lock\n            error_repos[repo.name] = error_message\n            mutex.unlock\n          end\n        }\n        if error_repos.length > 0\n          show_error(error_repos, action:'锁定')\n        end\n      end\n\n      # 校验分支统一性\n      def check_branch_consistency\n        if has_diff_branch?(all_repos)\n          if filter_config.auto_exec || Output.continue_with_user_remind?(\"当前所有仓库并不处于同一分支(可通过\\\"mgit branch --compact\\\"查看)，是否继续？\")\n            return\n          else\n            Output.puts_cancel_message\n            exit\n          end\n        end\n      end\n\n      # 检查是否存在不一致的分支\n      #\n      # @return [Boolean] 是否存在不一致分支\n      #\n      def has_diff_branch?(repos)\n        return false if repos.length == 0\n\n        branch = nil\n        repos.each { |repo|\n          current_branch = repo.status_checker.current_branch(strict_mode:false)\n          # current_branch为空值意味着HEAD游离\n          if current_branch.nil? || (!branch.nil? && branch != current_branch)\n            return true\n          elsif branch.nil?\n            branch = current_branch\n          end\n        }\n        return false\n      end\n\n      # 引导切换新仓库的分支\n      #\n      # @param missing_repos [Array<Repo>] 缺失仓库\n      #\n      # @param exist_repos [Array<Repo>] 已有仓库\n      #\n      # @return [Array<Repo>] 切换成功的仓库\n      #\n      def guide_to_checkout_branch(new_repos, exist_repos, append_message:nil)\n        return [] if new_repos.length == 0 || exist_repos.length == 0\n\n        # 寻找最多仓库所在分支作为推荐\n        branch_count = {}\n        exist_repos.each { |repo|\n          branch = repo.status_checker.current_branch(strict_mode:false, use_cache:true)\n          if !branch.nil?\n            branch_count[branch] = 0 if branch_count[branch].nil?\n            branch_count[branch] += 1\n          end\n        }\n        # 若已有仓库都游离，无法推荐切换，则直接返回\n        return [] if branch_count.length == 0\n        max_branch = branch_count.max_by { |k,v| v }.first\n\n        branch_group = {}\n        new_repos.each { |repo|\n          branch = repo.status_checker.current_branch(strict_mode:false, use_cache:true)\n          if branch.nil?\n            branch = 'HEAD游离'\n          elsif branch == max_branch\n            next\n          end\n          branch_group[branch] = [] if branch_group[branch].nil?\n          branch_group[branch].push(repo.name)\n        }\n\n        # 如果新仓库都在当前推荐分支则不操作\n        if branch_group.length == 0\n          return new_repos\n          # 指定了auto则跳过提示，直接开始同步\n        elsif branch_group.length > 0 && (filter_config.auto_exec || Output.continue_with_combined_interact_repos?(branch_group.to_a, \"检测到已有的仓库大部分(或全部)处于分支：#{max_branch}\\n    是否将以上仓库切换到该分支#{\"(#{append_message})\" if !append_message.nil?}？\", title:'新仓库所在分支'))\n\n          do_repos = []\n          remind_repos = []\n\n          new_repos.each { |repo|\n            if repo.status_checker.local_branch_exist?(max_branch) || repo.status_checker.remote_branch_exist?(max_branch)\n              do_repos.push(repo)\n            else\n              remind_repos.push(repo)\n            end\n          }\n\n          Output.puts_fail_block(remind_repos.map { |e| e.name }, \"以上仓库无对应分支，已跳过，请自行处理。\") if remind_repos.length > 0\n\n          if do_repos.length > 0\n            Output.puts_processing_message(\"开始切换分支...\")\n            _, error_repos = execute_git_cmd_with_repos('', '', do_repos) { |repo|\n              opts = \"#{remind_repos.include?(repo) ? '-b ' : ''}#{max_branch}\"\n              [\"checkout\", opts]\n            }\n\n            if error_repos.length > 0\n              return do_repos.select { |repo| !error_repos.keys.include?(repo.name)}\n            else\n              Output.puts_success_message(\"分支切换成功！\\n\")\n              return do_repos\n            end\n\n          end\n        end\n\n        return []\n      end\n\n      # 并发遍历\n      def concurrent_enumerate(array)\n        begin\n          max_concurrent_count = MGitConfig.query_with_key(root, :maxconcurrentcount)\n        rescue Error => e\n          Foundation.help!(e.msg)\n        end\n\n        array.peach(max_concurrent_count) { |item|\n          yield(item) if block_given?\n        }\n      end\n\n      # 带进度条串行执行\n      def serial_enumerate_with_progress(array)\n        task_count = 0\n        Output.update_progress(array.length, task_count)\n        array.each { |repo|\n          yield(repo) if block_given?\n          task_count += 1\n          Output.update_progress(array.length, task_count)\n        }\n      end\n\n      # git指令透传执行\n      def execute_git_cmd_with_repos(cmd, git_opts, repos)\n        mutex = Mutex.new\n        success_repos = {}\n        error_repos = {}\n        task_count = 0\n        Output.update_progress(repos.length, task_count)\n        concurrent_enumerate(repos) { |repo|\n          # 允许针对仓库对指令进行加工\n          cmd, git_opts = yield(repo) if block_given?\n          success, output = repo.execute_git_cmd(cmd, git_opts)\n\n          mutex.lock\n          if success\n            success_repos[repo.name] = output\n          else\n            error_repos[repo.name] = output\n          end\n          task_count += 1\n          Output.update_progress(repos.length, task_count)\n          mutex.unlock\n        }\n        show_error(error_repos)\n        return success_repos, error_repos\n      end\n\n      # shell指令透传执行(串行)\n      def execute_common_cmd_with_repos(abs_cmd, repos)\n        success_repos = {}\n        error_repos = {}\n        task_count = 0\n        Output.update_progress(repos.length, task_count)\n        repos.each { |repo|\n          # 允许针对仓库对指令进行加工\n          abs_cmd = yield(repo) if block_given?\n          new_abs_cmd = \"cd \\\"#{repo.path}\\\" && #{abs_cmd}\"\n          success, output = repo.execute(new_abs_cmd)\n          if success\n            success_repos[repo.name] = output\n          else\n            error_repos[repo.name] = output\n          end\n          task_count += 1\n          Output.update_progress(repos.length, task_count)\n        }\n        show_error(error_repos)\n        return success_repos, error_repos\n      end\n\n      # shell指令透传执行(并发)\n      def execute_common_cmd_with_repos_concurrent(abs_cmd, repos)\n        mutex = Mutex.new\n        success_repos = {}\n        error_repos = {}\n        task_count = 0\n        Output.update_progress(repos.length, task_count)\n        concurrent_enumerate(repos) { |repo|\n          # 允许针对仓库对指令进行加工\n          abs_cmd = yield(repo) if block_given?\n          new_abs_cmd = \"cd \\\"#{repo.path}\\\" && #{abs_cmd}\"\n          success, output = repo.execute(new_abs_cmd)\n\n          mutex.lock\n          if success\n            success_repos[repo.name] = output\n          else\n            error_repos[repo.name] = output\n          end\n          task_count += 1\n          Output.update_progress(repos.length, task_count)\n          mutex.unlock\n        }\n        show_error(error_repos)\n        return success_repos, error_repos\n      end\n\n      # 显示错误信息\n      def show_error(error_repos, action:nil)\n        if error_repos.keys.length > 0\n          # 压缩错误信息\n          error_detail = {}\n          error_repos.each { |repo_name, error|\n            error = '指令执行失败，但无任何输出，请自行检查。' if error == ''\n            error_detail[error] = [] if error_detail[error].nil?\n            error_detail[error].push(repo_name)\n          }\n\n          # 显示错误\n          error_detail.each { |error, repos|\n            Output.puts_fail_block(repos, \"以上仓库执行#{action}失败，原因：\\n#{error}\")\n          }\n        end\n      end\n\n      # 获取当前分支远程仓库信息\n      def pre_fetch\n        Output.puts_processing_message(\"获取远程仓库信息...\")\n        mutex = Mutex.new\n        error_repos = {}\n        task_count = 0\n        Output.update_progress(all_repos.length, task_count)\n        concurrent_enumerate(all_repos) { |repo|\n          Timer.start(repo.name, use_lock:true)\n          git_cmd = repo.git_cmd('fetch', '')\n          Utils.execute_shell_cmd(git_cmd) { |stdout, stderr, status|\n            error_msg = GitMessageParser.new(repo.config.url).parse_fetch_msg(stderr)\n\n            mutex.lock\n            if !status.success? || !error_msg.nil?\n              error_repos[repo.name] = error_msg.nil? ? stderr : error_msg\n            end\n            task_count += 1\n            Output.update_progress(all_repos.length, task_count)\n            mutex.unlock\n            Timer.stop(repo.name, use_lock:true)\n\n            # 标记状态更新\n            repo.status_checker.refresh\n          }\n        }\n        if error_repos.length > 0\n          show_error(error_repos, action:\"远程查询\")\n        else\n          Output.puts_success_message(\"获取成功！\\n\") if error_repos.length == 0\n        end\n      end\n\n      # 检查是否是全部定义的子仓库\n      #\n      # @param subrepos [LightRepo] 仓库轻量对象集合\n      #\n      # @return [Boolean] 是否是所有子仓库\n      #\n      def is_all_exec_sub_repos?(subrepos)\n        if subrepos.is_a?(Array)\n          subrepo_names = subrepos.map { |e| e.name }\n          return is_all_exec_sub_repos_by_name?(subrepo_names)\n        end\n      end\n\n      # 检查是否是全部定义的子仓库\n      #\n      # @param subrepos [String] 仓库名字数组\n      #\n      # @return [Boolean] 是否是所有子仓库\n      #\n      def is_all_exec_sub_repos_by_name?(subrepo_names)\n        if subrepo_names.is_a?(Array)\n          all_subrepo_name = config.repo_list.select { |e| !e.lock && !e.is_config_repo }.map { |e| e.name }\n          return subrepo_names == all_subrepo_name\n        end\n      end\n\n    end\n  end\nend\n"
  },
  {
    "path": "lib/m-git.rb",
    "content": "\n$:.unshift __dir__\n\nrequire 'open3'\nrequire 'fileutils'\nrequire 'json'\nrequire 'digest'\nrequire 'pathname'\nrequire 'uri'\nrequire 'yaml'\nrequire 'securerandom'\n\nrequire 'colored2'\nrequire 'peach'\nrequire 'tty-pager'\n\nrequire 'm-git/foundation'\nrequire 'm-git/output/output'\nmodule MGit\n  include MGit::Foundation\nend\n\nrequire 'm-git/argv'\nrequire 'm-git/hooks_manager'\nrequire 'm-git/command_manager'\nrequire 'm-git/base_command'\nrequire 'm-git/error'\nrequire 'm-git/repo'\nrequire 'm-git/manifest'\nrequire 'm-git/template'\nrequire 'm-git/version'\nrequire 'm-git/workspace'\n\n# 对外其他ruby脚本调用接口\nrequire 'm-git/open_api'\n\n# load plugin\nrequire 'm-git/plugin_manager'\n\n\nmodule MGit\n  # 加载插件\n  PluginManager.setup\n\n  module_function\n  # input\n  def run(raw_argv)\n    # 处理不带子命令或带全局参数的输入，如果带全局参数，目前版本对后续附加的子命令不处理。\n    raw_argv.unshift('self') if (raw_argv.first.nil? || (raw_argv.first =~ /-/) == 0)\n    need_verbose = raw_argv.delete('--verbose') || $__VERBOSE__ || false\n    argv = ARGV::Parser.parse(raw_argv)\n\n    begin\n      # 特殊处理'base'\n      cmd_class = CommandManager[argv.cmd]\n      Foundation.help!(\"调用非法指令\\\"#{argv.cmd}\\\"\") if cmd_class.nil?\n      cmd_class.new(argv).run\n    rescue => e\n      Output.puts_fail_message(\"执行该指令时发生异常：#{argv.cmd}\")\n      Output.puts_fail_message(\"异常信息：#{e.message}\")\n      Output.puts_fail_message(\"异常位置：#{e.backtrace.join(\"\\n\")}\") if need_verbose\n      exit\n    end\n  end\nend\n\n"
  },
  {
    "path": "m-git.gemspec",
    "content": "\nlib = File.expand_path(\"../lib\", __FILE__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequire \"m-git/version\"\n\nGem::Specification.new do |spec|\n  spec.name          = \"m-git\"\n  spec.version       = MGit::VERSION\n  spec.authors       = [\"zhangyu81\"]\n  spec.summary       = %q{A multi-repository management tool integrated with git.}\n  spec.description   = %q{A multi-repository management tool integrated with git. for detail see home page}\n  spec.homepage      = \"https://github.com/baidu/m-git\"\n  spec.license       = \"MIT\"\n\n  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'\n  # to allow pushing to a single host or delete this section to allow pushing to any host.\n  if spec.respond_to?(:metadata)\n    spec.metadata['allowed_push_host'] = \"https://rubygems.org\"\n\n    spec.metadata[\"homepage_uri\"] = spec.homepage\n    spec.metadata[\"source_code_uri\"] = \"https://github.com/baidu/m-git/tree/master\"\n    spec.metadata[\"changelog_uri\"] = \"https://github.com/baidu/m-git/tree/master/CHANGELOG.md\"\n  else\n    raise \"RubyGems 2.0 or newer is required to protect against \" \\\n      \"public gem pushes.\"\n  end\n\n  # Specify which files should be added to the gem when it is released.\n  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.\n  # spec.files         = Dir.chdir(File.expand_path('..', __FILE__)) do\n  #   `git ls-files -z`.split(\"\\x0\").reject { |f| f.match(%r{^(test|spec|features)/}) }\n  # end\n  spec.files         = %w(README.md LICENSE) + Dir['lib/**/*.rb']\n  spec.bindir        = \"./\"\n  spec.executables   = %w(mgit m-git)\n  spec.require_paths = [\"lib\"]\n\n  spec.add_runtime_dependency 'colored2', '~> 3.1'\n  spec.add_runtime_dependency 'peach', '~> 0.5'\n  spec.add_runtime_dependency 'tty-pager', '~> 0.12'\n\n  spec.add_development_dependency \"bundler\", \"~> 1.17\"\n  spec.add_development_dependency \"rake\", \">= 12.3.3\"\n  spec.add_development_dependency \"minitest\", '~> 5.14.1'\n  spec.add_development_dependency \"minitest-reporters\", '~> 1.4.2'\n\n  spec.required_ruby_version = '>= 2.3.0'\nend\n"
  },
  {
    "path": "manifest.json",
    "content": "{\n  \"remote\": \"https://gitee.com/baidu\",\n  \"version\":1,\n  \"mgit-excluded\": false,\n  \"dest\": \"Sources\",\n  \"repositories\": {\n    \"m-git\": {\n      \"abs-dest\": \"m-git\",\n      \"remote\": \"https://github.com\",\n      \"remote-path\": \"baidu/m-git.git\",\n      \"config-repo\": true\n    },\n    \"other_a\": {\n      \"remote-path\": \"DDParser\"\n    }\n  }\n}\n\n"
  },
  {
    "path": "mgit",
    "content": "#!/usr/bin/env ruby -W0\n#coding=utf-8\n\n#\n# if $PROGRAM_NAME == __FILE__\n#   ENV['BUNDLE_GEMFILE'] = File.expand_path('../Gemfile', __FILE__)\n#   require 'bundler/setup'\n# end\n\nrequire_relative 'lib/m-git'\n\n\nmodule MGit\n  extend self\n\n  SRC_ROOT = __dir__\nend\n\nMGit.run(ARGV)\n"
  },
  {
    "path": "test/argv/test_opt.rb",
    "content": "\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.new(nil)}.must_raise RuntimeError\n  end\n\n  describe \"Check Empty\" do\n    let(:entity) {\n      MGit::ARGV::Opt.new('key')\n    }\n    it \"is empty\" do\n      _(entity.empty?).must_equal true\n    end\n\n    it \"is not empty\" do\n      entity.value = 'a'\n      _(entity.empty?).must_equal false\n\n      entity.value = true\n      _(entity.empty?).must_equal false\n\n      entity.value = false\n      _(entity.empty?).must_equal true\n\n      entity.value = nil\n      _(entity.empty?).must_equal true\n    end\n\n  end\n\nend"
  },
  {
    "path": "test/argv/test_opt_list.rb",
    "content": "\n\n\nrequire_relative '../test_helper'\n\n\ndescribe MGit::ARGV::OptList do\n\n  let(:list) {\n    arr = []\n    opt = MGit::ARGV::Opt.new('--aa', short_key: '-a', info: \"first\", priority: 1)\n    opt.value = true\n    arr << opt\n\n    opt = MGit::ARGV::Opt.new('--cc', short_key: '-c', info: \"third\", priority: 3)\n    arr << opt\n\n    opt = MGit::ARGV::Opt.new('--bb', short_key: '-b', info: \"second\", priority: 2)\n    opt.value = true\n    arr << opt\n\n\n    MGit::ARGV::OptList.new(arr)\n  }\n\n  it \"#opt(key)\" do\n    _(list.opt('--aa').info).must_equal \"first\"\n\n    _(list.opt('-a').info).must_equal \"first\"\n\n    _(list.opt('--bb').info).must_equal \"second\"\n\n    _(list.opt('-b').info).must_equal \"second\"\n\n    _(list.opt('-c')).must_be_nil\n    _{list.opt('--cc').info}.must_raise NoMethodError\n  end\n\n  it \"#registered_opt(key)\" do\n    _(list.registered_opt('--cc').info).must_equal \"third\"\n    _(list.registered_opt('--d')).must_be_nil\n  end\n\n  it \"#did_set_opt?(key)\" do\n    _(list.did_set_opt?('-b')).must_equal true\n    _(list.did_set_opt?('-c')).must_equal false # no value\n    _(list.did_set_opt?('--d')).must_equal false\n  end\n\n  it \"#opts_ordered_by_priority\" do\n    opts = list.opts_ordered_by_priority\n    _(opts.first.info).must_equal \"third\"\n    _(opts.last.info).must_equal \"first\"\n  end\nend"
  },
  {
    "path": "test/argv/test_parser.rb",
    "content": "\n\nrequire_relative '../test_helper'\n\n\ndescribe MGit::ARGV::Parser do\n\n  let(:input_argv) {\n    cmd = \"checkout -b branch_name --mrepo boxapp BBAAccount --command test\".split(\" \")\n    MGit::ARGV::Parser.parse(cmd)\n  }\n\n\n  # # 指令名，如：\"mgit checkout -b branch_name\"的\"checkout\"\n  # attr_reader :cmd\n  #\n  # # 所有参数，如：\"mgit checkout -b branch_name\"的\"checkout -b branch_name\"\n  # attr_reader :pure_opts\n  #\n  # # 完整指令，如：\"mgit checkout -b branch_name\"\n  # attr_reader :absolute_cmd\n  #\n  # # 本次传入的mgit指令中自定义的部分，如：\"mgit checkout -b branch_name --mrepo boxapp BBAAccount --command test\"的\"[[--mrepo boxapp BBAAccount],[--command test]]\"\n  # attr_reader :raw_opts\n  #\n  # # 本次传入的mgit指令中git透传的部分，如：\"mgit checkout -b branch_name --mrepo boxapp BBAAccount\"的\"[[-b branch_name]]\"\n  # # has define method git_opts\n  #\n  # # 所有已注册的参数列表\n  # attr_reader :opt_list\n  #\n  describe \"#parse(argv)\" do\n    it \"#cmd\" do\n      _(input_argv.cmd).must_equal \"checkout\"\n    end\n\n    it \"#pure_opts\" do\n      _(input_argv.pure_opts).must_equal \"-b branch_name --mrepo boxapp BBAAccount --command test\"\n    end\n\n    it \"#absolute_cmd\" do\n      _(input_argv.absolute_cmd).must_equal \"checkout -b branch_name --mrepo boxapp BBAAccount --command test\"\n    end\n\n    it \"#raw_opts\" do\n      _(input_argv.raw_opts.first.join(\" \")).must_equal \"-b branch_name\"\n      _(input_argv.raw_opts.first.count).must_equal 2\n\n      _(input_argv.raw_opts[1].join(\" \")).must_equal \"--mrepo boxapp BBAAccount\"\n      _(input_argv.raw_opts[1].count).must_equal 3\n\n      _(input_argv.raw_opts.last.join(\" \")).must_equal \"--command test\"\n      _(input_argv.raw_opts.last.count).must_equal 2\n    end\n\n  end\n\nend"
  },
  {
    "path": "test/foundation/test_dir.rb",
    "content": "\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 \"#is_git_repo?(path)\" do\n    mock_git_path = '/stub/.git'\n\n    stub_block = lambda do |path|\n      return true if path == '/stub'\n      mock_git_path == path\n    end\n    File.stub :directory?, stub_block do\n      _(Dir.is_git_repo?('/stub')).must_equal true\n      _(Dir.is_git_repo?('/other')).must_equal false\n    end\n  end\n\n  it \"#is_in_git_repo?(path)\" do\n    mock_git_path = '/stub/.git'\n\n    stub_block = lambda do |path|\n      return true if path == '/stub' || path == '/stub/sub_dir'\n      mock_git_path == path\n    end\n    File.stub :directory?, stub_block do\n      _(Dir.is_in_git_repo?('/stub/sub_dir')).must_equal true\n      _(Dir.is_in_git_repo?('/other/sub_dir')).must_equal false\n    end\n  end\n\nend"
  },
  {
    "path": "test/foundation/test_mgit_config.rb",
    "content": "\n\nrequire_relative '../test_helper'\n\n\ndescribe MGit::MGitConfig do\n\n  STUB_CONFIG = {\n      'managegit' => true,\n      'maxconcurrentcount' => 4,\n      'savecache' => true\n  }\n\n  def __stub_config_env\n    MGit::MGitConfig.stub :__load_file, [STUB_CONFIG, nil] do\n      yield if block_given?\n    end\n  end\n\n  def __stub_write_config\n    MGit::MGitConfig.stub :write_to_file, nil do\n      yield if block_given?\n    end\n  end\n\n  it \"#query(root)\" do\n    __stub_config_env do\n      MGit::MGitConfig.query('') do |cfg_hash|\n        _(cfg_hash['managegit']).must_equal true\n        _(cfg_hash['maxconcurrentcount']).must_equal 4\n        _(cfg_hash['none_key']).must_be_nil\n      end\n    end\n  end\n\n  it \"#query_with_key(root, key_symbol)\" do\n    __stub_config_env do\n      query_value = MGit::MGitConfig.query_with_key('', 'managegit')\n      _(query_value).must_equal true\n\n      query_value = MGit::MGitConfig.query_with_key('', 'no-managegit')\n      _(query_value).must_be_nil\n    end\n  end\n\n  it \"#update(root)\" do\n    __stub_config_env do\n      __stub_write_config do\n        MGit::MGitConfig.update('') do |cfg_hash|\n          cfg_hash['managegit'] = false\n        end\n        query_value = MGit::MGitConfig.query_with_key('', 'managegit')\n        _(query_value).must_equal false\n\n        MGit::MGitConfig.update('') do |cfg_hash|\n          cfg_hash['managegit'] = true\n        end\n\n        query_value = MGit::MGitConfig.query_with_key('', 'managegit')\n        _(query_value).must_equal true\n      end\n    end\n  end\n\n  it \"#dump_config(root)\" do\n\n  end\n\n  it \"#to_suitable_value_for_key(root, key, value)\" do\n    __stub_config_env do\n      to_value = MGit::MGitConfig.to_suitable_value_for_key('', 'managegit', 1)\n      _(to_value).must_be_nil\n\n      to_value = MGit::MGitConfig.to_suitable_value_for_key('', 'managegit', \"false\")\n      _(to_value).must_equal false\n    end\n  end\n\n  it \"#write_to_file(root, content)\" do\n\n  end\nend\n"
  },
  {
    "path": "test/foundation/test_utils.rb",
    "content": "require_relative '../test_helper'\n\ndescribe MGit::Utils do\n\n  it \"#change_dir(dir)\" do\n    check_dir = File.expand_path('~')\n    cur = Dir.pwd\n    MGit::Utils.change_dir(check_dir)\n    _(Dir.pwd).must_equal check_dir\n\n    MGit::Utils.change_dir(cur)\n    _(Dir.pwd).must_equal cur\n\n    MGit::Utils.change_dir(cur)\n    _(Dir.pwd).must_equal cur\n  end\n\n  it \"#execute_under_dir(dir)\" do\n    check_dir = File.expand_path('~')\n    cur = Dir.pwd\n    MGit::Utils.execute_under_dir(check_dir) do\n      _(Dir.pwd).must_equal check_dir\n    end\n    _(Dir.pwd).must_equal cur\n\n    MGit::Utils.execute_under_dir(cur) do\n      _(Dir.pwd).must_equal cur\n    end\n    _(Dir.pwd).must_equal cur\n  end\n\n  it \"#relative_dir(dir_a, dir_b)\" do\n    check_a = '/test/a'\n    check_a_b = '/test/a/b'\n    check_b = '/test/b'\n\n    _(MGit::Utils.relative_dir(check_a, check_b, realpath: false)).must_equal \"../a\"\n    _(MGit::Utils.relative_dir(check_b, check_a, realpath: false)).must_equal \"../b\"\n\n    _(MGit::Utils.relative_dir(check_a, check_a, realpath: false)).must_equal \".\"\n\n    _(MGit::Utils.relative_dir(check_a, check_a_b, realpath: false)).must_equal \"..\"\n    _(MGit::Utils.relative_dir(check_b, check_a_b, realpath: false)).must_equal \"../../b\"\n  end\n\n  it \"#expand_path(path, base:nil)\" do\n    # check_a = '/test/a'\n    check_a_b = '/test/a/b'\n    check_b = '/test/b'\n\n    relative = MGit::Utils.relative_dir(check_b, check_a_b, realpath: false)\n    _(MGit::Utils.expand_path(relative, base: check_a_b)).must_equal check_b\n  end\n\n  it \"#generate_init_cache_path(root)\" do\n\n  end\n\n  it \"#link(target_path, link_path)\" do\n\n  end\n\n  it \"#show_clone_info(root, missing_light_repos)\" do\n\n  end\n\n  it \"#branch_exist_on_remote?(branch, git_url)\" do\n\n  end\n\n  it \"#url_consist?(url_a, url_b)\" do\n    url_a = 'https://a.b.cc?s=1'\n    url_a_port = 'https://a.b.cc:446'\n    url_b = 'http://a.b.cc?s=1'\n    url_b_port = 'http://a.b.cc:80'\n    url_ab = 'https://a.b.cc?s=1'\n    _(MGit::Utils.url_consist?(url_a, url_a_port)).must_equal false\n    _(MGit::Utils.url_consist?(url_b, url_ab)).must_equal false\n    _(MGit::Utils.url_consist?(url_b, url_b_port)).must_equal true\n  end\n\n  it \"#normalize_url(url)\" do\n    url_a = 'https://a.b.cc?s=1'\n    url_b = 'http://a.b.cc?s=1'\n\n    _(MGit::Utils.normalize_url(url_a)).must_equal 'https://a.b.cc:443'\n    _(MGit::Utils.normalize_url(url_b)).must_equal 'http://a.b.cc:80'\n  end\nend"
  },
  {
    "path": "test/manifest/test_light_repo.rb",
    "content": "\n\nrequire_relative '../test_helper'\n\ndescribe MGit::Manifest::LightRepo do\n\n\nend"
  },
  {
    "path": "test/test_argv.rb",
    "content": "require_relative 'test_helper'\n\ndescribe MGit::ARGV do\n\n  let(:input_argv) {\n    cmd = \"checkout -b branch_name --mrepo boxapp BBAAccount --command test\".split(\" \")\n    argv = MGit::ARGV::Parser.parse(cmd)\n\n    opts = []\n    opts << MGit::ARGV::Opt.new('--mrepo', info: 'first one')\n    opts << MGit::ARGV::Opt.new('--command')\n    opts << MGit::ARGV::Opt.new('--other_command', info: 'last one')\n    argv.register_opts(opts)\n    argv.resolve!\n    argv\n  }\n\n  it \"#update_opt(key, value)\" do\n\n    opt = input_argv.opt('--command')\n    _(opt.value).must_equal ['test']\n\n    input_argv.update_opt('--command', 'test1')\n    _(opt.value).must_equal 'test1'\n\n    input_argv.update_opt('--command', ['test'])\n  end\n\n  it \"#opt(key)\" do\n    opt = input_argv.opt('--command')\n    _(opt.value).must_equal ['test']\n\n    opt = input_argv.opt('--mrepo')\n    _(opt.value).must_equal ['boxapp', 'BBAAccount']\n  end\n\n  it \"#info(key)\" do\n    _(input_argv.info('--mrepo')).must_equal \"first one\"\n    _(input_argv.info('--other_command')).must_equal \"last one\"\n  end\n\n  it \"#git_opts(raw: true)\" do\n    _(input_argv.git_opts).must_equal \"-b \\\"branch_name\\\"\"\n    _(input_argv.git_opts(raw: false).first).must_equal ['-b', '\"branch_name\"']\n  end\n\n  it \"#is_option?(opt_str)\" do\n    _(input_argv.is_option?('--s')).must_equal true\n    _(input_argv.is_option?('--ss')).must_equal true\n    _(input_argv.is_option?('-s')).must_equal true\n    _(input_argv.is_option?('_s')).must_equal false\n    _(input_argv.is_option?('-ss')).must_equal true\n  end\n\n  describe \"stub_test\" do\n\n    it \"optlist_stub\" do\n      # stub_obj = MGit::ARGV::Opt.new('--command')\n\n      stub_block = Proc.new do |key|\n        MGit::ARGV::Opt.new(key, info: \"#{key}_info\")\n      end\n      input_argv.opt_list.stub :opt, stub_block do |mm|\n        _(mm.opt('--s').info).must_equal \"--s_info\"\n        _(input_argv.opt('-super').info).must_equal \"-super_info\"\n      end\n      _(input_argv.opt('--s')).must_be_nil\n    end\n\n    it \"optlist_stub_object\" do\n      stub_obj = MGit::ARGV::Opt.new('--command')\n      input_argv.opt_list.stub :opt, stub_obj do\n        _(input_argv.opt('--s').key).must_equal \"--command\"\n        _(input_argv.opt('-super').key).must_equal \"--command\"\n      end\n    end\n  end\n\n  describe \"mock_test\" do\n    it \"optlist_mock\" do\n      MGit::ARGV.define_method(:set_opt_list) do |list|\n        @opt_list = list\n      end\n      assert_nil input_argv.opt('--none')\n\n      opt_list = Minitest::Mock.new\n      mock_obj = MGit::ARGV::Opt.new('--default')\n      opt_list.expect :opt, mock_obj,  ['--none']\n      input_argv.set_opt_list opt_list\n      _(input_argv.opt('--none').key).must_equal '--default'\n    ensure\n      MGit::ARGV.undef_method(:set_opt_list)\n    end\n  end\n\nend"
  },
  {
    "path": "test/test_foundation.rb",
    "content": "\nrequire_relative 'test_helper'\n\ndescribe MGit::Foundation do\n\nend\n"
  },
  {
    "path": "test/test_helper.rb",
    "content": "\nrequire 'minitest/autorun'\nrequire 'minitest/reporters'\nMiniTest::Reporters.use! Minitest::Reporters::SpecReporter.new\n#\n\n# syntax: http://www.mattsears.com/articles/2011/12/10/minitest-quick-reference/\n#\n\nDir.glob(File.join(File.dirname(__FILE__), %w(.. lib *.rb ))).each do |file|\n  require file\nend\n\nmodule MGitTest\n  module FileProvider\n    TEST_ROOT = File.dirname(__FILE__)\n\n    EXAMPLE_DIR = File.join(TEST_ROOT, 'example')\n\n    EXAMPLE_GIT_DIR = File.join(EXAMPLE_DIR, 'git_repo')\n\n    EXAMPLE_NOT_GIT_DIR = File.join(EXAMPLE_DIR, 'not_git_repo')\n\n    FILE_IN_GIT = File.join(EXAMPLE_GIT_DIR, '.git/a')\n\n    EXAMPLE_IN_GIT_FILE = File.join(EXAMPLE_DIR, 'deep_dir/a')\n  end\nend\n"
  },
  {
    "path": "test/test_manifest.rb",
    "content": "\n"
  },
  {
    "path": "test/test_open_api.rb",
    "content": "\n\n\nrequire_relative './test_helper'\n\n\ndescribe MGit::OpenApi do\n\nend"
  },
  {
    "path": "test/test_plugin_manager.rb",
    "content": "\n\n\nrequire_relative './test_helper'\n\n\ndescribe MGit::PluginManager do\n\n\n\nend"
  },
  {
    "path": "test/test_workspace.rb",
    "content": "\n\n\nrequire_relative './test_helper'\n\n\ndescribe MGit::Workspace do\n\nend"
  }
]