m-git 2.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +85 -0
  4. data/lib/m-git.rb +66 -0
  5. data/lib/m-git/argv.rb +170 -0
  6. data/lib/m-git/argv/opt.rb +38 -0
  7. data/lib/m-git/argv/opt_list.rb +71 -0
  8. data/lib/m-git/argv/parser.rb +66 -0
  9. data/lib/m-git/base_command.rb +271 -0
  10. data/lib/m-git/command/add.rb +41 -0
  11. data/lib/m-git/command/branch.rb +90 -0
  12. data/lib/m-git/command/checkout.rb +106 -0
  13. data/lib/m-git/command/clean.rb +64 -0
  14. data/lib/m-git/command/commit.rb +84 -0
  15. data/lib/m-git/command/config.rb +202 -0
  16. data/lib/m-git/command/delete.rb +99 -0
  17. data/lib/m-git/command/fetch.rb +32 -0
  18. data/lib/m-git/command/forall.rb +81 -0
  19. data/lib/m-git/command/info.rb +74 -0
  20. data/lib/m-git/command/init.rb +324 -0
  21. data/lib/m-git/command/log.rb +73 -0
  22. data/lib/m-git/command/merge.rb +381 -0
  23. data/lib/m-git/command/pull.rb +364 -0
  24. data/lib/m-git/command/push.rb +311 -0
  25. data/lib/m-git/command/rebase.rb +348 -0
  26. data/lib/m-git/command/reset.rb +31 -0
  27. data/lib/m-git/command/self.rb +223 -0
  28. data/lib/m-git/command/stash.rb +189 -0
  29. data/lib/m-git/command/status.rb +135 -0
  30. data/lib/m-git/command/sync.rb +327 -0
  31. data/lib/m-git/command/tag.rb +67 -0
  32. data/lib/m-git/command_manager.rb +24 -0
  33. data/lib/m-git/error.rb +20 -0
  34. data/lib/m-git/foundation.rb +25 -0
  35. data/lib/m-git/foundation/constants.rb +107 -0
  36. data/lib/m-git/foundation/dir.rb +25 -0
  37. data/lib/m-git/foundation/duration_recorder.rb +92 -0
  38. data/lib/m-git/foundation/git_message_parser.rb +50 -0
  39. data/lib/m-git/foundation/lock.rb +32 -0
  40. data/lib/m-git/foundation/loger.rb +129 -0
  41. data/lib/m-git/foundation/mgit_config.rb +222 -0
  42. data/lib/m-git/foundation/operation_progress_manager.rb +139 -0
  43. data/lib/m-git/foundation/timer.rb +74 -0
  44. data/lib/m-git/foundation/utils.rb +361 -0
  45. data/lib/m-git/hooks_manager.rb +96 -0
  46. data/lib/m-git/manifest.rb +181 -0
  47. data/lib/m-git/manifest/cache_manager.rb +44 -0
  48. data/lib/m-git/manifest/internal.rb +182 -0
  49. data/lib/m-git/manifest/light_repo.rb +108 -0
  50. data/lib/m-git/manifest/light_repo_generator.rb +87 -0
  51. data/lib/m-git/manifest/linter.rb +153 -0
  52. data/lib/m-git/open_api.rb +427 -0
  53. data/lib/m-git/open_api/script_download_info.rb +37 -0
  54. data/lib/m-git/output/output.rb +461 -0
  55. data/lib/m-git/plugin_manager.rb +112 -0
  56. data/lib/m-git/repo.rb +133 -0
  57. data/lib/m-git/repo/status.rb +481 -0
  58. data/lib/m-git/repo/sync_helper.rb +254 -0
  59. data/lib/m-git/template.rb +9 -0
  60. data/lib/m-git/template/local_manifest.rb +27 -0
  61. data/lib/m-git/template/manifest_hook.rb +28 -0
  62. data/lib/m-git/template/post_download_hook.rb +29 -0
  63. data/lib/m-git/template/post_hook.rb +31 -0
  64. data/lib/m-git/template/pre_exec_hook.rb +31 -0
  65. data/lib/m-git/template/pre_hook.rb +29 -0
  66. data/lib/m-git/template/pre_push_hook.rb +32 -0
  67. data/lib/m-git/version.rb +6 -0
  68. data/lib/m-git/workspace.rb +648 -0
  69. data/lib/m-git/workspace/path_helper.rb +56 -0
  70. data/lib/m-git/workspace/workspace_helper.rb +159 -0
  71. data/m-git +1 -0
  72. data/mgit +19 -0
  73. metadata +218 -0
@@ -0,0 +1,87 @@
1
+
2
+ module MGit
3
+ class Manifest
4
+ # @!scope lightrepo生成器
5
+ #
6
+ class LightRepoGenerator
7
+
8
+ # 简单初始化,有写字段缺失,仅包含名字,相对路径,url
9
+ #
10
+ # @param name [String] 仓库名
11
+ #
12
+ # @param path [String] 仓库相对路径
13
+ #
14
+ # @param url [String] 仓库url
15
+ #
16
+ # @return [LightRepo] 配置对象
17
+ #
18
+ def self.simple_init(name, path, url)
19
+ LightRepo.new(name, path, nil, nil, nil, url, false, false, false, false)
20
+ end
21
+
22
+ def self.light_repo_with(name, config_json, parent_json)
23
+ light_repo = LightRepo.new(name)
24
+
25
+ light_repo.path = __parse_path(name, config_json, parent_json)
26
+ lock_info = config_json[Constants::REPO_CONFIG_KEY[:lock]]
27
+
28
+ light_repo.lock = lock_info && !lock_info.empty?
29
+ if light_repo.lock
30
+ light_repo.commit_id = lock_info[Constants::REPO_CONFIG_LOCK_KEY[:commit_id]]
31
+ light_repo.tag = lock_info[Constants::REPO_CONFIG_LOCK_KEY[:tag]]
32
+ light_repo.branch = lock_info[Constants::REPO_CONFIG_LOCK_KEY[:branch]]
33
+ end
34
+ light_repo.url = __parse_url(config_json, parent_json)
35
+
36
+ dummy = config_json[Constants::REPO_CONFIG_KEY[:dummy]]
37
+ dummy = !dummy.nil? && dummy == true
38
+ if dummy
39
+ excluded = true
40
+ else
41
+ excluded = config_json[Constants::REPO_CONFIG_KEY[:mgit_excluded]]
42
+ excluded = parent_json[Constants::CONFIG_KEY[:mgit_excluded]] if excluded.nil?
43
+ excluded = !excluded.nil? && excluded == true
44
+ end
45
+ light_repo.mgit_excluded = excluded
46
+ light_repo.dummy = dummy
47
+
48
+ is_config_repo = config_json[Constants::REPO_CONFIG_KEY[:config_repo]]
49
+ light_repo.is_config_repo = is_config_repo.nil? ? false : is_config_repo
50
+ light_repo
51
+ end
52
+
53
+ private
54
+
55
+ class << self
56
+ def __parse_path(repo_name, config_json, parent_json)
57
+ abs_path = config_json[Constants::REPO_CONFIG_KEY[:abs_dest]]
58
+ return abs_path if !abs_path.nil? && !abs_path.empty?
59
+
60
+ local_path = parent_json[Constants::REPO_CONFIG_KEY[:dest]]
61
+ # 替换key值中的‘/’字符,避免和路径混淆
62
+ repo_name = repo_name.gsub(/\//,':')
63
+ if local_path.nil?
64
+ Utils.safe_join(parent_json[Constants::CONFIG_KEY[:dest]], repo_name)
65
+ elsif !local_path.empty?
66
+ Utils.safe_join(local_path, repo_name)
67
+ else
68
+ repo_name
69
+ end
70
+ end
71
+
72
+ def __parse_url(config_json, parent_json)
73
+ source_remote = config_json[Constants::REPO_CONFIG_KEY[:remote]]
74
+ remote_path = config_json[Constants::REPO_CONFIG_KEY[:remote_path]]
75
+ return if remote_path.nil?
76
+ if source_remote.nil?
77
+ global_remote = parent_json[Constants::CONFIG_KEY[:remote]]
78
+ Utils.safe_join(global_remote, remote_path)
79
+ else
80
+ Utils.safe_join(source_remote, remote_path)
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,153 @@
1
+
2
+ module MGit
3
+ class Manifest
4
+ module Linter
5
+
6
+ # 校验配置文件路径
7
+ #
8
+ # @param path [Stirng] 配置文件路径或包含配置文件的目录
9
+ #
10
+ # @return [String] 配置文件合法路径
11
+ #
12
+ def lint_manifest_path(path)
13
+ manifest_name = Constants::CONFIG_FILE_NAME[:manifest]
14
+
15
+ if !File.exists?(path)
16
+ if File.symlink?(path)
17
+ terminate!("配置文件软链接#{path}失效,请执行\"mgit config -m <new_path>/manifest.json\"更新!")
18
+ else
19
+ terminate!("配置文件#{path}不存在!")
20
+ end
21
+ elsif File.basename(path) != manifest_name
22
+ terminate!("请指定名为#{manifest_name}的文件!", type:MGIT_ERROR_TYPE[:config_name_error])
23
+ end
24
+ end
25
+
26
+ # 校验本地配置文件路径
27
+ #
28
+ # @param path [String] 本地配置文件路径
29
+ #
30
+ # @return [String] 合法的本地配置文件路径
31
+ #
32
+ def lint_local_manifest_path(path)
33
+ local_manifest_name = Constants::CONFIG_FILE_NAME[:local_manifest]
34
+ terminate!("local配置文件#{path}不存在!") if !File.file?(path)
35
+ terminate!("请指定名为#{local_manifest_name}的文件!", type:MGIT_ERROR_TYPE[:config_name_error]) if File.basename(path) != local_manifest_name
36
+ end
37
+
38
+ # @!scope 检查lightrepo的仓库url是否重复
39
+ #
40
+ def lint_light_repos!
41
+ repo_urls = {}
42
+ light_repos.each { |light_repo|
43
+ next if light_repo.url.nil? || light_repo.url.length == 0
44
+ repo_urls[light_repo.url] = [] if repo_urls[light_repo.url].nil?
45
+ repo_urls[light_repo.url].push(light_repo.name)
46
+ }
47
+
48
+ error_repos = []
49
+ repo_urls.each { |_, value|
50
+ if value.length > 1
51
+ error_repos += value
52
+ end
53
+ }
54
+
55
+ if error_repos.length > 0
56
+ puts Output.generate_table(error_repos, separator:'|')
57
+ Foundation.help!("以上仓库url配置重复,请修改后重试!")
58
+ end
59
+ end
60
+
61
+ # 解析并校验配置文件
62
+ def lint_raw_json!(dict)
63
+ required_keys = Constants::REQUIRED_CONFIG_KEY
64
+ missing_required_keys = required_keys - dict.keys
65
+ terminate!("配置文件中缺失必需字段:#{missing_required_keys}") if missing_required_keys.length > 0
66
+
67
+ valid_keys = Constants::CONFIG_KEY.values
68
+ valid_repo_keys = Constants::REPO_CONFIG_KEY.values
69
+
70
+ dict.each { |k, v|
71
+ terminate!("配置文件中存在冗余字段:#{k}") unless valid_keys.include?(k)
72
+
73
+ if k == Constants::CONFIG_KEY[:repositories]
74
+ terminate!("配置文件中#{k}字段下的数据应为json格式!") if !dict[k].is_a?(Hash)
75
+
76
+ config_repos = []
77
+ dict[k].each { |repo_name, config|
78
+ terminate!("配置文件中#{k}.#{repo_name}字段下的数据应为json格式!") if !config.is_a?(Hash)
79
+
80
+ # 校验值类型
81
+ config.each { |rk, rv|
82
+ if rk == Constants::REPO_CONFIG_KEY[:mgit_excluded] ||
83
+ rk == Constants::REPO_CONFIG_KEY[:config_repo] ||
84
+ rk == Constants::REPO_CONFIG_KEY[:dummy]
85
+ terminate!("配置文件中#{k}.#{repo_name}.#{rk}字段下的数据应为Bool类型!") if !rv.is_a?(TrueClass) && !rv.is_a?(FalseClass)
86
+ elsif rk == Constants::REPO_CONFIG_KEY[:lock]
87
+ terminate!("配置文件中#{k}.#{repo_name}.#{rk}字段下的数据应为Json类型!") if !rv.is_a?(Hash)
88
+ elsif valid_repo_keys.include?(rk)
89
+ terminate!("配置文件中#{k}.#{repo_name}.#{rk}字段下的数据应为String类型!") if !rv.is_a?(String)
90
+ end
91
+ }
92
+
93
+ # 如果mgit_excluded字段是false或者缺省,则纳入mgit多仓库管理,进行严格校验
94
+ mgit_excluded = config[Constants::REPO_CONFIG_KEY[:mgit_excluded]]
95
+ global_mgit_excluded = dict[Constants::CONFIG_KEY[:mgit_excluded]]
96
+
97
+ if (mgit_excluded.nil? || mgit_excluded == false) && (global_mgit_excluded.nil? || global_mgit_excluded == false)
98
+ # 校验仓库配置必须字段
99
+ valid_required_repo_keys = Constants::REQUIRED_REPO_CONFIG_KEY # 不可缺省的字段
100
+ missing_required_keys = valid_required_repo_keys - config.keys
101
+ terminate!("配置文件中#{k}.#{repo_name}下有缺失字段:#{missing_required_keys.join(', ')}") if missing_required_keys.length > 0
102
+
103
+ # 校验仓库配置冗余字段
104
+ # extra_keys = config.keys - valid_repo_keys
105
+ # terminate!("配置文件中#{k}.#{repo_name}下有冗余字段:#{extra_keys.join(', ')}") if extra_keys.length > 0
106
+
107
+ # 统计指定的配置仓库
108
+ config_repo = config[Constants::REPO_CONFIG_KEY[:config_repo]]
109
+ if !config_repo.nil? && config_repo == true
110
+ config_repos.push(repo_name)
111
+ end
112
+
113
+ # 校验锁定点
114
+ lock_key = Constants::REPO_CONFIG_KEY[:lock]
115
+ lock_config = config[lock_key]
116
+ if !lock_config.nil?
117
+ valid_lock_keys = Constants::REPO_CONFIG_LOCK_KEY.values
118
+
119
+ # 校验锁定点配置值
120
+ lock_config.each { |ck, cv|
121
+ terminate!("配置文件中#{k}.#{repo_name}.#{lock_key}.#{ck}字段下的数据应为String类型!") if !cv.is_a?(String)
122
+ }
123
+
124
+ # 校验锁定点配置必须字段
125
+ terminate!("配置文件中#{k}.#{repo_name}.#{lock_key}下只能指定字段:#{valid_lock_keys.join(', ')}中的一个!") if lock_config.keys.length != 1 || !valid_lock_keys.include?(lock_config.keys.first)
126
+
127
+ # 校验锁定点配置冗余字段
128
+ # extra_keys = lock_config.keys - valid_lock_keys
129
+ # terminate!("配置文件中#{k}.#{repo_name}.#{lock_key}下有冗余字段:#{extra_keys.join(', ')}") if extra_keys.length > 0
130
+
131
+ end
132
+ end
133
+ }
134
+
135
+ # 校验配置仓库字段的合法性
136
+ if config_repos.length > 1
137
+ puts Output.generate_table(config_repos)
138
+ terminate!("配置表中同时指定了以上多个仓库为配置仓库,仅允许指定最多一个!")
139
+ end
140
+ elsif k == Constants::CONFIG_KEY[:version]
141
+ terminate!("配置文件中#{k}字段下的数据应为Integer类型!") if !dict[k].is_a?(Integer)
142
+ elsif k == Constants::CONFIG_KEY[:mgit_excluded]
143
+ terminate!("配置文件中#{k}字段下的数据应为Boolean类型!") if !dict[k].is_a?(TrueClass) && !dict[k].is_a?(FalseClass)
144
+ else
145
+ terminate!("配置文件中#{k}字段下的数据应为String类型!") if !dict[k].is_a?(String)
146
+ end
147
+ }
148
+ end
149
+
150
+ end
151
+
152
+ end
153
+ end
@@ -0,0 +1,427 @@
1
+ #coding=utf-8
2
+
3
+ require 'm-git/open_api/script_download_info'
4
+
5
+ module MGit
6
+
7
+ # --- Ruby环境调用 ---
8
+ class OpenApi
9
+ class << self
10
+
11
+ # 根据配置表下载仓库,并同步工作区
12
+ #
13
+ # @param config_content [String/Hash] 配置表json字符串或字典(key值需为String),不可为nil。
14
+ #
15
+ # @param download_root [String] 仓库下载的根目录,不可为nil。
16
+ #
17
+ # Block: (OpenApi::ScriptDownloadInfo) 下载结果对象
18
+ #
19
+ # ========= 用法 ===========
20
+ ### info对象:[OpenApi::ScriptDownloadInfo]
21
+ # MGit::OpenApi.sync_repos(json,root) { |info|
22
+ # puts "name:#{info.repo_name} \n path:#{info.repo_path}\n result:#{info.result}\n error:#{info.output}\n progress:#{info.progress}"
23
+ # if info.result == OpenApi::DOWNLOAD_RESULT::EXIST
24
+ # elsif info.result == OpenApi::DOWNLOAD_RESULT::SUCCESS
25
+ # elsif info.result == OpenApi::DOWNLOAD_RESULT::FAIL
26
+ # end
27
+ # }
28
+ def sync_repos(config_content, download_root)
29
+ # 空值校验
30
+ error = self.validate_argv(__method__, {"config_content" => config_content, "download_root" => config_content})
31
+ yield(ScriptDownloadInfo.new(nil, nil, DownloadResult::FAIL, error, 0)) if block_given? && !error.nil?
32
+
33
+ begin
34
+ config = Manifest.simple_parse(config_content, strict_mode:false)
35
+ rescue Error => e
36
+ yield(ScriptDownloadInfo.new(nil, nil, DownloadResult::FAIL, e.msg, 0)) if block_given?
37
+ return
38
+ end
39
+
40
+ # 同步工作区仓库(压入或弹出),此时配置表缓存未覆盖,同步操作若取消无需恢复缓存。
41
+ Utils.sync_workspace(download_root, config, recover_cache_if_cancelled:false)
42
+
43
+ # 更新配置缓存
44
+ config.update_cache_with_content(download_root, config.config)
45
+
46
+ # 下载
47
+ self.download(config, download_root, sync_exist:true) { |download_info|
48
+ yield(download_info) if block_given?
49
+ }
50
+
51
+ end
52
+
53
+ # 根据配置表下载仓库
54
+ #
55
+ # @param config_content [String/Hash] 配置表json字符串或字典(key值需为String),不可为nil。
56
+ #
57
+ # @param download_root [String] 仓库下载的根目录,不可为nil。
58
+ #
59
+ # @param manage_git [Boolean] 新下载的仓库是否托管.git,若为false,则在工作区保留新克隆仓库的.git,否则将.git托管到.mgit/sourct-git中,并在工作区创建其软链接
60
+ #
61
+ # Block: (OpenApi::ScriptDownloadInfo) 下载结果对象
62
+ #
63
+ # ========= 用法 ===========
64
+ ### info对象:[OpenApi::ScriptDownloadInfo]
65
+ # MGit::OpenApi.download_repos(json,root) { |info|
66
+ # puts "name:#{info.repo_name} \n path:#{info.repo_path}\n result:#{info.result}\n error:#{info.output}\n progress:#{info.progress}"
67
+ # if info.result == OpenApi::DOWNLOAD_RESULT::EXIST
68
+ # elsif info.result == OpenApi::DOWNLOAD_RESULT::SUCCESS
69
+ # elsif info.result == OpenApi::DOWNLOAD_RESULT::FAIL
70
+ # end
71
+ # }
72
+ def download_repos(config_content, download_root, manage_git:true)
73
+
74
+ # 空值校验
75
+ error = self.validate_argv(__method__, {"config_content" => config_content, "download_root" => config_content})
76
+ yield(ScriptDownloadInfo.new(nil, nil, DownloadResult::FAIL, error, 0)) if block_given? && !error.nil?
77
+
78
+ begin
79
+ config = Manifest.simple_parse(config_content, strict_mode:false)
80
+ rescue Error => e
81
+ yield(ScriptDownloadInfo.new(nil, nil, DownloadResult::FAIL, e.msg, 0)) if block_given?
82
+ return
83
+ end
84
+
85
+ self.download(config, download_root, manage_git:manage_git) { |download_info|
86
+ yield(download_info) if block_given?
87
+ }
88
+
89
+ end
90
+
91
+ # 将仓库切到特定的分支或commit
92
+ #
93
+ # @param repo_name [String] 仓库名,不可为nil。
94
+ #
95
+ # @param repo_path [String] 仓库本地地址,不可为nil。
96
+ #
97
+ # @param create_branch [String] 是否创建新分支。
98
+ #
99
+ # @param branch [String] 需要切换的分支,可为nil。。
100
+ # 如果非nil,若仓库有该分支则直接切换,没有则在指定commit上创建。
101
+ # 如果为nil,则直接切换到指定commit。
102
+ #
103
+ # @param commit_id [String] 需要切换的commit id,不可为nil。
104
+ #
105
+ # @param allow_fetch [String] 指定在本地无指定分支或commit时是否fetch后重试。
106
+ #
107
+ # @return [(String, Boolean)] (error, did_create_new_branch)
108
+ # - error: 错误信息,若无错误,返回nile
109
+ # - did_create_new_branch: 本次checkout是否创建新分支
110
+ # ========= 用法 ===========
111
+ ### 自信切commit
112
+ # MGit::OpenApi.checkout(name, abs_path, base_commit:'4922620')
113
+ #
114
+ ### 自信切分支,分支确定存在
115
+ # MGit::OpenApi.checkout(name, abs_path, branch:'master')
116
+ #
117
+ ### 尝试切分支,没有就创建
118
+ # MGit::OpenApi.checkout(name, abs_path, create_branch:true, branch:'master', base_commit:'4922620')
119
+ def checkout(repo_name, repo_path, create_branch:false, branch:nil, base_commit:nil, allow_fetch:false)
120
+
121
+ # 空值校验
122
+ error = self.validate_argv(__method__, {"repo_name" => repo_name, "repo_path" => repo_path})
123
+ return error, false if !error.nil?
124
+ return 'branch和base_commit必须传入一个值!', false if branch.nil? && base_commit.nil?
125
+
126
+ # 生成仓库对象
127
+ if Repo.is_git_repo?(repo_path)
128
+ repo = Repo.new(repo_name, repo_path)
129
+ else
130
+ return "路径位置\"#{repo_path}\"不是git仓库!", false
131
+ end
132
+
133
+ error = nil
134
+ did_create_new_branch = false
135
+
136
+ # 如果指定可fetch,那么在本地缺失信息时执行fetch
137
+ if allow_fetch
138
+
139
+ # 查询分支和commit
140
+ should_fetch = !branch.nil? && !repo.status_checker.local_branch_exist?(branch) ||
141
+ !base_commit.nil? && !repo.status_checker.commit_exist?(base_commit)
142
+
143
+ error = fetch(repo.name, repo.path) if should_fetch
144
+
145
+ return error, did_create_new_branch if !error.nil?
146
+ end
147
+
148
+ if !branch.nil?
149
+
150
+ # 已在指定分支则不操作
151
+ if repo.status_checker.current_branch(strict_mode:false) != branch
152
+
153
+ # 有本地改动禁止新建/切换分支
154
+ if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]
155
+ error = "仓库#{repo_name}有改动,无法切换/创建分支!"
156
+
157
+ # 有本地或对应远程分支,直接切换
158
+ elsif repo.status_checker.local_branch_exist?(branch) || repo.status_checker.remote_branch_exist?(branch)
159
+ success, output = repo.execute_git_cmd('checkout', branch)
160
+ error = output if !success
161
+
162
+ # 无分支,在指定commit上创建
163
+ elsif create_branch
164
+
165
+ if !base_commit.nil?
166
+ if repo.status_checker.commit_exist?(base_commit)
167
+ success, output = repo.execute_git_cmd('checkout', "-b #{branch} #{base_commit}")
168
+ if !success
169
+ error = output
170
+ else
171
+ did_create_new_branch = true
172
+ end
173
+ else
174
+ error = "仓库#{repo_name}创建新分支时,未找到指定基点commit!"
175
+ end
176
+ else
177
+ error = "仓库#{repo_name}创建新分支时,没有指定基点commit!"
178
+ end
179
+
180
+ else
181
+ error = "仓库#{repo_name}无分支#{branch},且未指定创建!"
182
+ end
183
+
184
+ end
185
+
186
+ elsif !base_commit.nil?
187
+
188
+ # 已在指定commit则不操作
189
+ if !repo.status_checker.current_branch(strict_mode:false).nil? ||
190
+ repo.status_checker.current_head(strict_mode:false) != base_commit
191
+
192
+
193
+ # 有本地改动禁止新建/切换分支
194
+ if repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:dirty]
195
+ error = "仓库#{repo_name}有改动,无法切换HEAD!"
196
+
197
+ # 未指定branch,直接切到指定commit
198
+ elsif repo.status_checker.commit_exist?(base_commit)
199
+ success, output = repo.execute_git_cmd('checkout', base_commit)
200
+ error = output if !success
201
+
202
+ # 指定commit不存在报错
203
+ else
204
+ error = "仓库#{repo_name}切换操作失败,指定commit不存在!"
205
+ end
206
+
207
+ end
208
+
209
+ end
210
+
211
+ return error, did_create_new_branch
212
+ end
213
+
214
+ # 执行fetch操作
215
+ #
216
+ # @param repo_name [Stirng] 仓库名
217
+ #
218
+ # @param repo_path [String] 仓库路径
219
+ #
220
+ # @return [String] 错误信息,如果执行成功返回nil
221
+ #
222
+ def fetch(repo_name, repo_path)
223
+ # 空值校验
224
+ error = self.validate_argv(__method__, {"repo_name" => repo_name, "repo_path" => repo_path})
225
+ return error if !error.nil?
226
+
227
+ if Dir.exist?(repo_path) && Repo.is_git_repo?(repo_path)
228
+ repo = Repo.new(repo_name, repo_path)
229
+ success, output = repo.execute_git_cmd('fetch', '')
230
+ return output if !success
231
+ else
232
+ return '指定路径不存在或不是git仓库!'
233
+ end
234
+ end
235
+
236
+ # 执行pull操作
237
+ #
238
+ # @param repo_name [Stirng] 仓库名
239
+ #
240
+ # @param repo_path [String] 仓库路径
241
+ #
242
+ # @return [String] 错误信息,如果执行成功返回nil
243
+ #
244
+ def pull(repo_name, repo_path)
245
+ # 空值校验
246
+ error = self.validate_argv(__method__, {"repo_name" => repo_name, "repo_path" => repo_path})
247
+ return error if !error.nil?
248
+
249
+ if Dir.exist?(repo_path) && Repo.is_git_repo?(repo_path)
250
+ repo = Repo.new(repo_name, repo_path)
251
+ success, output = repo.execute_git_cmd('pull', '')
252
+ return output if !success
253
+ else
254
+ return '指定路径不存在或不是git仓库!'
255
+ end
256
+ end
257
+
258
+ # 检查给定仓库是否工作区脏,工作区的.git是否是软链接
259
+ #
260
+ # @param path_dict [Hash] 待校验的冗余仓库地址:{ "name": "abs_path" }
261
+ #
262
+ # @return [Hash] 冗余仓库状态信息:{ "name": { "clean": true, "git_linked": true , "error": "error_msg"} }
263
+ # - "clean": 为true,表示工作区干净,否则脏。
264
+ # - "git_linked": 为true,表示.git实体放在.mgit/source-git下,工作区.git仅为软链接,否则为实体。
265
+ # - "error": 如果校验成功则无该字段,否则校验失败,值为错误信息。注意,若出错则无"clean"和"git_linked"字段。
266
+ #
267
+ def check_extra_repos(path_dict)
268
+
269
+ # 空值校验
270
+ error = self.validate_argv(__method__, {"path_dict" => path_dict})
271
+ return {} if !error.nil?
272
+
273
+ output = {}
274
+ path_dict.each { |name, path|
275
+ output[name] = {}
276
+ # 生成仓库对象
277
+ if Repo.is_git_repo?(path)
278
+ repo = Repo.new(name, path)
279
+ output[name]['clean'] = repo.status_checker.status == Repo::Status::GIT_REPO_STATUS[:clean]
280
+ output[name]['git_linked'] = File.symlink?(File.join(path, '.git'))
281
+ else
282
+ output[name]['error'] = "路径位置\"#{repo_path}\"不是git仓库!"
283
+ end
284
+ }
285
+ return output
286
+ end
287
+
288
+ # 并发遍历
289
+ #
290
+ # @param array [<Object>] 遍历数组
291
+ #
292
+ # @param max_concurrent_count [Integer] 最大并发数
293
+ #
294
+ # ========= 用法 ===========
295
+ # concurrent_enumerate([item1, item2...]) { |item|
296
+ # do something with item...
297
+ # }
298
+ def concurrent_enumerate(array, max_concurrent_count:5)
299
+ if array.is_a?(Array) && array.length > 0
300
+ array.peach(max_concurrent_count) { |item|
301
+ yield(item) if block_given?
302
+ }
303
+ end
304
+ end
305
+
306
+ # 在不拉仓库的情况下,批量查询当前用户是否有权限拉取代码
307
+ #
308
+ # @param root [String] 工程根目录
309
+ #
310
+ # @param url_list [Array<String>] 一组远程仓库url
311
+ #
312
+ # ========= 用法 ===========
313
+ # url_list = [
314
+ # 'https://github.com/baidu/baiduapp-platform/a',
315
+ # 'https://github.com/baidu/baiduapp-platform/b'
316
+ # ]
317
+ # MGit::OpenApi.check_permission_batch(arr) { |url, has_permission, progress|
318
+ # do something...
319
+ # }
320
+ def check_permission_batch(root, url_list)
321
+ mutex = Mutex.new
322
+ task_count = 0
323
+ total_task = url_list.length
324
+ concurrent_enumerate(url_list) { |url|
325
+ has_permission = self.check_permission(url, root:root)
326
+ mutex.lock
327
+ task_count += 1
328
+ progress = Float(task_count) / total_task
329
+ yield(url, has_permission, progress) if block_given?
330
+ mutex.unlock
331
+ }
332
+ end
333
+
334
+ # 在不拉仓库的情况下,查询当前用户是否有权限拉取代码
335
+ #
336
+ # @param root [String] 工程根目录
337
+ #
338
+ # @param url [String] 远程仓库url
339
+ #
340
+ # @return [Boolean] 是否有权限
341
+ #
342
+ # ========= 用法 ===========
343
+ # url = 'https://github.com/baidu/baiduapp-platform/a'
344
+ # result = MGit::OpenApi.check_permission(url)
345
+ def check_permission(url, root:nil)
346
+ if !root.nil?
347
+ git_store = Utils.generate_git_store(root, url)
348
+ if !git_store.nil? && Dir.exist?(git_store)
349
+ return true
350
+ end
351
+ end
352
+
353
+ return Utils.has_permission_of_remote?(url)
354
+ end
355
+
356
+ # ----- Util -----
357
+ # 校验参数合法性
358
+ #
359
+ # @param method_name [String] 方法名
360
+ #
361
+ # @param args [Hash] 参数数组
362
+ #
363
+ # @return [String] 错误信息,正常返回nil
364
+ #
365
+ def validate_argv(method_name, args)
366
+ args.each { |k,v|
367
+ if v.nil?
368
+ return "MGit API调用错误: MGit::OpenApi.#{method_name}()的#{k}参数不能为空!"
369
+ end
370
+ }
371
+ return nil
372
+ end
373
+
374
+ # 根据配置对象下载仓库
375
+ #
376
+ # @param config_content [String/Hash] 配置表json字符串或字典(key值需为String),不可为nil。
377
+ #
378
+ # @param download_root [String] 仓库下载的根目录,不可为nil。
379
+ #
380
+ # @param manage_git [Boolean] 新下载的仓库是否托管.git,若为false,则在工作区保留新克隆仓库的.git,否则将.git托管到.mgit/sourct-git中,并在工作区创建其软链接
381
+ #
382
+ # ========= 用法 ===========
383
+ # self.download(config, download_root) { |download_info|
384
+ # do something with download_info....
385
+ # }
386
+ def download(config, download_root, manage_git:true, sync_exist: false)
387
+ task_count = 0
388
+ total_task = config.light_repos.length
389
+ mutex = Mutex.new
390
+ concurrent_enumerate(config.light_repos) { |light_repo|
391
+ name = light_repo.name
392
+ path = light_repo.abs_dest(download_root)
393
+ result = nil
394
+ output = nil
395
+ progress = 0
396
+
397
+ if Dir.exist?(path) && Repo.is_git_repo?(path)
398
+ if sync_exist
399
+ repo, error = Repo.generate_softly(download_root, light_repo)
400
+ error = Repo::SyncHelper.sync_exist_repo(repo, repo.config) if !repo.nil?
401
+ end
402
+ result = DownloadResult::EXIST
403
+ elsif Utils.has_permission_of_remote?(light_repo.url)
404
+ error, repo = Repo::SyncHelper.sync_new_repo(light_repo, download_root, link_git:manage_git)
405
+ if !error.nil?
406
+ result = DownloadResult::FAIL
407
+ output = error
408
+ else
409
+ result = DownloadResult::SUCCESS
410
+ end
411
+ else
412
+ result = DownloadResult::FAIL
413
+ output = "当前用户没有该仓库的克隆权限:#{light_repo.name}(#{light_repo.url})!"
414
+ end
415
+
416
+ mutex.lock
417
+ task_count += 1
418
+ progress = Float(task_count) / total_task
419
+ yield(ScriptDownloadInfo.new(name, path, result, output, progress)) if block_given?
420
+ mutex.unlock
421
+ }
422
+ end
423
+ end
424
+
425
+ end
426
+
427
+ end