hiiro 0.1.50 → 0.1.51
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 +4 -4
- data/bin/h-app +50 -12
- data/bin/h-branch +4 -5
- data/lib/hiiro/git/branch.rb +65 -0
- data/lib/hiiro/git/branches.rb +79 -0
- data/lib/hiiro/git/pr.rb +107 -0
- data/lib/hiiro/git/remote.rb +77 -0
- data/lib/hiiro/git/worktree.rb +60 -0
- data/lib/hiiro/git/worktrees.rb +93 -0
- data/lib/hiiro/git.rb +197 -0
- data/lib/hiiro/history.rb +6 -8
- data/lib/hiiro/version.rb +1 -1
- data/lib/hiiro.rb +5 -0
- data/plugins/tasks.rb +11 -27
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6181ad8d712bc544591aaa46f8c5c555b33f76cf6ed572cb70369f1f56b91bd9
|
|
4
|
+
data.tar.gz: 1a7490eb2e795ffab86b7b4ad1f24fe46fc6d0ba508e76bc074a88470b1eb437
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2ad79ec0a1e709d3cc7ce65e3142c23612b34f4b63e15d2811671cdca96858744f9dc57d2541b297df6e3abddaf61714ffe06f9491d694622ce129e65a3d5841
|
|
7
|
+
data.tar.gz: 7e9e81804965cb3d58a7baa8a2d2ed1597ff474ec0cf56998a86328fe91014286ff7604d7faf6940b6e09e91453afd357285f2be3d2160fb22455dea65275ec6
|
data/bin/h-app
CHANGED
|
@@ -11,11 +11,6 @@ def load_apps
|
|
|
11
11
|
YAML.safe_load_file(APPS_FILE) || {}
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def git_root
|
|
15
|
-
root = `git rev-parse --show-toplevel 2>/dev/null`.strip
|
|
16
|
-
root.empty? ? nil : root
|
|
17
|
-
end
|
|
18
|
-
|
|
19
14
|
def task_root
|
|
20
15
|
# Fall back to current task's tree path if not in a git repo
|
|
21
16
|
env = Environment.current rescue nil
|
|
@@ -28,12 +23,6 @@ def task_root
|
|
|
28
23
|
tree&.path
|
|
29
24
|
end
|
|
30
25
|
|
|
31
|
-
def resolve_app_path(app_relative_path)
|
|
32
|
-
root = git_root || task_root
|
|
33
|
-
return nil unless root
|
|
34
|
-
|
|
35
|
-
File.join(root, app_relative_path)
|
|
36
|
-
end
|
|
37
26
|
|
|
38
27
|
def relative_cd_path(target_path)
|
|
39
28
|
pwd = Pathname.new(Dir.pwd)
|
|
@@ -57,7 +46,7 @@ Hiiro.run(*ARGV, plugins: [:Tasks]) {
|
|
|
57
46
|
}
|
|
58
47
|
|
|
59
48
|
add_subcmd(:cd) { |app_name=nil|
|
|
60
|
-
root =
|
|
49
|
+
root = git.root || task_root
|
|
61
50
|
|
|
62
51
|
unless root
|
|
63
52
|
puts "Not in a git repo or task - cannot resolve path"
|
|
@@ -97,4 +86,53 @@ Hiiro.run(*ARGV, plugins: [:Tasks]) {
|
|
|
97
86
|
apps.each { |name, path| puts format(" %-20s => %s", name, path) }
|
|
98
87
|
end
|
|
99
88
|
}
|
|
89
|
+
|
|
90
|
+
add_subcmd(:path) { |app_name=nil|
|
|
91
|
+
root = git.root || task_root
|
|
92
|
+
|
|
93
|
+
unless root
|
|
94
|
+
puts "Not in a git repo or task - cannot resolve path"
|
|
95
|
+
next
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if app_name.nil? || app_name.empty?
|
|
99
|
+
puts relative_cd_path(root)
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
apps = load_apps
|
|
104
|
+
match = apps.find { |name, _| name.start_with?(app_name) }
|
|
105
|
+
|
|
106
|
+
if match
|
|
107
|
+
name, app_relative_path = match
|
|
108
|
+
target = File.join(root, app_relative_path)
|
|
109
|
+
puts relative_cd_path(target)
|
|
110
|
+
else
|
|
111
|
+
puts "App '#{app_name}' not found"
|
|
112
|
+
end
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
add_subcmd(:abspath) { |app_name=nil|
|
|
116
|
+
root = git.root || task_root
|
|
117
|
+
|
|
118
|
+
unless root
|
|
119
|
+
puts "Not in a git repo or task - cannot resolve path"
|
|
120
|
+
next
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if app_name.nil? || app_name.empty?
|
|
124
|
+
puts root
|
|
125
|
+
next
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
apps = load_apps
|
|
129
|
+
match = apps.find { |name, _| name.start_with?(app_name) }
|
|
130
|
+
|
|
131
|
+
if match
|
|
132
|
+
name, app_relative_path = match
|
|
133
|
+
puts File.join(root, app_relative_path)
|
|
134
|
+
else
|
|
135
|
+
puts "App '#{app_name}' not found"
|
|
136
|
+
end
|
|
137
|
+
}
|
|
100
138
|
}
|
data/bin/h-branch
CHANGED
|
@@ -107,8 +107,7 @@ class BranchManager
|
|
|
107
107
|
private
|
|
108
108
|
|
|
109
109
|
def current_branch
|
|
110
|
-
branch
|
|
111
|
-
branch.empty? ? nil : branch
|
|
110
|
+
hiiro.git.branch
|
|
112
111
|
end
|
|
113
112
|
|
|
114
113
|
def build_entry(branch_name)
|
|
@@ -208,10 +207,10 @@ hiiro.add_subcmd(:save) { manager.save }
|
|
|
208
207
|
hiiro.add_subcmd(:history) { |*args| manager.history(args) }
|
|
209
208
|
hiiro.add_subcmd(:current) { manager.current }
|
|
210
209
|
hiiro.add_subcmd(:select) do |*args|
|
|
211
|
-
branches =
|
|
210
|
+
branches = hiiro.git.branches(sort_by: 'authordate', ignore_case: true)
|
|
212
211
|
|
|
213
|
-
lines = branches.each_with_object({}) do |
|
|
214
|
-
h[
|
|
212
|
+
lines = branches.each_with_object({}) do |name, h|
|
|
213
|
+
h[" #{name}"] = name
|
|
215
214
|
end
|
|
216
215
|
|
|
217
216
|
if args.any?
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
class Hiiro
|
|
2
|
+
class Git
|
|
3
|
+
class Branch
|
|
4
|
+
attr_reader :name, :ref, :upstream, :head
|
|
5
|
+
|
|
6
|
+
def self.from_format_line(line, format: :short)
|
|
7
|
+
# Parses output from git branch --format
|
|
8
|
+
new(name: line.strip)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.current
|
|
12
|
+
name = `git branch --show-current 2>/dev/null`.strip
|
|
13
|
+
return nil if name.empty?
|
|
14
|
+
new(name: name, current: true)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(name:, ref: nil, upstream: nil, head: nil, current: false)
|
|
18
|
+
@name = name
|
|
19
|
+
@ref = ref || "refs/heads/#{name}"
|
|
20
|
+
@upstream = upstream
|
|
21
|
+
@head = head
|
|
22
|
+
@current = current
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def current?
|
|
26
|
+
@current
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def local?
|
|
30
|
+
ref.start_with?('refs/heads/')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def remote?
|
|
34
|
+
ref.start_with?('refs/remotes/')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def checkout
|
|
38
|
+
system('git', 'checkout', name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def delete(force: false)
|
|
42
|
+
flag = force ? '-D' : '-d'
|
|
43
|
+
system('git', 'branch', flag, name)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def exists?
|
|
47
|
+
system('git', 'show-ref', '--verify', '--quiet', ref)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_s
|
|
51
|
+
name
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_h
|
|
55
|
+
{
|
|
56
|
+
name: name,
|
|
57
|
+
ref: ref,
|
|
58
|
+
upstream: upstream,
|
|
59
|
+
head: head,
|
|
60
|
+
current: current?,
|
|
61
|
+
}.compact
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
class Hiiro
|
|
2
|
+
class Git
|
|
3
|
+
class Branches
|
|
4
|
+
include Enumerable
|
|
5
|
+
|
|
6
|
+
def self.fetch(sort_by: nil, ignore_case: false, remote: false)
|
|
7
|
+
args = ['git', 'branch', '--format=%(refname:short)']
|
|
8
|
+
args << '-i' if ignore_case
|
|
9
|
+
args << '-r' if remote
|
|
10
|
+
args << "-a" if remote == :all
|
|
11
|
+
args << "--sort=#{sort_by}" if sort_by
|
|
12
|
+
|
|
13
|
+
output = `#{args.shelljoin} 2>/dev/null`
|
|
14
|
+
from_names(output.split("\n").map(&:strip))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.from_names(names)
|
|
18
|
+
branches = names.map { |name| Branch.new(name: name) }
|
|
19
|
+
new(branches)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.local(sort_by: nil, ignore_case: false)
|
|
23
|
+
fetch(sort_by: sort_by, ignore_case: ignore_case, remote: false)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.remote(sort_by: nil, ignore_case: false)
|
|
27
|
+
fetch(sort_by: sort_by, ignore_case: ignore_case, remote: true)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.all(sort_by: nil, ignore_case: false)
|
|
31
|
+
fetch(sort_by: sort_by, ignore_case: ignore_case, remote: :all)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
attr_reader :branches
|
|
35
|
+
|
|
36
|
+
def initialize(branches)
|
|
37
|
+
@branches = branches
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def each(&block)
|
|
41
|
+
branches.each(&block)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def find_by_name(name)
|
|
45
|
+
branches.find { |b| b.name == name }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def matching(prefix)
|
|
49
|
+
self.class.new(branches.select { |b| b.name.start_with?(prefix) })
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def containing(substring)
|
|
53
|
+
self.class.new(branches.select { |b| b.name.include?(substring) })
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def names
|
|
57
|
+
branches.map(&:name)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def current
|
|
61
|
+
branches.find(&:current?)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def empty?
|
|
65
|
+
branches.empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def size
|
|
69
|
+
branches.size
|
|
70
|
+
end
|
|
71
|
+
alias count size
|
|
72
|
+
alias length size
|
|
73
|
+
|
|
74
|
+
def to_a
|
|
75
|
+
branches.dup
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/hiiro/git/pr.rb
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
class Hiiro
|
|
2
|
+
class Git
|
|
3
|
+
class Pr
|
|
4
|
+
attr_reader :number, :title, :state, :url, :head_branch, :base_branch
|
|
5
|
+
|
|
6
|
+
def self.current
|
|
7
|
+
output = `gh pr view --json number,title,state,url,headRefName,baseRefName 2>/dev/null`
|
|
8
|
+
return nil if output.empty?
|
|
9
|
+
|
|
10
|
+
require 'json'
|
|
11
|
+
data = JSON.parse(output)
|
|
12
|
+
from_gh_json(data)
|
|
13
|
+
rescue
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.from_gh_json(data)
|
|
18
|
+
new(
|
|
19
|
+
number: data['number'],
|
|
20
|
+
title: data['title'],
|
|
21
|
+
state: data['state'],
|
|
22
|
+
url: data['url'],
|
|
23
|
+
head_branch: data['headRefName'],
|
|
24
|
+
base_branch: data['baseRefName'],
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.list(state: 'open', limit: 30)
|
|
29
|
+
output = `gh pr list --state #{state} --limit #{limit} --json number,title,state,url,headRefName,baseRefName 2>/dev/null`
|
|
30
|
+
return [] if output.empty?
|
|
31
|
+
|
|
32
|
+
require 'json'
|
|
33
|
+
JSON.parse(output).map { |data| from_gh_json(data) }
|
|
34
|
+
rescue
|
|
35
|
+
[]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.create(title:, body: nil, base: nil, draft: false)
|
|
39
|
+
args = ['gh', 'pr', 'create', '--title', title]
|
|
40
|
+
args += ['--body', body] if body
|
|
41
|
+
args += ['--base', base] if base
|
|
42
|
+
args << '--draft' if draft
|
|
43
|
+
|
|
44
|
+
system(*args)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize(number:, title: nil, state: nil, url: nil, head_branch: nil, base_branch: nil)
|
|
48
|
+
@number = number
|
|
49
|
+
@title = title
|
|
50
|
+
@state = state
|
|
51
|
+
@url = url
|
|
52
|
+
@head_branch = head_branch
|
|
53
|
+
@base_branch = base_branch
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def open?
|
|
57
|
+
state&.downcase == 'open'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def closed?
|
|
61
|
+
state&.downcase == 'closed'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def merged?
|
|
65
|
+
state&.downcase == 'merged'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def view
|
|
69
|
+
system('gh', 'pr', 'view', number.to_s)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def checkout
|
|
73
|
+
system('gh', 'pr', 'checkout', number.to_s)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def merge(method: nil, delete_branch: true)
|
|
77
|
+
args = ['gh', 'pr', 'merge', number.to_s]
|
|
78
|
+
args << "--#{method}" if method # squash, merge, rebase
|
|
79
|
+
args << '--delete-branch' if delete_branch
|
|
80
|
+
system(*args)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def close
|
|
84
|
+
system('gh', 'pr', 'close', number.to_s)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def reopen
|
|
88
|
+
system('gh', 'pr', 'reopen', number.to_s)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def to_s
|
|
92
|
+
"##{number}: #{title}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def to_h
|
|
96
|
+
{
|
|
97
|
+
number: number,
|
|
98
|
+
title: title,
|
|
99
|
+
state: state,
|
|
100
|
+
url: url,
|
|
101
|
+
head_branch: head_branch,
|
|
102
|
+
base_branch: base_branch,
|
|
103
|
+
}.compact
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
class Hiiro
|
|
2
|
+
class Git
|
|
3
|
+
class Remote
|
|
4
|
+
attr_reader :name, :fetch_url, :push_url
|
|
5
|
+
|
|
6
|
+
def self.all
|
|
7
|
+
output = `git remote 2>/dev/null`
|
|
8
|
+
output.split("\n").map { |name| new(name: name.strip) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.origin
|
|
12
|
+
new(name: 'origin')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.from_verbose_line(line)
|
|
16
|
+
# Parses: origin git@github.com:user/repo.git (fetch)
|
|
17
|
+
match = line.match(/^(\S+)\s+(\S+)\s+\((fetch|push)\)/)
|
|
18
|
+
return nil unless match
|
|
19
|
+
|
|
20
|
+
name, url, type = match.captures
|
|
21
|
+
attrs = { name: name }
|
|
22
|
+
attrs[type == 'fetch' ? :fetch_url : :push_url] = url
|
|
23
|
+
new(**attrs)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(name:, fetch_url: nil, push_url: nil)
|
|
27
|
+
@name = name
|
|
28
|
+
@fetch_url = fetch_url
|
|
29
|
+
@push_url = push_url
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def url
|
|
33
|
+
fetch_url || push_url || fetch_remote_url
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def push(branch = nil, force: false)
|
|
37
|
+
args = ['git', 'push', name]
|
|
38
|
+
args << '-f' if force
|
|
39
|
+
args << branch if branch
|
|
40
|
+
system(*args)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def pull(branch = nil)
|
|
44
|
+
args = ['git', 'pull', name]
|
|
45
|
+
args << branch if branch
|
|
46
|
+
system(*args)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fetch_remote
|
|
50
|
+
system('git', 'fetch', name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def exists?
|
|
54
|
+
system('git', 'remote', 'get-url', name, out: File::NULL, err: File::NULL)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_s
|
|
58
|
+
name
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_h
|
|
62
|
+
{
|
|
63
|
+
name: name,
|
|
64
|
+
fetch_url: fetch_url,
|
|
65
|
+
push_url: push_url,
|
|
66
|
+
}.compact
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def fetch_remote_url
|
|
72
|
+
url = `git remote get-url #{name.shellescape} 2>/dev/null`.strip
|
|
73
|
+
url.empty? ? nil : url
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
class Hiiro
|
|
2
|
+
class Git
|
|
3
|
+
class Worktree
|
|
4
|
+
attr_reader :path, :head, :branch
|
|
5
|
+
|
|
6
|
+
def self.from_porcelain_block(lines)
|
|
7
|
+
attrs = {}
|
|
8
|
+
lines.each do |line|
|
|
9
|
+
case line
|
|
10
|
+
when /^worktree (.+)/
|
|
11
|
+
attrs[:path] = $1
|
|
12
|
+
when /^HEAD (.+)/
|
|
13
|
+
attrs[:head] = $1
|
|
14
|
+
when /^branch refs\/heads\/(.+)/
|
|
15
|
+
attrs[:branch] = $1
|
|
16
|
+
when 'detached'
|
|
17
|
+
attrs[:detached] = true
|
|
18
|
+
when 'bare'
|
|
19
|
+
attrs[:bare] = true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
new(**attrs) if attrs[:path]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(path:, head: nil, branch: nil, detached: false, bare: false)
|
|
26
|
+
@path = path
|
|
27
|
+
@head = head
|
|
28
|
+
@branch = branch
|
|
29
|
+
@detached = detached
|
|
30
|
+
@bare = bare
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def detached?
|
|
34
|
+
@detached
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def bare?
|
|
38
|
+
@bare
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def name
|
|
42
|
+
File.basename(path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def match?(pwd)
|
|
46
|
+
pwd == path || pwd.start_with?(path + '/')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_h
|
|
50
|
+
{
|
|
51
|
+
path: path,
|
|
52
|
+
head: head,
|
|
53
|
+
branch: branch,
|
|
54
|
+
detached: detached?,
|
|
55
|
+
bare: bare?,
|
|
56
|
+
}.compact
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
class Hiiro
|
|
2
|
+
class Git
|
|
3
|
+
class Worktrees
|
|
4
|
+
include Enumerable
|
|
5
|
+
|
|
6
|
+
def self.from_porcelain(output)
|
|
7
|
+
return new([]) if output.nil? || output.empty?
|
|
8
|
+
|
|
9
|
+
blocks = []
|
|
10
|
+
current_block = []
|
|
11
|
+
|
|
12
|
+
output.each_line do |line|
|
|
13
|
+
line = line.chomp
|
|
14
|
+
if line.empty?
|
|
15
|
+
blocks << current_block unless current_block.empty?
|
|
16
|
+
current_block = []
|
|
17
|
+
else
|
|
18
|
+
current_block << line
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
blocks << current_block unless current_block.empty?
|
|
22
|
+
|
|
23
|
+
worktrees = blocks.map { |block| Worktree.from_porcelain_block(block) }.compact
|
|
24
|
+
new(worktrees)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.fetch(repo_path: nil)
|
|
28
|
+
output = if repo_path
|
|
29
|
+
`git -C #{repo_path.shellescape} worktree list --porcelain 2>/dev/null`
|
|
30
|
+
else
|
|
31
|
+
`git worktree list --porcelain 2>/dev/null`
|
|
32
|
+
end
|
|
33
|
+
from_porcelain(output)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :worktrees
|
|
37
|
+
|
|
38
|
+
def initialize(worktrees)
|
|
39
|
+
@worktrees = worktrees
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def each(&block)
|
|
43
|
+
worktrees.each(&block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def find_by_path(path)
|
|
47
|
+
worktrees.find { |wt| wt.path == path }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def find_by_name(name)
|
|
51
|
+
worktrees.find { |wt| wt.name == name }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def find_by_branch(branch)
|
|
55
|
+
worktrees.find { |wt| wt.branch == branch }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def matching(pwd)
|
|
59
|
+
worktrees.find { |wt| wt.match?(pwd) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def without_bare
|
|
63
|
+
self.class.new(worktrees.reject(&:bare?))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def detached
|
|
67
|
+
self.class.new(worktrees.select(&:detached?))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def with_branch
|
|
71
|
+
self.class.new(worktrees.reject(&:detached?))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def names
|
|
75
|
+
worktrees.map(&:name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def paths
|
|
79
|
+
worktrees.map(&:path)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def empty?
|
|
83
|
+
worktrees.empty?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def size
|
|
87
|
+
worktrees.size
|
|
88
|
+
end
|
|
89
|
+
alias count size
|
|
90
|
+
alias length size
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/hiiro/git.rb
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
require_relative 'git/worktree'
|
|
2
|
+
require_relative 'git/worktrees'
|
|
3
|
+
require_relative 'git/branch'
|
|
4
|
+
require_relative 'git/branches'
|
|
5
|
+
require_relative 'git/remote'
|
|
6
|
+
require_relative 'git/pr'
|
|
7
|
+
|
|
8
|
+
class Hiiro
|
|
9
|
+
class Git
|
|
10
|
+
attr_reader :hiiro, :pwd
|
|
11
|
+
|
|
12
|
+
def initialize(hiiro, pwd)
|
|
13
|
+
@hiiro = hiiro
|
|
14
|
+
@pwd = pwd
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Query methods
|
|
18
|
+
|
|
19
|
+
def in_repo?
|
|
20
|
+
!root.nil?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def root
|
|
24
|
+
@root ||= run_safe('rev-parse', '--show-toplevel')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def detached?
|
|
28
|
+
current_branch_name == 'HEAD'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def commit(ref = 'HEAD', short: false)
|
|
32
|
+
args = short ? ['rev-parse', '--short', ref] : ['rev-parse', ref]
|
|
33
|
+
run_safe(*args)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Branch convenience methods
|
|
37
|
+
|
|
38
|
+
def branch
|
|
39
|
+
current_branch_name
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def branch_current
|
|
43
|
+
run_safe('branch', '--show-current')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def current_branch
|
|
47
|
+
Branch.current
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def current_branch_name
|
|
51
|
+
run_safe('rev-parse', '--abbrev-ref', 'HEAD')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def branches(sort_by: nil, ignore_case: false)
|
|
55
|
+
Branches.fetch(sort_by: sort_by, ignore_case: ignore_case).names
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def branches_collection(sort_by: nil, ignore_case: false)
|
|
59
|
+
Branches.fetch(sort_by: sort_by, ignore_case: ignore_case)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def branch_exists?(name)
|
|
63
|
+
run_success?('show-ref', '--verify', '--quiet', "refs/heads/#{name}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def create_branch(name, start_point = nil)
|
|
67
|
+
args = ['checkout', '-b', name]
|
|
68
|
+
args << start_point if start_point
|
|
69
|
+
run_system(*args)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def checkout(ref)
|
|
73
|
+
run_system('checkout', ref)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def delete_branch(name, force: false)
|
|
77
|
+
flag = force ? '-D' : '-d'
|
|
78
|
+
run_system('branch', flag, name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Worktree convenience methods
|
|
82
|
+
|
|
83
|
+
def worktrees(repo_path: nil)
|
|
84
|
+
worktrees_collection(repo_path: repo_path).to_a
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def worktrees_collection(repo_path: nil)
|
|
88
|
+
Worktrees.fetch(repo_path: repo_path)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def worktrees_porcelain(repo_path: nil)
|
|
92
|
+
if repo_path
|
|
93
|
+
run_safe('-C', repo_path, 'worktree', 'list', '--porcelain')
|
|
94
|
+
else
|
|
95
|
+
run_safe('worktree', 'list', '--porcelain')
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def add_worktree(path, branch: nil, detach: false)
|
|
100
|
+
args = ['worktree', 'add']
|
|
101
|
+
args << '--detach' if detach
|
|
102
|
+
args << path
|
|
103
|
+
args << branch if branch && !detach
|
|
104
|
+
run_system(*args)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def add_worktree_detached(path, repo_path: nil)
|
|
108
|
+
if repo_path
|
|
109
|
+
run_system('-C', repo_path, 'worktree', 'add', '--detach', path)
|
|
110
|
+
else
|
|
111
|
+
run_system('worktree', 'add', '--detach', path)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def move_worktree(from, to, repo_path: nil)
|
|
116
|
+
if repo_path
|
|
117
|
+
run_system('-C', repo_path, 'worktree', 'move', from, to)
|
|
118
|
+
else
|
|
119
|
+
run_system('worktree', 'move', from, to)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def remove_worktree(path, force: false)
|
|
124
|
+
args = ['worktree', 'remove']
|
|
125
|
+
args << '--force' if force
|
|
126
|
+
args << path
|
|
127
|
+
run_system(*args)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def lock_worktree(path)
|
|
131
|
+
run_system('worktree', 'lock', path)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def unlock_worktree(path)
|
|
135
|
+
run_system('worktree', 'unlock', path)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def prune_worktrees
|
|
139
|
+
run_system('worktree', 'prune')
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def repair_worktrees
|
|
143
|
+
run_system('worktree', 'repair')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Remote convenience methods
|
|
147
|
+
|
|
148
|
+
def remotes
|
|
149
|
+
Remote.all
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def origin
|
|
153
|
+
Remote.origin
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def push(remote: 'origin', branch: nil, force: false)
|
|
157
|
+
Remote.new(name: remote).push(branch, force: force)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def pull(remote: 'origin', branch: nil)
|
|
161
|
+
Remote.new(name: remote).pull(branch)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def fetch_remote(remote: 'origin')
|
|
165
|
+
Remote.new(name: remote).fetch_remote
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# PR convenience methods
|
|
169
|
+
|
|
170
|
+
def current_pr
|
|
171
|
+
Pr.current
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def list_prs(state: 'open', limit: 30)
|
|
175
|
+
Pr.list(state: state, limit: limit)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def create_pr(title:, body: nil, base: nil, draft: false)
|
|
179
|
+
Pr.create(title: title, body: body, base: base, draft: draft)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def run_safe(*args)
|
|
185
|
+
result = `git #{args.shelljoin} 2>/dev/null`.strip
|
|
186
|
+
result.empty? ? nil : result
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def run_system(*args)
|
|
190
|
+
system('git', *args)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def run_success?(*args)
|
|
194
|
+
system('git', *args, out: File::NULL, err: File::NULL)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
data/lib/hiiro/history.rb
CHANGED
|
@@ -259,17 +259,15 @@ class Hiiro
|
|
|
259
259
|
end
|
|
260
260
|
|
|
261
261
|
def current_git_branch
|
|
262
|
-
|
|
263
|
-
branch.empty? ? nil : branch
|
|
264
|
-
rescue
|
|
265
|
-
nil
|
|
262
|
+
git_helper.branch_current
|
|
266
263
|
end
|
|
267
264
|
|
|
268
265
|
def current_git_worktree
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
266
|
+
git_helper.root
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def git_helper
|
|
270
|
+
@git_helper ||= Git.new(nil, Dir.pwd)
|
|
273
271
|
end
|
|
274
272
|
end
|
|
275
273
|
end
|
data/lib/hiiro/version.rb
CHANGED
data/lib/hiiro.rb
CHANGED
|
@@ -3,6 +3,7 @@ require "yaml"
|
|
|
3
3
|
require "shellwords"
|
|
4
4
|
|
|
5
5
|
require_relative "hiiro/version"
|
|
6
|
+
require_relative "hiiro/git"
|
|
6
7
|
require_relative "hiiro/history"
|
|
7
8
|
require_relative "hiiro/options"
|
|
8
9
|
require_relative "hiiro/sk"
|
|
@@ -148,6 +149,10 @@ class Hiiro
|
|
|
148
149
|
|
|
149
150
|
def pins = @pins ||= Pin.new(self)
|
|
150
151
|
|
|
152
|
+
def git
|
|
153
|
+
@git ||= Git.new(self, Dir.pwd)
|
|
154
|
+
end
|
|
155
|
+
|
|
151
156
|
def load_plugins(*plugins)
|
|
152
157
|
plugins.flatten.each { |plugin| load_plugin(plugin) }
|
|
153
158
|
end
|
data/plugins/tasks.rb
CHANGED
|
@@ -36,27 +36,11 @@ class Tree
|
|
|
36
36
|
attr_reader :path, :head, :branch
|
|
37
37
|
|
|
38
38
|
def self.all(repo_path: REPO_PATH)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
output.lines(chomp: true).each do |line|
|
|
45
|
-
case line
|
|
46
|
-
when /^worktree (.*)/
|
|
47
|
-
trees << new(**current) if current
|
|
48
|
-
current = { path: $1 }
|
|
49
|
-
when /^HEAD (.*)/
|
|
50
|
-
current[:head] = $1 if current
|
|
51
|
-
when /^branch refs\/heads\/(.*)/
|
|
52
|
-
current[:branch] = $1 if current
|
|
53
|
-
when 'bare'
|
|
54
|
-
current = nil
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
trees << new(**current) if current
|
|
59
|
-
trees
|
|
39
|
+
git = Hiiro::Git.new(nil, repo_path)
|
|
40
|
+
git.worktrees(repo_path: repo_path).map do |wt|
|
|
41
|
+
next if wt.bare?
|
|
42
|
+
new(path: wt.path, head: wt.head, branch: wt.branch)
|
|
43
|
+
end.compact
|
|
60
44
|
end
|
|
61
45
|
|
|
62
46
|
def initialize(path:, head: nil, branch: nil)
|
|
@@ -401,18 +385,19 @@ class TaskManager
|
|
|
401
385
|
|
|
402
386
|
target_path = File.join(WORK_DIR, subtree_name)
|
|
403
387
|
|
|
388
|
+
git = Hiiro::Git.new(nil, REPO_PATH)
|
|
404
389
|
available = find_available_tree
|
|
405
390
|
if available
|
|
406
391
|
puts "Renaming worktree '#{available.name}' to '#{subtree_name}'..."
|
|
407
392
|
FileUtils.mkdir_p(File.dirname(target_path))
|
|
408
|
-
unless
|
|
393
|
+
unless git.move_worktree(available.path, target_path, repo_path: REPO_PATH)
|
|
409
394
|
puts "ERROR: Failed to rename worktree"
|
|
410
395
|
return
|
|
411
396
|
end
|
|
412
397
|
else
|
|
413
398
|
puts "Creating new worktree '#{subtree_name}'..."
|
|
414
399
|
FileUtils.mkdir_p(File.dirname(target_path))
|
|
415
|
-
unless
|
|
400
|
+
unless git.add_worktree_detached(target_path, repo_path: REPO_PATH)
|
|
416
401
|
puts "ERROR: Failed to create worktree"
|
|
417
402
|
return
|
|
418
403
|
end
|
|
@@ -507,7 +492,7 @@ class TaskManager
|
|
|
507
492
|
display_name = scope == :subtask ? task.short_name : task.name
|
|
508
493
|
puts format("%s %-25s tree: %-20s%s", marker, display_name, task.tree_name || '(none)', branch_str)
|
|
509
494
|
|
|
510
|
-
# Show
|
|
495
|
+
# Show subtasks under parent
|
|
511
496
|
if scope == :task
|
|
512
497
|
subs = subtasks(task)
|
|
513
498
|
subs.each do |st|
|
|
@@ -515,8 +500,7 @@ class TaskManager
|
|
|
515
500
|
sub_tree = environment.find_tree(st.tree_name)
|
|
516
501
|
sub_branch = sub_tree&.branch || (sub_tree&.detached? ? '(detached)' : nil)
|
|
517
502
|
sub_branch_str = sub_branch ? " [#{sub_branch}]" : ""
|
|
518
|
-
|
|
519
|
-
puts format("%s %s/%-*s tree: %-20s%s", sub_marker, padding, 25 - task.name.length - 1, st.short_name, st.tree_name || '(none)', sub_branch_str)
|
|
503
|
+
puts format("%s - %-23s tree: %-20s%s", sub_marker, st.short_name, st.tree_name || '(none)', sub_branch_str)
|
|
520
504
|
end
|
|
521
505
|
end
|
|
522
506
|
end
|
|
@@ -627,7 +611,7 @@ class TaskManager
|
|
|
627
611
|
tree = environment.find_tree(task.tree_name)
|
|
628
612
|
tree&.path || File.join(WORK_DIR, task.tree_name)
|
|
629
613
|
else
|
|
630
|
-
|
|
614
|
+
Hiiro::Git.new(nil, Dir.pwd).root
|
|
631
615
|
end
|
|
632
616
|
|
|
633
617
|
if app_name.nil?
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hiiro
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.51
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua Toyota
|
|
@@ -69,6 +69,13 @@ files:
|
|
|
69
69
|
- exe/h
|
|
70
70
|
- hiiro.gemspec
|
|
71
71
|
- lib/hiiro.rb
|
|
72
|
+
- lib/hiiro/git.rb
|
|
73
|
+
- lib/hiiro/git/branch.rb
|
|
74
|
+
- lib/hiiro/git/branches.rb
|
|
75
|
+
- lib/hiiro/git/pr.rb
|
|
76
|
+
- lib/hiiro/git/remote.rb
|
|
77
|
+
- lib/hiiro/git/worktree.rb
|
|
78
|
+
- lib/hiiro/git/worktrees.rb
|
|
72
79
|
- lib/hiiro/history.rb
|
|
73
80
|
- lib/hiiro/options.rb
|
|
74
81
|
- lib/hiiro/sk.rb
|