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,112 @@
1
+
2
+ module MGit
3
+ class PluginManager
4
+
5
+ # @!scope 加载插件
6
+ # 1. 先加载本地源码插件
7
+ # 2. 搜索加载gem插件
8
+ # 3. 处理加载注入的插件
9
+ #
10
+ def self.setup
11
+ lib_dir = File.dirname(__FILE__)
12
+ plugins_dir = File.join(File.dirname(File.dirname(lib_dir)), 'plugins')
13
+ load_local_plugin_dir('mgit', plugins_dir)
14
+ load_local_plugin_dir('m-git', plugins_dir)
15
+ load_gem_plugins('mgit')
16
+ load_gem_plugins('m-git')
17
+
18
+ inject_flag = '--inject='.freeze
19
+ inject_arg = ::ARGV.find { |arg| arg.start_with?(inject_flag) }
20
+ if inject_arg
21
+ ::ARGV.delete(inject_arg)
22
+ inject_file = inject_arg[inject_flag.length..-1]
23
+ if !inject_file.start_with?('~') && !inject_file.start_with?('/')
24
+ inject_file = File.join(Dir.pwd, inject_file)
25
+ end
26
+ inject_file = File.expand_path(inject_file)
27
+ if File.exist?(inject_file)
28
+ if File.file?(inject_file)
29
+ require inject_file
30
+ elsif File.directory?(inject_file)
31
+ load_local_plugins('mgit', inject_file)
32
+ load_local_plugins('m-git', inject_file)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ # 加载本地的plugin优先,然后加载gem的plugin
39
+ # [Hash{String=> [String]}]
40
+ #
41
+ def self.loaded_plugins
42
+ @loaded_plugins ||= {}
43
+ end
44
+
45
+ # 加载插件集合目录,该目录下每个文件夹遍历加载一次
46
+ #
47
+ def self.load_local_plugin_dir(plugin_prefix, plugins_dir)
48
+ Dir.foreach(plugins_dir) do |file|
49
+ next if file == '.' || file == '..' || file == '.DS_Store'
50
+ plugin_root = File.join(plugins_dir, file)
51
+ next unless File.directory?(plugin_root)
52
+ load_local_plugins(plugin_prefix, plugin_root, file)
53
+ end if Dir.exist?(plugins_dir)
54
+ end
55
+
56
+ # 加载单个本地插件
57
+ #
58
+ def self.load_local_plugins(plugin_prefix, plugin_root, with_name = nil)
59
+ with_name ||= plugin_root
60
+ glob = "#{plugin_prefix}_plugin#{Gem.suffix_pattern}"
61
+ glob = File.join(plugin_root, '**', glob)
62
+ plugin_files = Dir[glob].map { |f| f.untaint }
63
+ return if loaded_plugins[with_name] || plugin_files.nil? || plugin_files.empty?
64
+ safe_activate_plugin_files(with_name, plugin_files)
65
+ loaded_plugins[with_name] = plugin_files
66
+ end
67
+
68
+ # 加载已安装的gem插件
69
+ #
70
+ def self.load_gem_plugins(plugin_prefix)
71
+ glob = "#{plugin_prefix}_plugin#{Gem.suffix_pattern}"
72
+ gem_plugins = Gem::Specification.latest_specs.map do |spec|
73
+ matches = spec.matches_for_glob(glob)
74
+ [spec, matches] unless matches.empty?
75
+ end.compact
76
+
77
+ gem_plugins.map do |spec, paths|
78
+ next if loaded_plugins[spec.name]
79
+ safe_activate_gem(spec, paths)
80
+ loaded_plugins[spec.full_name] = paths
81
+ end
82
+ end
83
+
84
+ def self.safe_activate_gem(spec, paths)
85
+ spec.activate
86
+ paths.each { |path| require(path) }
87
+ true
88
+ rescue Exception => exception # rubocop:disable RescueException
89
+ message = "\n---------------------------------------------"
90
+ message << "\n加载插件失败 `#{spec.full_name}`.\n"
91
+ message << "\n#{exception.class} - #{exception.message}"
92
+ message << "\n#{exception.backtrace.join("\n")}"
93
+ message << "\n---------------------------------------------\n"
94
+ warn message.ansi.yellow
95
+ false
96
+ end
97
+
98
+ def self.safe_activate_plugin_files(plugin_name, paths)
99
+ paths.each { |path| require(path) }
100
+ true
101
+ rescue Exception => exception
102
+ message = "\n---------------------------------------------"
103
+ message << "\n加载插件失败 `#{plugin_name}`.\n"
104
+ message << "\n#{exception.class} - #{exception.message}"
105
+ message << "\n#{exception.backtrace.join("\n")}"
106
+ message << "\n---------------------------------------------\n"
107
+ warn message.ansi.yellow
108
+ false
109
+ end
110
+
111
+ end
112
+ end
data/lib/m-git/repo.rb ADDED
@@ -0,0 +1,133 @@
1
+ #coding=utf-8
2
+
3
+ require 'm-git/repo/status'
4
+ require 'm-git/repo/sync_helper'
5
+
6
+ module MGit
7
+
8
+ class Repo
9
+
10
+ # 仓库名
11
+ attr_reader :name
12
+
13
+ # 仓库实体完整路径
14
+ attr_reader :path
15
+
16
+ # 仓库状态检查器
17
+ attr_reader :status_checker
18
+
19
+ # 配置文件中的设置:Manifest::LightRepo
20
+ attr_reader :config
21
+
22
+ def initialize(name, path, config:nil)
23
+ @name = name
24
+ @path = path
25
+ @config = config
26
+ @status_checker = Status.new(path)
27
+ end
28
+
29
+ # 根据传入的绝对路径生成repo对象,如果传入路径不对应git仓库,则返回nil
30
+ # return [(Repo, String)] (repo, error_message)
31
+ def self.generate_softly(root, config)
32
+ abs_path = config.abs_dest(root)
33
+ if self.is_git_repo?(abs_path)
34
+ repo = Repo.new(config.name, config.abs_dest(root), config:config)
35
+ return repo, nil
36
+ else
37
+ return nil, "路径位置\"#{abs_path}\"不是git仓库!"
38
+ end
39
+ end
40
+
41
+ # 根据传入的绝对路径生成repo对象,如果传入路径不对应git仓库,则抛出异常
42
+ def self.generate_strictly(root, config)
43
+ abs_path = config.abs_dest(root)
44
+ if self.is_git_repo?(abs_path)
45
+ return Repo.new(config.name, config.abs_dest(root), config:config)
46
+ elsif File.directory?(abs_path)
47
+ Foundation.help!("路径位置\"#{abs_path}\"不是git仓库!请先确认并手动删除该文件夹,然后执行\"mgit sync -n\"重新下载。")
48
+ else
49
+ # (注意,如果不希望被同步仓库的.git实体被mgit管理,请执行\"mgit sync -n -o\",该方式将不会把.git实体放置到.mgit/souce-git中,更适合开发中途接入mgit的用户)
50
+ Foundation.help!("路径位置\"#{abs_path}\"不是git仓库!请执行\"mgit sync -n\"重新下载。")
51
+ end
52
+ end
53
+
54
+ def self.check_git_dest(root, config)
55
+ abs_path = config.abs_dest(root)
56
+ if self.is_git_repo?(abs_path)
57
+ true
58
+ elsif File.directory?(abs_path)
59
+ Foundation.help!("路径位置\"#{abs_path}\"不是git仓库!请先确认并手动删除该文件夹,然后执行\"mgit sync -n\"重新下载。")
60
+ else
61
+ false
62
+ end
63
+ end
64
+
65
+ # 检查传入路径是不是git仓库
66
+ #
67
+ # @param path [String] 仓库路径
68
+ #
69
+ def self.is_git_repo?(path)
70
+ Dir.is_git_repo?(path)
71
+ end
72
+
73
+ # 对仓库执行shell指令的入口
74
+ #
75
+ # @param abs_cmd [String] 完整指令
76
+ #
77
+ # @return [Boolean,String] 是否成功;输出结果
78
+ #
79
+ def execute(abs_cmd)
80
+ Timer.start(name, use_lock:true)
81
+ Utils.execute_shell_cmd(abs_cmd) { |stdout, stderr, status|
82
+ # 标记状态更新
83
+ @status_checker.refresh
84
+
85
+ Timer.stop(name, use_lock:true)
86
+ if status.success?
87
+ output = stdout.nil? || stdout.length == 0 ? stderr : stdout
88
+ return true, output
89
+ else
90
+ output = stderr.nil? || stderr.length == 0 ? stdout : stderr
91
+ return false, output
92
+ end
93
+ }
94
+ end
95
+
96
+ # 对仓库执行git指令的入口
97
+ def execute_git_cmd(cmd, opts)
98
+ return execute(git_cmd(cmd, opts))
99
+ end
100
+
101
+ # 对git指令进行加工,指定正确的执行目录
102
+ def git_cmd(cmd, opts)
103
+ git_dir = File.join(@path, '.git')
104
+
105
+ # 组装导出变量
106
+ export_pair = nil
107
+ Constants::MGIT_EXPORT_INFO.each { |k,v|
108
+ if !export_pair.nil?
109
+ export_pair += " #{k.to_s}=#{v}"
110
+ else
111
+ export_pair = "export #{k.to_s}=#{v}"
112
+ end
113
+ }
114
+ export_pair += " && " if !export_pair.nil?
115
+
116
+ return "#{export_pair}git --git-dir=\"#{git_dir}\" --work-tree=\"#{@path}\" #{cmd} #{opts}"
117
+ end
118
+
119
+ # 判断实际url和配置url是否一致
120
+ #
121
+ # @return [Boolean] 是否一致
122
+ #
123
+ def url_consist?
124
+ if !self.config.nil?
125
+ return Utils.url_consist?(self.status_checker.default_url, self.config.url)
126
+ else
127
+ return true
128
+ end
129
+ end
130
+
131
+ end
132
+
133
+ end
@@ -0,0 +1,481 @@
1
+ #coding=utf-8
2
+
3
+ module MGit
4
+ class Repo
5
+ class Status
6
+
7
+ # https://git-scm.com/docs/git-status
8
+ # status格式:XY PATH, X表示暂存区状态,Y表示工作区状态,在merge冲突的情况下,XY为冲突两边状态,未跟踪文件为??,忽略文件为!!。
9
+ # 如:
10
+ # MM some_file
11
+ # M some_file
12
+ # M some_file
13
+ #
14
+ # 其中:
15
+ # ' ' = unmodified
16
+ # M = modified
17
+ # A = added
18
+ # D = deleted
19
+ # R = renamed
20
+ # C = copied
21
+ # U = updated but unmerged
22
+
23
+ # 具体规则:
24
+ # X Y Meaning
25
+ # -------------------------------------------------
26
+ # [AMD] not updated
27
+ # M [ MD] updated in index
28
+ # A [ MD] added to index
29
+ # D deleted from index
30
+ # R [ MD] renamed in index
31
+ # C [ MD] copied in index
32
+ # [MARC] index and work tree matches
33
+ # [ MARC] M work tree changed since index
34
+ # [ MARC] D deleted in work tree
35
+ # [ D] R renamed in work tree
36
+ # [ D] C copied in work tree
37
+ # -------------------------------------------------
38
+ # D D unmerged, both deleted
39
+ # A U unmerged, added by us
40
+ # U D unmerged, deleted by them
41
+ # U A unmerged, added by them
42
+ # D U unmerged, deleted by us
43
+ # A A unmerged, both added
44
+ # U U unmerged, both modified
45
+ # -------------------------------------------------
46
+ # ? ? untracked
47
+ # ! ! ignored
48
+ # -------------------------------------------------
49
+
50
+ FILE_STATUS = {
51
+ :unmodified => ' ',
52
+ :modified => 'M',
53
+ :added => 'A',
54
+ :deleted => 'D',
55
+ :renamed => 'R',
56
+ :copied => 'C'
57
+ }.freeze
58
+
59
+ FILE_STATUS_CONFLICT = {
60
+ :both_deleted => 'DD',
61
+ :we_added => 'AU',
62
+ :they_deleted => 'UD',
63
+ :they_added => 'UA',
64
+ :we_deleted => 'DU',
65
+ :both_added => 'AA',
66
+ :both_modified => 'UU'
67
+ }.freeze
68
+
69
+ FILE_STATUS_SPECIAL = {
70
+ :untracked => '??',
71
+ :ignored => '!!'
72
+ }.freeze
73
+
74
+ FILE_STATUS_MESSAGE = {
75
+ FILE_STATUS[:unmodified].to_s => nil,
76
+ FILE_STATUS[:modified].to_s => '[已修改]',
77
+ FILE_STATUS[:added].to_s => '[已添加]',
78
+ FILE_STATUS[:deleted].to_s => '[已删除]',
79
+ FILE_STATUS[:renamed].to_s => '[重命名]',
80
+ FILE_STATUS[:copied].to_s => '[已拷贝]',
81
+ FILE_STATUS_CONFLICT[:both_deleted].to_s => '[删除|删除]',
82
+ FILE_STATUS_CONFLICT[:we_added].to_s => '[添加|修改]',
83
+ FILE_STATUS_CONFLICT[:they_deleted].to_s => '[修改|删除]',
84
+ FILE_STATUS_CONFLICT[:they_added].to_s => '[修改|添加]',
85
+ FILE_STATUS_CONFLICT[:we_deleted].to_s => '[删除|修改]',
86
+ FILE_STATUS_CONFLICT[:both_added].to_s => '[添加|添加]',
87
+ FILE_STATUS_CONFLICT[:both_modified].to_s => '[修改|修改]',
88
+ FILE_STATUS_SPECIAL[:untracked].to_s => '[未跟踪]',
89
+ FILE_STATUS_SPECIAL[:ignored].to_s => '[被忽略]'
90
+ }.freeze
91
+
92
+ STATUS_TYPE = {
93
+ :normal => 1,
94
+ :conflicts => 2,
95
+ :special => 3
96
+ }.freeze
97
+
98
+ GIT_REPO_STATUS = {
99
+ :clean => 'clean',
100
+ :dirty => 'dirty'
101
+ }.freeze
102
+
103
+ GIT_REPO_STATUS_DIRTY_ZONE = {
104
+ :index => 1, # 暂存区
105
+ :work_tree => 1 << 1, # 工作区
106
+ :special => 1 << 2 # 未跟踪和被ignore
107
+ }.freeze
108
+
109
+ GIT_BRANCH_STATUS = {
110
+ :ahead => 'ahead',
111
+ :behind => 'behind',
112
+ :detached => 'detached',
113
+ :diverged => 'diverged',
114
+ :no_remote => 'no_remote',
115
+ :no_tracking => 'no_tracking',
116
+ :up_to_date => 'up_to_date'
117
+ }.freeze
118
+
119
+ def initialize(path)
120
+ @path = path
121
+ @status, @message = nil, nil
122
+ @branch_status, @branch_message, @dirty_zone = nil, nil, nil
123
+ end
124
+
125
+ def status
126
+ check_repo_status if @status.nil?
127
+ return @status
128
+ end
129
+
130
+ def message
131
+ check_repo_status if @message.nil?
132
+ return @message
133
+ end
134
+
135
+ def dirty_zone
136
+ check_repo_status if @dirty_zone.nil?
137
+ return @dirty_zone
138
+ end
139
+
140
+ def branch_status
141
+ check_branch_status if @branch_status.nil?
142
+ return @branch_status
143
+ end
144
+
145
+ def branch_message
146
+ check_branch_status if @branch_message.nil?
147
+ return @branch_message
148
+ end
149
+
150
+ # 是否处于merge中间态
151
+ def is_in_merge_progress?
152
+ cmd = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" merge HEAD"
153
+ Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|
154
+ return !status.success?
155
+ }
156
+ end
157
+
158
+ # 是否处于rebase中间态
159
+ def is_in_rebase_progress?
160
+ cmd = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" rebase HEAD"
161
+ Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|
162
+ return !status.success?
163
+ }
164
+ end
165
+
166
+ # 当前分支是否是某个分支的祖先
167
+ def is_ancestor_of_branch?(branch)
168
+ c_branch = current_branch
169
+ cmd = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" merge-base --is-ancestor #{c_branch} #{branch}"
170
+ cmd2 = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" rev-parse --verify #{branch}"
171
+ cmd3 = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" rev-parse --verify #{c_branch}"
172
+
173
+ is_ancestor = false
174
+ Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|
175
+ is_ancestor = status.success?
176
+ }
177
+
178
+ # 当两个分支指向同一个commit的时候,“merge-base --is-ancestor”指令依然返回true,这里判断如果是这样当情况,就返回false
179
+ if is_ancestor
180
+ branch_hash = nil
181
+ Utils.execute_shell_cmd(cmd2) { |stdout, stderr, status|
182
+ branch_hash = stdout.chomp if status.success?
183
+ }
184
+
185
+ c_branch_hash = nil
186
+ Utils.execute_shell_cmd(cmd3) { |stdout, stderr, status|
187
+ c_branch_hash = stdout.chomp if status.success?
188
+ }
189
+ return !branch_hash.nil? && !c_branch_hash.nil? && branch_hash != c_branch_hash
190
+ else
191
+ return false
192
+ end
193
+ end
194
+
195
+ # 查询追踪的远程分支
196
+ #
197
+ # @param branch [String] 查询分支
198
+ #
199
+ # @param use_cache [Boolean] default: false,是否使用缓存
200
+ #
201
+ # @return [String] 追踪的远程分支
202
+ #
203
+ def tracking_branch(branch, use_cache:false)
204
+ return @tracking_branch if use_cache && !@tracking_branch.nil?
205
+
206
+ cmd = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" rev-parse --abbrev-ref #{branch}@{u}"
207
+ Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|
208
+ if status.success?
209
+ @tracking_branch = stdout.chomp
210
+ return @tracking_branch
211
+ end
212
+ return nil
213
+ }
214
+ end
215
+
216
+ # 查询当前分支
217
+ #
218
+ # @param strict_mode [Boolean] default: true,是否是严格模式。在严格模式下,失败即终止。在非严格模式下,失败返回nil。
219
+ #
220
+ # @param use_cache [Boolean] default: false,是否使用缓存
221
+ #
222
+ # @return [String] 当前分支,查询失败或游离返回nil
223
+ #
224
+ def current_branch(strict_mode:true, use_cache:false)
225
+ return @current_branch if use_cache && !@current_branch.nil?
226
+
227
+ cmd = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" symbolic-ref --short -q HEAD"
228
+ Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|
229
+ if status.success?
230
+ @current_branch = stdout.chomp
231
+ return @current_branch
232
+ elsif strict_mode
233
+ Foundation.help!("仓库#{File.basename(@path)}当前分支查询失败:当前HEAD不指向任何分支!")
234
+ else
235
+ return nil
236
+ end
237
+ }
238
+ end
239
+
240
+ # 查询当前HEAD指向的commit
241
+ #
242
+ # @param strict_mode [Boolean] default: true,是否是严格模式。在严格模式下,失败即终止。在非严格模式下,失败返回nil。
243
+ #
244
+ # @return [String] commit id
245
+ #
246
+ def current_head(strict_mode:true)
247
+ cmd = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" rev-parse --short HEAD"
248
+ Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|
249
+ if status.success?
250
+ return stdout.chomp
251
+ elsif strict_mode
252
+ Foundation.help!("仓库#{File.basename(@path)}HEAD指向查询失败:#{stderr}")
253
+ else
254
+ return nil
255
+ end
256
+ }
257
+ end
258
+
259
+ # 指定分支本地是否存在
260
+ def local_branch_exist?(branch)
261
+ return has_branch?(branch, false)
262
+ end
263
+
264
+ # 指定分支是否存在对应远程分支(origin)
265
+ def remote_branch_exist?(branch)
266
+ return has_branch?(branch, true)
267
+ end
268
+
269
+ # commit是否存在
270
+ def commit_exist?(commit)
271
+ cmd = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" cat-file -t #{commit}"
272
+ Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|
273
+ return status.success?
274
+ }
275
+ end
276
+
277
+ # 查询仓库url
278
+ def default_url
279
+ cmd = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" config remote.origin.url"
280
+ Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|
281
+ if status.success?
282
+ return stdout.chomp
283
+ else
284
+ return nil
285
+ end
286
+ }
287
+ end
288
+
289
+ # 清空所有缓存内容
290
+ def refresh
291
+ @status, @message = nil, nil
292
+ @branch_status, @branch_message = nil, nil
293
+ @dirty_zone = 0
294
+ end
295
+
296
+ private
297
+
298
+ # 拼接.git在工作区的路径
299
+ def git_dir
300
+ return File.join(@path, '.git')
301
+ end
302
+
303
+ # 返回工作区路径
304
+ def work_tree
305
+ return @path
306
+ end
307
+
308
+ # 分支是否存在
309
+ #
310
+ # @param branch [String] 查询分支
311
+ #
312
+ # @param is_remote [Boolean] 是否查询远程,若是,则查询origin/<branch>,否则仅查询<branch>
313
+ #
314
+ def has_branch?(branch, is_remote)
315
+ return false if branch.nil?
316
+
317
+ # 如果检查分支是当前分支(格式为"* current_branch"),后续检查会失效,因此直接返回true
318
+ return true if branch == current_branch(strict_mode:false) && !is_remote
319
+
320
+ padding = " " # 终端输出的分支名前有两个空格
321
+ cmd = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" branch #{is_remote ? '-r ' : ''}| grep -xi \"#{padding}#{is_remote ? 'origin/' : ''}#{branch}\""
322
+ Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|
323
+ return status.success?
324
+ }
325
+ end
326
+
327
+ # 查询仓库状态
328
+ def check_repo_status
329
+ cmd = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" status --porcelain"
330
+ Utils.execute_shell_cmd(cmd) { |stdout, stderr, status|
331
+ if status.success?
332
+ if stdout.length > 0
333
+ @status, @message, @dirty_zone = parse_change(stdout.split("\n"))
334
+ else
335
+ @status = GIT_REPO_STATUS[:clean]
336
+ @message = [["仓库状态", ['无改动']]]
337
+ end
338
+ else
339
+ Foundation.help!("仓库#{File.basename(@path)}状态查询失败:#{stderr}")
340
+ end
341
+ }
342
+ end
343
+
344
+ # 查询分支状态
345
+ def check_branch_status
346
+ branch = current_branch(strict_mode:false)
347
+ remote_branch = tracking_branch(branch)
348
+ is_tracking = !remote_branch.nil?
349
+
350
+ # 当前HEAD不指向任何分支
351
+ if branch.nil?
352
+ @branch_status = GIT_BRANCH_STATUS[:detached]
353
+ @branch_message = "当前HEAD处于游离状态"
354
+ # 当前已经追踪远程分支
355
+ elsif is_tracking
356
+ cmd1 = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" rev-list #{branch}..#{remote_branch}"
357
+ cmd2 = "git --git-dir=\"#{git_dir}\" --work-tree=\"#{work_tree}\" rev-list #{remote_branch}..#{branch}"
358
+ stdout1, stdout2 = nil, nil
359
+
360
+ Utils.execute_shell_cmd(cmd1) { |stdout, stderr, status|
361
+ if status.success?
362
+ stdout1 = stdout
363
+ else
364
+ Foundation.help!("检查仓库clean细节时,执行#{cmd1}命令失败:#{stderr}")
365
+ end
366
+ }.execute_shell_cmd(cmd2) { |stdout, stderr, status|
367
+ if status.success?
368
+ stdout2 = stdout
369
+ else
370
+ Foundation.help!("检查仓库clean细节时,执行#{cmd2}命令失败:#{stderr}")
371
+ end
372
+ }
373
+
374
+ if stdout1.length == 0 && stdout2.length == 0
375
+ @branch_status = GIT_BRANCH_STATUS[:up_to_date]
376
+ @branch_message = "当前分支与远程分支[同步]"
377
+ elsif stdout1.length == 0 && stdout2.length > 0
378
+ @branch_status = GIT_BRANCH_STATUS[:ahead]
379
+ # @branch_message = "当前分支超前远程分支[#{stdout2.split("\n").length}]个提交"
380
+ @branch_message = "当前分支[超前]远程分支"
381
+ elsif stdout1.length > 0 && stdout2.length == 0
382
+ @branch_status = GIT_BRANCH_STATUS[:behind]
383
+ # @branch_message = "当前分支落后远程分支[#{stdout1.split("\n").length}]个提交"
384
+ @branch_message = "当前分支[落后]远程分支"
385
+ elsif stdout1.length > 0 && stdout2.length > 0
386
+ @branch_status = GIT_BRANCH_STATUS[:diverged]
387
+ @branch_message = "当前分支与远程分支产生[分叉]"
388
+ end
389
+ elsif has_branch?(branch, true)
390
+ # 有默认远程分支,但尚未追踪
391
+ @branch_status = GIT_BRANCH_STATUS[:no_tracking]
392
+ @branch_message = "未追踪远程分支\"origin/#{branch}\""
393
+ else
394
+ # 无默认远程分支
395
+ @branch_status = GIT_BRANCH_STATUS[:no_remote]
396
+ @branch_message = "对应远程分支不存在"
397
+ end
398
+ end
399
+
400
+ # 解析状态
401
+ #
402
+ # @param list [Array<String>] 状态行数组(通过git status -s输出)
403
+ #
404
+ # @return [GIT_REPO_STATUS,String,GIT_REPO_STATUS_DIRTY_ZONE] 状态;描述信息;脏区域
405
+ #
406
+ def parse_change(list)
407
+ index_message, work_tree_message, conflict_message, special_message = [], [], [], []
408
+ list.each { |line|
409
+ index_status = line[0]
410
+ work_tree_status = line[1]
411
+ combined_status = index_status + work_tree_status
412
+ changed_file = line[3..-1]
413
+
414
+ change_message = convert_file_status(STATUS_TYPE[:conflicts], combined_status)
415
+ if !change_message.nil?
416
+ conflict_message.push(change_message + changed_file)
417
+ end
418
+
419
+ change_message = convert_file_status(STATUS_TYPE[:special], combined_status)
420
+ if !change_message.nil?
421
+ special_message.push(change_message + changed_file)
422
+ end
423
+
424
+ change_message = convert_file_status(STATUS_TYPE[:normal], index_status)
425
+ if !change_message.nil?
426
+ index_message.push(change_message + changed_file)
427
+ end
428
+
429
+ change_message = convert_file_status(STATUS_TYPE[:normal], work_tree_status)
430
+ if !change_message.nil?
431
+ work_tree_message.push(change_message + changed_file)
432
+ end
433
+ }
434
+
435
+ output = []
436
+ dirty_zone = 0
437
+ if index_message.length > 0
438
+ output.push(['暂存区', index_message])
439
+ dirty_zone |= GIT_REPO_STATUS_DIRTY_ZONE[:index]
440
+ end
441
+
442
+ if work_tree_message.length > 0
443
+ output.push(['工作区', work_tree_message])
444
+ dirty_zone |= GIT_REPO_STATUS_DIRTY_ZONE[:work_tree]
445
+ end
446
+
447
+ if conflict_message.length > 0
448
+ output.push(['冲突[我方|对方]', conflict_message])
449
+ dirty_zone |= GIT_REPO_STATUS_DIRTY_ZONE[:work_tree]
450
+ end
451
+
452
+ if special_message.length > 0
453
+ output.push(['特殊', special_message])
454
+ dirty_zone |= GIT_REPO_STATUS_DIRTY_ZONE[:special]
455
+ end
456
+
457
+ status = GIT_REPO_STATUS[:dirty]
458
+ return status, output, dirty_zone
459
+ end
460
+
461
+ # 转化文件状态
462
+ #
463
+ # @param type [STATUS_TYPE] 状态类型
464
+ #
465
+ # @param status [String] 文件状态
466
+ #
467
+ # @return [FILE_STATUS_MESSAGE] 文件状态描述
468
+ #
469
+ def convert_file_status(type, status)
470
+ if type == STATUS_TYPE[:normal]
471
+ return FILE_STATUS_MESSAGE[status.to_s] if FILE_STATUS.values.include?(status)
472
+ elsif type == STATUS_TYPE[:conflicts]
473
+ return FILE_STATUS_MESSAGE[status.to_s] if FILE_STATUS_CONFLICT.values.include?(status)
474
+ elsif type == STATUS_TYPE[:special]
475
+ return FILE_STATUS_MESSAGE[status.to_s] if FILE_STATUS_SPECIAL.values.include?(status)
476
+ end
477
+ return nil
478
+ end
479
+ end
480
+ end
481
+ end