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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bec62fbab5b37a26be0c5ebb4dcd22ab074a34742591639ff307ccd5fced02e8
4
- data.tar.gz: 8581b7676239c1a528a6d47defe0389b6e4494624e25e7c3978a2c5bc8e92d39
3
+ metadata.gz: 6181ad8d712bc544591aaa46f8c5c555b33f76cf6ed572cb70369f1f56b91bd9
4
+ data.tar.gz: 1a7490eb2e795ffab86b7b4ad1f24fe46fc6d0ba508e76bc074a88470b1eb437
5
5
  SHA512:
6
- metadata.gz: 0143e20a9ed37176b309ee43ad2ce9bf2ad64a0f266fa8628f9321581b6c3cd31f250025bacefd941b79e48c9b2da6c931be7895e298ef6a2260539b28d2f07f
7
- data.tar.gz: 2a37a1b8cc2991154a334aa54b60bb3243f5d3f4776936eb2b6c3b3f8b7166f00e6aafa1374d19faa7fbef535f85d6a1a658be4ff26aad42590ca7d8a4f60ba1
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 = git_root || task_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 = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
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 = `git branch -i --sort=authordate`.lines(chomp: true)
210
+ branches = hiiro.git.branches(sort_by: 'authordate', ignore_case: true)
212
211
 
213
- lines = branches.each_with_object({}) do |ln,h|
214
- h[ln] = ln.sub(/../, '')
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
@@ -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
- branch = `git branch --show-current 2>/dev/null`.strip
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
- worktree = `git rev-parse --show-toplevel 2>/dev/null`.strip
270
- worktree.empty? ? nil : worktree
271
- rescue
272
- nil
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
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.50"
2
+ VERSION = "0.1.51"
3
3
  end
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
- output = `git -C #{repo_path} worktree list --porcelain 2>/dev/null`
40
-
41
- trees = []
42
- current = nil
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 system('git', '-C', REPO_PATH, 'worktree', 'move', available.path, target_path)
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 system('git', '-C', REPO_PATH, 'worktree', 'add', '--detach', target_path)
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 subtask count for top-level tasks
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
- padding = " " * task.name.length
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
- `git rev-parse --show-toplevel`.strip
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.50
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