mui-git 0.1.0
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/CHANGELOG.md +33 -0
- data/LICENSE.txt +21 -0
- data/README.md +95 -0
- data/Rakefile +8 -0
- data/lib/mui/git/buffers/base.rb +36 -0
- data/lib/mui/git/buffers/blame_buffer.rb +46 -0
- data/lib/mui/git/buffers/commit_buffer.rb +99 -0
- data/lib/mui/git/buffers/commit_show_buffer.rb +35 -0
- data/lib/mui/git/buffers/diff_buffer.rb +42 -0
- data/lib/mui/git/buffers/log_buffer.rb +39 -0
- data/lib/mui/git/buffers/status_buffer.rb +128 -0
- data/lib/mui/git/command_runner.rb +116 -0
- data/lib/mui/git/highlighters/diff_highlighter.rb +45 -0
- data/lib/mui/git/plugin.rb +333 -0
- data/lib/mui/git/version.rb +7 -0
- data/lib/mui/git.rb +23 -0
- data/lib/mui_git.rb +3 -0
- data/sig/mui/git.rbs +6 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f65a509ed569794ac6dbf4870c84e64683fb9a298f6d775b6fdd0bf18d7426e5
|
|
4
|
+
data.tar.gz: 70f1650ec729b6d33e09e9631f6dd9e80ad639e2139c8d5035a67934b30cff6d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 34f184cb80f5b402926b9627b6823ec8c1c3e1a941055c4c6640f55f1a95b555ebbd06f9d7efeffe931c12c579f91fea152e4fb6aa26c96ba8b87441022cb785
|
|
7
|
+
data.tar.gz: 6e73af2750dc349d17d5daa7516e8ed28e493381cc53d246bcceea1badf7a9cc63be030fde6426b168b8a8500a222209efb6f821c732e2014d69e9718047e0f5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- Initial release
|
|
5
|
+
- Interactive git status buffer (`:Git` or `:Git status`)
|
|
6
|
+
- Diff buffer with syntax highlighting (`:Git diff`)
|
|
7
|
+
- Git log viewing (`:Git log`)
|
|
8
|
+
- Git blame viewing (`:Git blame`)
|
|
9
|
+
- Stage files (`:Git add` or `s` key)
|
|
10
|
+
- Unstage files (`u` key)
|
|
11
|
+
- Toggle stage/unstage (`-` key)
|
|
12
|
+
- Vertical diff split (`dv` keys)
|
|
13
|
+
- Close buffer (`q` key)
|
|
14
|
+
- Refresh status (`R` key)
|
|
15
|
+
- Diff highlighting:
|
|
16
|
+
- Added lines in green
|
|
17
|
+
- Deleted lines in red
|
|
18
|
+
- Hunk headers in cyan
|
|
19
|
+
- File headers in yellow (bold)
|
|
20
|
+
- Commit message buffer:
|
|
21
|
+
- Press `cc` in status buffer to open commit message editor
|
|
22
|
+
- Write commit message at top of buffer (lines starting with `#` are ignored)
|
|
23
|
+
- Save with `:w` to execute commit
|
|
24
|
+
- Cancel with `:q`
|
|
25
|
+
- Shows current branch and staged files as reference
|
|
26
|
+
- Status buffer automatically refreshes after successful commit
|
|
27
|
+
- Log/Blame commit navigation:
|
|
28
|
+
- Press `Enter` in log buffer to view commit diff
|
|
29
|
+
- Press `Enter` in blame buffer to view the commit that last modified the current line
|
|
30
|
+
- Commit diff opens in vertical split with syntax highlighting
|
|
31
|
+
- Uncommitted changes (`00000000`) in blame buffer are skipped
|
|
32
|
+
- New buffer classes: LogBuffer, BlameBuffer, CommitShowBuffer
|
|
33
|
+
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 S-H-GAMELINKS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# mui-git
|
|
2
|
+
|
|
3
|
+
A Git integration plugin for [Mui](https://github.com/S-H-GAMELINKS/mui) editor, inspired by vim-fugitive.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your `.muirc`:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
Mui.use "mui-git"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install via RubyGems (when published):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install mui-git
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Interactive git status buffer
|
|
22
|
+
- Diff viewing with syntax highlighting
|
|
23
|
+
- Stage/unstage files with keyboard shortcuts
|
|
24
|
+
- Commit message editor buffer
|
|
25
|
+
- Git log and blame viewing with commit diff navigation
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
| Command | Description |
|
|
30
|
+
|---------|-------------|
|
|
31
|
+
| `:Git` or `:Git status` | Open git status buffer |
|
|
32
|
+
| `:Git diff [file]` | Open diff buffer for file (current file if omitted) |
|
|
33
|
+
| `:Git log [limit]` | Show commit log (default: 20 commits) |
|
|
34
|
+
| `:Git blame [file]` | Show git blame for file |
|
|
35
|
+
| `:Git add [file\|%]` | Stage file (`%` for current file) |
|
|
36
|
+
| `:Git commit <message>` | Create commit with message |
|
|
37
|
+
|
|
38
|
+
## Keymaps (Status Buffer)
|
|
39
|
+
|
|
40
|
+
| Key | Description |
|
|
41
|
+
|-----|-------------|
|
|
42
|
+
| `s` | Stage file under cursor |
|
|
43
|
+
| `u` | Unstage file under cursor |
|
|
44
|
+
| `-` | Toggle stage/unstage |
|
|
45
|
+
| `dv` | Open diff in vertical split |
|
|
46
|
+
| `cc` | Open commit message buffer |
|
|
47
|
+
| `q` | Close buffer |
|
|
48
|
+
| `R` | Refresh status |
|
|
49
|
+
|
|
50
|
+
## Keymaps (Log/Blame Buffer)
|
|
51
|
+
|
|
52
|
+
| Key | Description |
|
|
53
|
+
|-----|-------------|
|
|
54
|
+
| `Enter` | Show commit diff in vertical split |
|
|
55
|
+
| `q` | Close buffer |
|
|
56
|
+
|
|
57
|
+
In the log buffer (`:Git log`), pressing `Enter` on a commit line opens the full commit diff with syntax highlighting.
|
|
58
|
+
|
|
59
|
+
In the blame buffer (`:Git blame`), pressing `Enter` on any line shows the diff for that line's commit. Uncommitted changes (shown as `00000000`) are skipped.
|
|
60
|
+
|
|
61
|
+
## Commit Message Buffer
|
|
62
|
+
|
|
63
|
+
When you press `cc` in the status buffer, a commit message editor opens:
|
|
64
|
+
|
|
65
|
+
1. Write your commit message at the top of the buffer
|
|
66
|
+
2. Lines starting with `#` are comments and will be ignored
|
|
67
|
+
3. Save with `:w` to execute the commit
|
|
68
|
+
4. Close with `:q` to cancel
|
|
69
|
+
|
|
70
|
+
The buffer shows the current branch and staged files as reference.
|
|
71
|
+
|
|
72
|
+
## Diff Highlighting
|
|
73
|
+
|
|
74
|
+
Diff output is syntax highlighted:
|
|
75
|
+
- Added lines (`+`) are shown in green
|
|
76
|
+
- Deleted lines (`-`) are shown in red
|
|
77
|
+
- Hunk headers (`@@`) are shown in cyan
|
|
78
|
+
- File headers are shown in yellow (bold)
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
bundle install
|
|
86
|
+
bundle exec rake test
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Contributing
|
|
90
|
+
|
|
91
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/S-H-GAMELINKS/mui-git.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Git
|
|
5
|
+
module Buffers
|
|
6
|
+
# Base class for Git buffers (inherits from Mui::Buffer)
|
|
7
|
+
# Provides common functionality for git-related buffers
|
|
8
|
+
class Base < Mui::Buffer
|
|
9
|
+
attr_accessor :pending_key
|
|
10
|
+
|
|
11
|
+
def initialize(name)
|
|
12
|
+
super(name)
|
|
13
|
+
@readonly = true
|
|
14
|
+
@pending_key = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Update buffer content (works even for readonly buffers)
|
|
18
|
+
def refresh_content(text)
|
|
19
|
+
@lines = text.lines.map(&:chomp)
|
|
20
|
+
@lines = [""] if @lines.empty?
|
|
21
|
+
@modified = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Set pending key for multi-key sequences (like dv, cc)
|
|
25
|
+
def set_pending(key)
|
|
26
|
+
@pending_key = key
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Clear pending key
|
|
30
|
+
def clear_pending
|
|
31
|
+
@pending_key = nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Git
|
|
5
|
+
module Buffers
|
|
6
|
+
# Buffer for displaying git blame with commit selection
|
|
7
|
+
class BlameBuffer < Base
|
|
8
|
+
attr_reader :runner, :file_path
|
|
9
|
+
|
|
10
|
+
def initialize(file_path, runner: CommandRunner.new)
|
|
11
|
+
super("[Git Blame: #{File.basename(file_path)}]")
|
|
12
|
+
@file_path = file_path
|
|
13
|
+
@runner = runner
|
|
14
|
+
refresh
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def refresh
|
|
18
|
+
output = @runner.blame(@file_path)
|
|
19
|
+
if output.empty?
|
|
20
|
+
refresh_content("(no blame data)")
|
|
21
|
+
else
|
|
22
|
+
refresh_content(output)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Extract commit hash from current line
|
|
27
|
+
# Blame format: "abc12345 (Author Name 2024-01-01 12:00:00 +0900 1) code..."
|
|
28
|
+
# or for uncommitted: "00000000 (Not Committed Yet ...)"
|
|
29
|
+
def commit_at(row)
|
|
30
|
+
line = @lines[row]
|
|
31
|
+
return nil unless line
|
|
32
|
+
|
|
33
|
+
# Match commit hash at start of line (skip 00000000 for uncommitted)
|
|
34
|
+
match = line.match(/^([a-f0-9]{7,40})\s/)
|
|
35
|
+
return nil unless match
|
|
36
|
+
|
|
37
|
+
hash = match[1]
|
|
38
|
+
# Skip uncommitted changes (all zeros)
|
|
39
|
+
return nil if hash.match?(/^0+$/)
|
|
40
|
+
|
|
41
|
+
hash
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Git
|
|
5
|
+
module Buffers
|
|
6
|
+
# Buffer for editing git commit message
|
|
7
|
+
# When saved, executes git commit with the message
|
|
8
|
+
class CommitBuffer < Mui::Buffer
|
|
9
|
+
attr_reader :runner, :editor, :on_commit
|
|
10
|
+
|
|
11
|
+
COMMIT_MSG_TEMPLATE = <<~MSG
|
|
12
|
+
|
|
13
|
+
# Please enter the commit message for your changes.
|
|
14
|
+
# Lines starting with '#' will be ignored.
|
|
15
|
+
#
|
|
16
|
+
# On branch: %<branch>s
|
|
17
|
+
#
|
|
18
|
+
# Changes to be committed:
|
|
19
|
+
%<staged>s
|
|
20
|
+
MSG
|
|
21
|
+
|
|
22
|
+
def initialize(runner: CommandRunner.new, editor: nil, on_commit: nil)
|
|
23
|
+
super("[Git Commit]")
|
|
24
|
+
@runner = runner
|
|
25
|
+
@editor = editor
|
|
26
|
+
@on_commit = on_commit
|
|
27
|
+
@readonly = false
|
|
28
|
+
setup_template
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Override save to execute git commit instead of writing to file
|
|
32
|
+
def save(_path = nil)
|
|
33
|
+
message = extract_message
|
|
34
|
+
if message.empty?
|
|
35
|
+
@editor&.message = "Git: empty commit message, aborting"
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
output = @runner.commit(message)
|
|
41
|
+
if output =~ /\[.+? ([a-f0-9]+)\]/
|
|
42
|
+
@editor&.message = "Git: committed [#{::Regexp.last_match(1)}]"
|
|
43
|
+
else
|
|
44
|
+
@editor&.message = "Git: committed"
|
|
45
|
+
end
|
|
46
|
+
@modified = false
|
|
47
|
+
@on_commit&.call
|
|
48
|
+
close_buffer
|
|
49
|
+
rescue CommandRunner::GitCommandError => e
|
|
50
|
+
@editor&.message = "Git error: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def setup_template
|
|
57
|
+
branch = @runner.current_branch rescue "unknown"
|
|
58
|
+
staged = format_staged_files
|
|
59
|
+
|
|
60
|
+
template = format(COMMIT_MSG_TEMPLATE, branch: branch, staged: staged)
|
|
61
|
+
@lines = template.lines.map(&:chomp)
|
|
62
|
+
@lines = [""] if @lines.empty?
|
|
63
|
+
@modified = false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def format_staged_files
|
|
67
|
+
output = @runner.status
|
|
68
|
+
staged_lines = output.lines.select do |line|
|
|
69
|
+
index_status = line[0]
|
|
70
|
+
index_status != " " && index_status != "?"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if staged_lines.empty?
|
|
74
|
+
"# (no staged changes)"
|
|
75
|
+
else
|
|
76
|
+
staged_lines.map { |line| "# #{line.strip}" }.join("\n")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def extract_message
|
|
81
|
+
# Filter out comment lines and join
|
|
82
|
+
message_lines = @lines.reject { |line| line.start_with?("#") }
|
|
83
|
+
# Remove leading/trailing blank lines
|
|
84
|
+
message_lines.shift while message_lines.first&.strip&.empty?
|
|
85
|
+
message_lines.pop while message_lines.last&.strip&.empty?
|
|
86
|
+
message_lines.join("\n").strip
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def close_buffer
|
|
90
|
+
return unless @editor
|
|
91
|
+
|
|
92
|
+
window_manager = @editor.tab_manager.current_tab.window_manager
|
|
93
|
+
current_window = window_manager.active_window
|
|
94
|
+
window_manager.close_current_window if current_window&.buffer == self
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../highlighters/diff_highlighter"
|
|
4
|
+
|
|
5
|
+
module Mui
|
|
6
|
+
module Git
|
|
7
|
+
module Buffers
|
|
8
|
+
# Buffer for displaying a specific commit's diff
|
|
9
|
+
class CommitShowBuffer < Base
|
|
10
|
+
attr_reader :runner, :commit_hash
|
|
11
|
+
|
|
12
|
+
def initialize(commit_hash, runner: CommandRunner.new)
|
|
13
|
+
super("[Git Commit: #{commit_hash[0, 7]}]")
|
|
14
|
+
@commit_hash = commit_hash
|
|
15
|
+
@runner = runner
|
|
16
|
+
refresh
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def refresh
|
|
20
|
+
output = @runner.show(@commit_hash)
|
|
21
|
+
if output.empty?
|
|
22
|
+
refresh_content("(no commit data)")
|
|
23
|
+
else
|
|
24
|
+
refresh_content(output)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Provide diff highlighter for this buffer
|
|
29
|
+
def custom_highlighters(color_scheme)
|
|
30
|
+
[Highlighters::DiffHighlighter.new(color_scheme)]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../highlighters/diff_highlighter"
|
|
4
|
+
|
|
5
|
+
module Mui
|
|
6
|
+
module Git
|
|
7
|
+
module Buffers
|
|
8
|
+
# Buffer for displaying git diff
|
|
9
|
+
class DiffBuffer < Base
|
|
10
|
+
attr_reader :file_path, :runner
|
|
11
|
+
|
|
12
|
+
def initialize(file_path, runner: CommandRunner.new, staged: false)
|
|
13
|
+
super("[Git Diff: #{File.basename(file_path)}]")
|
|
14
|
+
@file_path = file_path
|
|
15
|
+
@runner = runner
|
|
16
|
+
@staged = staged
|
|
17
|
+
refresh
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Refresh diff content
|
|
21
|
+
def refresh
|
|
22
|
+
output = if @staged
|
|
23
|
+
@runner.diff_staged(@file_path)
|
|
24
|
+
else
|
|
25
|
+
@runner.diff(@file_path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if output.empty?
|
|
29
|
+
refresh_content("(no changes)")
|
|
30
|
+
else
|
|
31
|
+
refresh_content(output)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Provide diff highlighter for this buffer
|
|
36
|
+
def custom_highlighters(color_scheme)
|
|
37
|
+
[Highlighters::DiffHighlighter.new(color_scheme)]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Git
|
|
5
|
+
module Buffers
|
|
6
|
+
# Buffer for displaying git log with commit selection
|
|
7
|
+
class LogBuffer < Base
|
|
8
|
+
attr_reader :runner
|
|
9
|
+
|
|
10
|
+
def initialize(runner: CommandRunner.new, limit: 20)
|
|
11
|
+
super("[Git Log]")
|
|
12
|
+
@runner = runner
|
|
13
|
+
@limit = limit
|
|
14
|
+
refresh
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def refresh
|
|
18
|
+
output = @runner.log(limit: @limit)
|
|
19
|
+
if output.empty?
|
|
20
|
+
refresh_content("(no commits)")
|
|
21
|
+
else
|
|
22
|
+
refresh_content(output)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Extract commit hash from current line
|
|
27
|
+
# Log format: "abc1234 commit message..."
|
|
28
|
+
def commit_at(row)
|
|
29
|
+
line = @lines[row]
|
|
30
|
+
return nil unless line
|
|
31
|
+
|
|
32
|
+
# Match short commit hash at start of line
|
|
33
|
+
match = line.match(/^([a-f0-9]{7,40})\s/)
|
|
34
|
+
match&.[](1)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Git
|
|
5
|
+
module Buffers
|
|
6
|
+
# Interactive git status buffer
|
|
7
|
+
# Displays staged/unstaged files and allows staging/unstaging
|
|
8
|
+
class StatusBuffer < Base
|
|
9
|
+
# Represents a file in git status
|
|
10
|
+
FileEntry = Struct.new(:index_status, :work_tree_status, :path, keyword_init: true) do
|
|
11
|
+
def staged?
|
|
12
|
+
index_status != " " && index_status != "?"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def unstaged?
|
|
16
|
+
work_tree_status != " "
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def untracked?
|
|
20
|
+
index_status == "?" && work_tree_status == "?"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def status_display
|
|
24
|
+
case
|
|
25
|
+
when untracked? then "??"
|
|
26
|
+
when staged? && unstaged? then "#{index_status}#{work_tree_status}"
|
|
27
|
+
when staged? then "#{index_status} "
|
|
28
|
+
when unstaged? then " #{work_tree_status}"
|
|
29
|
+
else " "
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
attr_reader :files, :runner
|
|
35
|
+
|
|
36
|
+
def initialize(runner: CommandRunner.new)
|
|
37
|
+
super("[Git Status]")
|
|
38
|
+
@runner = runner
|
|
39
|
+
@files = []
|
|
40
|
+
@staged_files = []
|
|
41
|
+
@unstaged_files = []
|
|
42
|
+
refresh
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Refresh git status
|
|
46
|
+
def refresh
|
|
47
|
+
output = @runner.status
|
|
48
|
+
@files = parse_status(output)
|
|
49
|
+
@staged_files = @files.select(&:staged?)
|
|
50
|
+
@unstaged_files = @files.select { |f| f.unstaged? || f.untracked? }
|
|
51
|
+
refresh_content(format_display)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get file at cursor position
|
|
55
|
+
def file_at(row)
|
|
56
|
+
# Account for header lines
|
|
57
|
+
# Line 0: "Staged:" header (or "Unstaged:" if no staged files)
|
|
58
|
+
# Lines 1..staged_count: staged files
|
|
59
|
+
# Line staged_count+1: empty line (if both sections exist)
|
|
60
|
+
# Line staged_count+2: "Unstaged:" header
|
|
61
|
+
# Lines staged_count+3..end: unstaged files
|
|
62
|
+
|
|
63
|
+
staged_start_row = 1
|
|
64
|
+
staged_end_row = staged_start_row + @staged_files.size - 1
|
|
65
|
+
|
|
66
|
+
unstaged_header_row = @staged_files.empty? ? 0 : staged_end_row + 2
|
|
67
|
+
unstaged_start_row = unstaged_header_row + 1
|
|
68
|
+
|
|
69
|
+
if @staged_files.any? && row >= staged_start_row && row <= staged_end_row
|
|
70
|
+
@staged_files[row - staged_start_row]
|
|
71
|
+
elsif @unstaged_files.any? && row >= unstaged_start_row
|
|
72
|
+
index = row - unstaged_start_row
|
|
73
|
+
@unstaged_files[index] if index < @unstaged_files.size
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def parse_status(output)
|
|
80
|
+
output.lines.map do |line|
|
|
81
|
+
next if line.strip.empty?
|
|
82
|
+
|
|
83
|
+
index_status = line[0]
|
|
84
|
+
work_tree_status = line[1]
|
|
85
|
+
path = line[3..].strip
|
|
86
|
+
|
|
87
|
+
FileEntry.new(
|
|
88
|
+
index_status: index_status,
|
|
89
|
+
work_tree_status: work_tree_status,
|
|
90
|
+
path: path
|
|
91
|
+
)
|
|
92
|
+
end.compact
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def format_display
|
|
96
|
+
lines = []
|
|
97
|
+
|
|
98
|
+
if @staged_files.any?
|
|
99
|
+
lines << "Staged:"
|
|
100
|
+
@staged_files.each do |file|
|
|
101
|
+
lines << " #{file.status_display} #{file.path}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if @staged_files.any? && @unstaged_files.any?
|
|
106
|
+
lines << ""
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if @unstaged_files.any?
|
|
110
|
+
lines << "Unstaged:"
|
|
111
|
+
@unstaged_files.each do |file|
|
|
112
|
+
lines << " #{file.status_display} #{file.path}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if lines.empty?
|
|
117
|
+
lines << "Working tree clean"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
lines << ""
|
|
121
|
+
lines << "Press: s=stage, u=unstage, -=toggle, dv=diff, cc=commit, q=quit, R=refresh"
|
|
122
|
+
|
|
123
|
+
lines.join("\n")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
|
|
6
|
+
module Mui
|
|
7
|
+
module Git
|
|
8
|
+
# Executes git commands and handles errors
|
|
9
|
+
class CommandRunner
|
|
10
|
+
class NotInGitRepositoryError < StandardError
|
|
11
|
+
def initialize
|
|
12
|
+
super("Not in a git repository")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class GitCommandError < StandardError
|
|
17
|
+
attr_reader :stderr, :exit_status
|
|
18
|
+
|
|
19
|
+
def initialize(message, stderr: nil, exit_status: nil)
|
|
20
|
+
super(message)
|
|
21
|
+
@stderr = stderr
|
|
22
|
+
@exit_status = exit_status
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Check if current directory is inside a git repository
|
|
27
|
+
def in_git_repository?
|
|
28
|
+
_, _, status = Open3.capture3("git", "rev-parse", "--git-dir")
|
|
29
|
+
status.success?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get git status in porcelain format
|
|
33
|
+
def status
|
|
34
|
+
run("status", "--porcelain")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get diff for a file or all files
|
|
38
|
+
def diff(file_path = nil)
|
|
39
|
+
if file_path
|
|
40
|
+
run("diff", "--", file_path)
|
|
41
|
+
else
|
|
42
|
+
run("diff")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get staged diff
|
|
47
|
+
def diff_staged(file_path = nil)
|
|
48
|
+
if file_path
|
|
49
|
+
run("diff", "--cached", "--", file_path)
|
|
50
|
+
else
|
|
51
|
+
run("diff", "--cached")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get git log
|
|
56
|
+
def log(limit: 20, format: nil)
|
|
57
|
+
args = ["log", "--oneline", "--decorate", "-n", limit.to_s]
|
|
58
|
+
args += ["--format=#{format}"] if format
|
|
59
|
+
run(*args)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get git blame for a file
|
|
63
|
+
def blame(file_path)
|
|
64
|
+
run("blame", "--", file_path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Stage a file
|
|
68
|
+
def add(file_path)
|
|
69
|
+
run("add", "--", file_path)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Unstage a file
|
|
73
|
+
def reset(file_path)
|
|
74
|
+
run("reset", "HEAD", "--", file_path)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Create a commit
|
|
78
|
+
def commit(message)
|
|
79
|
+
run("commit", "-m", message)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get current branch name
|
|
83
|
+
def current_branch
|
|
84
|
+
run("rev-parse", "--abbrev-ref", "HEAD").strip
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Show diff for a specific commit
|
|
88
|
+
def show(commit_hash)
|
|
89
|
+
run("show", "--format=fuller", commit_hash)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Run a git command synchronously
|
|
93
|
+
def run(*args)
|
|
94
|
+
ensure_git_repository!
|
|
95
|
+
|
|
96
|
+
stdout, stderr, status = Open3.capture3("git", *args)
|
|
97
|
+
|
|
98
|
+
unless status.success?
|
|
99
|
+
raise GitCommandError.new(
|
|
100
|
+
"git #{args.join(" ")} failed: #{stderr.lines.first&.strip}",
|
|
101
|
+
stderr: stderr,
|
|
102
|
+
exit_status: status.exitstatus
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
stdout
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def ensure_git_repository!
|
|
112
|
+
raise NotInGitRepositoryError unless in_git_repository?
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Git
|
|
5
|
+
module Highlighters
|
|
6
|
+
# Highlighter for git diff output
|
|
7
|
+
class DiffHighlighter < Mui::Highlighters::Base
|
|
8
|
+
PRIORITY_DIFF = 50
|
|
9
|
+
|
|
10
|
+
def highlights_for(row, line, _options = {})
|
|
11
|
+
style = style_for_line(line)
|
|
12
|
+
return [] unless style
|
|
13
|
+
|
|
14
|
+
[
|
|
15
|
+
Mui::Highlight.new(
|
|
16
|
+
start_col: 0,
|
|
17
|
+
end_col: line.length,
|
|
18
|
+
style: style,
|
|
19
|
+
priority: priority
|
|
20
|
+
)
|
|
21
|
+
]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def priority
|
|
25
|
+
PRIORITY_DIFF
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def style_for_line(line)
|
|
31
|
+
case line
|
|
32
|
+
when /^@@/
|
|
33
|
+
:diff_hunk
|
|
34
|
+
when /^\+\+\+/, /^---/, /^diff --git/, /^index /
|
|
35
|
+
:diff_header
|
|
36
|
+
when /^\+/
|
|
37
|
+
:diff_add
|
|
38
|
+
when /^-/
|
|
39
|
+
:diff_delete
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Git
|
|
5
|
+
# Main plugin class for mui-git
|
|
6
|
+
# Registers commands and keymaps for git integration
|
|
7
|
+
class Plugin < Mui::Plugin
|
|
8
|
+
name "git"
|
|
9
|
+
|
|
10
|
+
def setup
|
|
11
|
+
register_commands
|
|
12
|
+
register_keymaps
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def register_commands
|
|
18
|
+
# Main :Git command
|
|
19
|
+
command(:Git) do |ctx, args|
|
|
20
|
+
handle_git_command(ctx, args)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def register_keymaps
|
|
25
|
+
# Keymaps for Git buffers
|
|
26
|
+
register_status_keymaps
|
|
27
|
+
register_log_blame_keymaps
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def register_status_keymaps
|
|
31
|
+
# Stage file
|
|
32
|
+
keymap(:normal, "s") do |ctx|
|
|
33
|
+
next unless status_buffer?(ctx.buffer)
|
|
34
|
+
|
|
35
|
+
handle_stage(ctx)
|
|
36
|
+
true # Indicate key was handled
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Unstage file
|
|
40
|
+
keymap(:normal, "u") do |ctx|
|
|
41
|
+
next unless status_buffer?(ctx.buffer)
|
|
42
|
+
|
|
43
|
+
handle_unstage(ctx)
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Toggle stage/unstage
|
|
48
|
+
keymap(:normal, "-") do |ctx|
|
|
49
|
+
next unless status_buffer?(ctx.buffer)
|
|
50
|
+
|
|
51
|
+
handle_toggle(ctx)
|
|
52
|
+
true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Diff (first key of 'dv')
|
|
56
|
+
keymap(:normal, "d") do |ctx|
|
|
57
|
+
next unless git_buffer?(ctx.buffer)
|
|
58
|
+
|
|
59
|
+
ctx.buffer.set_pending("d")
|
|
60
|
+
true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Diff vertical split (when 'v' pressed after 'd')
|
|
64
|
+
keymap(:normal, "v") do |ctx|
|
|
65
|
+
next unless git_buffer?(ctx.buffer)
|
|
66
|
+
next unless ctx.buffer.pending_key == "d"
|
|
67
|
+
|
|
68
|
+
ctx.buffer.clear_pending
|
|
69
|
+
handle_diff_vertical(ctx)
|
|
70
|
+
true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Commit (first key of 'cc')
|
|
74
|
+
keymap(:normal, "c") do |ctx|
|
|
75
|
+
next unless status_buffer?(ctx.buffer)
|
|
76
|
+
|
|
77
|
+
if ctx.buffer.pending_key == "c"
|
|
78
|
+
ctx.buffer.clear_pending
|
|
79
|
+
handle_commit(ctx)
|
|
80
|
+
else
|
|
81
|
+
ctx.buffer.set_pending("c")
|
|
82
|
+
end
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Quit buffer
|
|
87
|
+
keymap(:normal, "q") do |ctx|
|
|
88
|
+
next unless git_buffer?(ctx.buffer)
|
|
89
|
+
|
|
90
|
+
handle_quit(ctx)
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Refresh
|
|
95
|
+
keymap(:normal, "R") do |ctx|
|
|
96
|
+
next unless status_buffer?(ctx.buffer)
|
|
97
|
+
|
|
98
|
+
handle_refresh(ctx)
|
|
99
|
+
true
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def register_log_blame_keymaps
|
|
104
|
+
# Enter key to show commit diff in Log/Blame buffers
|
|
105
|
+
keymap(:normal, "\r") do |ctx|
|
|
106
|
+
next unless log_buffer?(ctx.buffer) || blame_buffer?(ctx.buffer)
|
|
107
|
+
|
|
108
|
+
handle_show_commit(ctx)
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Command handlers
|
|
114
|
+
|
|
115
|
+
def handle_git_command(ctx, args)
|
|
116
|
+
# args is a string like "log 20" or nil
|
|
117
|
+
arg_parts = args.to_s.split(/\s+/)
|
|
118
|
+
subcommand = arg_parts[0]&.strip || ""
|
|
119
|
+
|
|
120
|
+
runner = CommandRunner.new
|
|
121
|
+
|
|
122
|
+
unless runner.in_git_repository?
|
|
123
|
+
ctx.set_message("Git: not in a git repository")
|
|
124
|
+
return
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
case subcommand
|
|
128
|
+
when "", "status"
|
|
129
|
+
open_status_buffer(ctx, runner)
|
|
130
|
+
when "diff"
|
|
131
|
+
file_path = arg_parts[1] || current_file_path(ctx)
|
|
132
|
+
open_diff_buffer(ctx, file_path, runner)
|
|
133
|
+
when "log"
|
|
134
|
+
limit = arg_parts[1]&.to_i || 20
|
|
135
|
+
show_log(ctx, limit, runner)
|
|
136
|
+
when "blame"
|
|
137
|
+
file_path = arg_parts[1] || current_file_path(ctx)
|
|
138
|
+
show_blame(ctx, file_path, runner)
|
|
139
|
+
when "add"
|
|
140
|
+
file_path = resolve_file_path(arg_parts[1], ctx)
|
|
141
|
+
stage_file(ctx, file_path, runner)
|
|
142
|
+
when "commit"
|
|
143
|
+
message = arg_parts[1..].join(" ") if arg_parts.size > 1
|
|
144
|
+
create_commit(ctx, message, runner)
|
|
145
|
+
else
|
|
146
|
+
ctx.set_message("Git: unknown command '#{subcommand}'")
|
|
147
|
+
end
|
|
148
|
+
rescue CommandRunner::NotInGitRepositoryError
|
|
149
|
+
ctx.set_message("Git: not in a git repository")
|
|
150
|
+
rescue CommandRunner::GitCommandError => e
|
|
151
|
+
ctx.set_message("Git error: #{e.message}")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Status buffer handlers
|
|
155
|
+
|
|
156
|
+
def handle_stage(ctx)
|
|
157
|
+
file = ctx.buffer.file_at(ctx.window.cursor_row)
|
|
158
|
+
return unless file
|
|
159
|
+
|
|
160
|
+
ctx.run_shell_command("git add -- #{Shellwords.escape(file.path)}",
|
|
161
|
+
on_complete: lambda { |job|
|
|
162
|
+
if job.result[:success]
|
|
163
|
+
ctx.buffer.refresh
|
|
164
|
+
ctx.set_message("Staged: #{file.path}")
|
|
165
|
+
else
|
|
166
|
+
ctx.set_message("Git error: #{job.result[:stderr]&.lines&.first&.strip}")
|
|
167
|
+
end
|
|
168
|
+
})
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def handle_unstage(ctx)
|
|
172
|
+
file = ctx.buffer.file_at(ctx.window.cursor_row)
|
|
173
|
+
return unless file
|
|
174
|
+
|
|
175
|
+
ctx.run_shell_command("git reset HEAD -- #{Shellwords.escape(file.path)}",
|
|
176
|
+
on_complete: lambda { |job|
|
|
177
|
+
if job.result[:success]
|
|
178
|
+
ctx.buffer.refresh
|
|
179
|
+
ctx.set_message("Unstaged: #{file.path}")
|
|
180
|
+
else
|
|
181
|
+
ctx.set_message("Git error: #{job.result[:stderr]&.lines&.first&.strip}")
|
|
182
|
+
end
|
|
183
|
+
})
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def handle_toggle(ctx)
|
|
187
|
+
file = ctx.buffer.file_at(ctx.window.cursor_row)
|
|
188
|
+
return unless file
|
|
189
|
+
|
|
190
|
+
if file.staged?
|
|
191
|
+
handle_unstage(ctx)
|
|
192
|
+
else
|
|
193
|
+
handle_stage(ctx)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def handle_diff_vertical(ctx)
|
|
198
|
+
if status_buffer?(ctx.buffer)
|
|
199
|
+
file = ctx.buffer.file_at(ctx.window.cursor_row)
|
|
200
|
+
return unless file
|
|
201
|
+
|
|
202
|
+
diff_buffer = Buffers::DiffBuffer.new(file.path, runner: ctx.buffer.runner, staged: file.staged?)
|
|
203
|
+
ctx.editor.window_manager.split_vertical(diff_buffer)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def handle_commit(ctx)
|
|
208
|
+
runner = ctx.buffer.respond_to?(:runner) ? ctx.buffer.runner : CommandRunner.new
|
|
209
|
+
status_buffer = ctx.buffer if status_buffer?(ctx.buffer)
|
|
210
|
+
|
|
211
|
+
# Callback to refresh status buffer after commit
|
|
212
|
+
on_commit = lambda do
|
|
213
|
+
status_buffer&.refresh
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
commit_buffer = Buffers::CommitBuffer.new(
|
|
217
|
+
runner: runner,
|
|
218
|
+
editor: ctx.editor,
|
|
219
|
+
on_commit: on_commit
|
|
220
|
+
)
|
|
221
|
+
ctx.editor.window_manager.split_horizontal(commit_buffer)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def handle_quit(ctx)
|
|
225
|
+
ctx.editor.window_manager.close_window(ctx.window)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def handle_refresh(ctx)
|
|
229
|
+
ctx.buffer.refresh
|
|
230
|
+
ctx.set_message("Git: status refreshed")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Helper methods
|
|
234
|
+
|
|
235
|
+
def open_status_buffer(ctx, runner)
|
|
236
|
+
status_buffer = Buffers::StatusBuffer.new(runner: runner)
|
|
237
|
+
ctx.editor.window_manager.split_horizontal(status_buffer)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def open_diff_buffer(ctx, file_path, runner)
|
|
241
|
+
if file_path.nil? || file_path.empty?
|
|
242
|
+
ctx.set_message("Git: no file specified")
|
|
243
|
+
return
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
diff_buffer = Buffers::DiffBuffer.new(file_path, runner: runner)
|
|
247
|
+
ctx.editor.window_manager.split_horizontal(diff_buffer)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def show_log(ctx, limit, runner)
|
|
251
|
+
log_buffer = Buffers::LogBuffer.new(runner: runner, limit: limit)
|
|
252
|
+
ctx.editor.window_manager.split_horizontal(log_buffer)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def show_blame(ctx, file_path, runner)
|
|
256
|
+
if file_path.nil? || file_path.empty?
|
|
257
|
+
ctx.set_message("Git: no file specified")
|
|
258
|
+
return
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
blame_buffer = Buffers::BlameBuffer.new(file_path, runner: runner)
|
|
262
|
+
ctx.editor.window_manager.split_horizontal(blame_buffer)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def handle_show_commit(ctx)
|
|
266
|
+
commit_hash = ctx.buffer.commit_at(ctx.window.cursor_row)
|
|
267
|
+
return ctx.set_message("Git: no commit at cursor") unless commit_hash
|
|
268
|
+
|
|
269
|
+
runner = ctx.buffer.runner
|
|
270
|
+
commit_buffer = Buffers::CommitShowBuffer.new(commit_hash, runner: runner)
|
|
271
|
+
ctx.editor.window_manager.split_vertical(commit_buffer)
|
|
272
|
+
rescue CommandRunner::GitCommandError => e
|
|
273
|
+
ctx.set_message("Git error: #{e.message}")
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def stage_file(ctx, file_path, runner)
|
|
277
|
+
if file_path.nil? || file_path.empty?
|
|
278
|
+
ctx.set_message("Git: no file specified")
|
|
279
|
+
return
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
runner.add(file_path)
|
|
283
|
+
ctx.set_message("Git: staged #{file_path}")
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def create_commit(ctx, message, runner)
|
|
287
|
+
if message.nil? || message.strip.empty?
|
|
288
|
+
ctx.set_message("Git: commit message required. Usage: :Git commit <message>")
|
|
289
|
+
return
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
output = runner.commit(message)
|
|
293
|
+
# Extract commit hash from output
|
|
294
|
+
if output =~ /\[.+? ([a-f0-9]+)\]/
|
|
295
|
+
ctx.set_message("Git: committed [#{::Regexp.last_match(1)}]")
|
|
296
|
+
else
|
|
297
|
+
ctx.set_message("Git: committed")
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def status_buffer?(buffer)
|
|
302
|
+
buffer.is_a?(Buffers::StatusBuffer)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def log_buffer?(buffer)
|
|
306
|
+
buffer.is_a?(Buffers::LogBuffer)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def blame_buffer?(buffer)
|
|
310
|
+
buffer.is_a?(Buffers::BlameBuffer)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def git_buffer?(buffer)
|
|
314
|
+
buffer.is_a?(Buffers::Base) ||
|
|
315
|
+
buffer.is_a?(Buffers::LogBuffer) ||
|
|
316
|
+
buffer.is_a?(Buffers::BlameBuffer)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def current_file_path(ctx)
|
|
320
|
+
path = ctx.buffer.file_path
|
|
321
|
+
return nil if path.nil? || path.start_with?("[")
|
|
322
|
+
|
|
323
|
+
path
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def resolve_file_path(arg, ctx)
|
|
327
|
+
return current_file_path(ctx) if arg == "%"
|
|
328
|
+
|
|
329
|
+
arg
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
data/lib/mui/git.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mui"
|
|
4
|
+
|
|
5
|
+
require_relative "git/version"
|
|
6
|
+
require_relative "git/command_runner"
|
|
7
|
+
require_relative "git/buffers/base"
|
|
8
|
+
require_relative "git/buffers/status_buffer"
|
|
9
|
+
require_relative "git/buffers/diff_buffer"
|
|
10
|
+
require_relative "git/buffers/commit_buffer"
|
|
11
|
+
require_relative "git/buffers/log_buffer"
|
|
12
|
+
require_relative "git/buffers/blame_buffer"
|
|
13
|
+
require_relative "git/buffers/commit_show_buffer"
|
|
14
|
+
require_relative "git/plugin"
|
|
15
|
+
|
|
16
|
+
module Mui
|
|
17
|
+
module Git
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Register the plugin with Mui
|
|
23
|
+
Mui.plugin_manager.register(:git, Mui::Git::Plugin)
|
data/lib/mui_git.rb
ADDED
data/sig/mui/git.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mui-git
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- S-H-GAMELINKS
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: mui
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.1.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.1.0
|
|
26
|
+
description: A Git integration plugin for Mui TUI editor. Provides interactive git
|
|
27
|
+
status buffer with staging/unstaging, diff view, and more.
|
|
28
|
+
email:
|
|
29
|
+
- gamelinks007@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- LICENSE.txt
|
|
36
|
+
- README.md
|
|
37
|
+
- Rakefile
|
|
38
|
+
- lib/mui/git.rb
|
|
39
|
+
- lib/mui/git/buffers/base.rb
|
|
40
|
+
- lib/mui/git/buffers/blame_buffer.rb
|
|
41
|
+
- lib/mui/git/buffers/commit_buffer.rb
|
|
42
|
+
- lib/mui/git/buffers/commit_show_buffer.rb
|
|
43
|
+
- lib/mui/git/buffers/diff_buffer.rb
|
|
44
|
+
- lib/mui/git/buffers/log_buffer.rb
|
|
45
|
+
- lib/mui/git/buffers/status_buffer.rb
|
|
46
|
+
- lib/mui/git/command_runner.rb
|
|
47
|
+
- lib/mui/git/highlighters/diff_highlighter.rb
|
|
48
|
+
- lib/mui/git/plugin.rb
|
|
49
|
+
- lib/mui/git/version.rb
|
|
50
|
+
- lib/mui_git.rb
|
|
51
|
+
- sig/mui/git.rbs
|
|
52
|
+
homepage: https://github.com/S-H-GAMELINKS/mui-git
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
homepage_uri: https://github.com/S-H-GAMELINKS/mui-git
|
|
57
|
+
source_code_uri: https://github.com/S-H-GAMELINKS/mui-git
|
|
58
|
+
changelog_uri: https://github.com/S-H-GAMELINKS/mui-git/blob/master/CHANGELOG.md
|
|
59
|
+
rdoc_options: []
|
|
60
|
+
require_paths:
|
|
61
|
+
- lib
|
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: 3.2.0
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
requirements: []
|
|
73
|
+
rubygems_version: 4.0.0.beta2
|
|
74
|
+
specification_version: 4
|
|
75
|
+
summary: Git integration plugin for Mui editor (fugitive.vim-like)
|
|
76
|
+
test_files: []
|