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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +85 -0
- data/lib/m-git.rb +66 -0
- data/lib/m-git/argv.rb +170 -0
- data/lib/m-git/argv/opt.rb +38 -0
- data/lib/m-git/argv/opt_list.rb +71 -0
- data/lib/m-git/argv/parser.rb +66 -0
- data/lib/m-git/base_command.rb +271 -0
- data/lib/m-git/command/add.rb +41 -0
- data/lib/m-git/command/branch.rb +90 -0
- data/lib/m-git/command/checkout.rb +106 -0
- data/lib/m-git/command/clean.rb +64 -0
- data/lib/m-git/command/commit.rb +84 -0
- data/lib/m-git/command/config.rb +202 -0
- data/lib/m-git/command/delete.rb +99 -0
- data/lib/m-git/command/fetch.rb +32 -0
- data/lib/m-git/command/forall.rb +81 -0
- data/lib/m-git/command/info.rb +74 -0
- data/lib/m-git/command/init.rb +324 -0
- data/lib/m-git/command/log.rb +73 -0
- data/lib/m-git/command/merge.rb +381 -0
- data/lib/m-git/command/pull.rb +364 -0
- data/lib/m-git/command/push.rb +311 -0
- data/lib/m-git/command/rebase.rb +348 -0
- data/lib/m-git/command/reset.rb +31 -0
- data/lib/m-git/command/self.rb +223 -0
- data/lib/m-git/command/stash.rb +189 -0
- data/lib/m-git/command/status.rb +135 -0
- data/lib/m-git/command/sync.rb +327 -0
- data/lib/m-git/command/tag.rb +67 -0
- data/lib/m-git/command_manager.rb +24 -0
- data/lib/m-git/error.rb +20 -0
- data/lib/m-git/foundation.rb +25 -0
- data/lib/m-git/foundation/constants.rb +107 -0
- data/lib/m-git/foundation/dir.rb +25 -0
- data/lib/m-git/foundation/duration_recorder.rb +92 -0
- data/lib/m-git/foundation/git_message_parser.rb +50 -0
- data/lib/m-git/foundation/lock.rb +32 -0
- data/lib/m-git/foundation/loger.rb +129 -0
- data/lib/m-git/foundation/mgit_config.rb +222 -0
- data/lib/m-git/foundation/operation_progress_manager.rb +139 -0
- data/lib/m-git/foundation/timer.rb +74 -0
- data/lib/m-git/foundation/utils.rb +361 -0
- data/lib/m-git/hooks_manager.rb +96 -0
- data/lib/m-git/manifest.rb +181 -0
- data/lib/m-git/manifest/cache_manager.rb +44 -0
- data/lib/m-git/manifest/internal.rb +182 -0
- data/lib/m-git/manifest/light_repo.rb +108 -0
- data/lib/m-git/manifest/light_repo_generator.rb +87 -0
- data/lib/m-git/manifest/linter.rb +153 -0
- data/lib/m-git/open_api.rb +427 -0
- data/lib/m-git/open_api/script_download_info.rb +37 -0
- data/lib/m-git/output/output.rb +461 -0
- data/lib/m-git/plugin_manager.rb +112 -0
- data/lib/m-git/repo.rb +133 -0
- data/lib/m-git/repo/status.rb +481 -0
- data/lib/m-git/repo/sync_helper.rb +254 -0
- data/lib/m-git/template.rb +9 -0
- data/lib/m-git/template/local_manifest.rb +27 -0
- data/lib/m-git/template/manifest_hook.rb +28 -0
- data/lib/m-git/template/post_download_hook.rb +29 -0
- data/lib/m-git/template/post_hook.rb +31 -0
- data/lib/m-git/template/pre_exec_hook.rb +31 -0
- data/lib/m-git/template/pre_hook.rb +29 -0
- data/lib/m-git/template/pre_push_hook.rb +32 -0
- data/lib/m-git/version.rb +6 -0
- data/lib/m-git/workspace.rb +648 -0
- data/lib/m-git/workspace/path_helper.rb +56 -0
- data/lib/m-git/workspace/workspace_helper.rb +159 -0
- data/m-git +1 -0
- data/mgit +19 -0
- 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
|