m-git 2.5.4

Sign up to get free protection for your applications and to get access to all the features.
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