Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c05cc67
fix: fix module path in main.go
0zXD Aug 7, 2025
c6e53b3
update CI.yml
0zXD Aug 7, 2025
6f4fa82
update go.mod
0zXD Aug 7, 2025
bc804f1
fix tui.go
bakayu Aug 7, 2025
a64708c
initial implementation of the TUI for the application.
0zXD Aug 13, 2025
1e03ae5
add Makefile
bakayu Aug 17, 2025
53d35f1
fix failing tests and update helpers
bakayu Aug 17, 2025
0c7c43b
Merge branch 'master' into tui/display-tui.go
bakayu Aug 17, 2025
46d0826
add make sync to Makefile
bakayu Aug 17, 2025
584409d
Merge branch 'master' into tui/display-tui.go
bakayu Aug 17, 2025
54748eb
update Makefile and build.yml
bakayu Aug 17, 2025
9ce81d5
add `make run` to Makefile
bakayu Aug 18, 2025
7d267b8
refactor: separate git commands into individual files
bakayu Aug 18, 2025
22cadd4
fix ci-lint errors
bakayu Aug 18, 2025
76c6819
WIP: make TUI structure
bakayu Aug 21, 2025
001fae8
WIP: add keybinding hints
bakayu Aug 21, 2025
d940f42
feat: add help menu toggle
bakayu Aug 21, 2025
42204ae
refactor+feat: theme, focus logic, mouse events
bakayu Aug 22, 2025
2975fe8
fix failing tests
bakayu Aug 22, 2025
1b4a631
refactor, ux and tests:
bakayu Aug 24, 2025
c0ae99f
feat: display repo name and branch in status panel
bakayu Sep 3, 2025
bd70e19
wip: add content to files panel
bakayu Sep 4, 2025
7732397
feat: display filtree in files panel
bakayu Sep 4, 2025
5fd220b
feat: display local branches in branches panel
bakayu Sep 4, 2025
ea11823
ops-fix: bump golangci-lint action to v8
bakayu Sep 4, 2025
c6e0902
fix ci errors
bakayu Sep 4, 2025
d18eea6
feat: display log history in Commits panel
bakayu Sep 4, 2025
7587539
feat: add column wide cursor for highlighting selected line
bakayu Sep 4, 2025
b49ff8d
feat: make lines in left-panels clickable
bakayu Sep 4, 2025
e623ffa
"could apply De Morgan's law (staticcheck)" 😭
bakayu Sep 4, 2025
881a18b
feat: display content in stash panel, add styling
bakayu Sep 5, 2025
3d17244
extend log's functionality
bakayu Sep 5, 2025
65bd376
refactor
bakayu Sep 5, 2025
0d84b60
fix failing test
bakayu Sep 5, 2025
f444ef3
refactor: move duplicate values to constants.go
bakayu Sep 6, 2025
ea7b1cd
update leftPanelWidthRatio
bakayu Sep 6, 2025
444f92c
add keybinds for Branches, Commits and Stash panels
bakayu Sep 6, 2025
4e49a36
feat: display content on main panel from left panels
bakayu Sep 7, 2025
e4094fd
fix failing tests, more changes:
bakayu Sep 7, 2025
9c84271
feat: add some functionality to the left panels
bakayu Sep 8, 2025
dbb57d6
fix failing test
bakayu Sep 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
go-version: '1.24'

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v8
with:
version: latest
args: --timeout=5m
Expand Down
15 changes: 5 additions & 10 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ name: build

# This workflow runs if the CI workflow is successful
on:
workflow_run:
workflows: ["CI"] # Keep consistent with actual CI workflow name
types:
- completed
branches: [master]
workflow_dispatch:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build:
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand All @@ -22,14 +19,12 @@ jobs:
with:
go-version: 1.24

# Syncs dependencies and builds the project
- name: Build
run: make build

- name: Run tests
run: make test

- name: Install
run: make install

- name: Clean up
run: make clean
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ go.work.sum
.vscode/

/gitx
/gitx.exe
/gitx.exe

build/
50 changes: 50 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Go parameters
BINARY_NAME=gitx
CMD_PATH=./cmd/gitx
BUILD_DIR=./build

# Default target executed when you run `make`
all: build

# Syncs dependencies
sync:
@echo "Syncing dependencies..."
@go mod tidy
@echo "Dependencies synced."

# Builds the binary
build: sync
@echo "Building the application..."
@mkdir -p $(BUILD_DIR)
@go build -o $(BUILD_DIR)/$(BINARY_NAME) $(CMD_PATH)
@echo "Binary available at $(BUILD_DIR)/$(BINARY_NAME)"

# Runs the application
run: build
@echo "Running $(BINARY_NAME)..."
@$(BUILD_DIR)/$(BINARY_NAME)

# Runs all tests
test:
@echo "Running tests..."
@go test -v ./...

# Runs golangci-lint
ci:
@echo "Running golangci-lint..."
@golangci-lint run

# Installs the binary to /usr/local/bin
install: build
@echo "Installing $(BINARY_NAME)..."
@go install $(CMD_PATH)
@echo "$(BINARY_NAME) installed successfully"

# Cleans the build artifacts
clean:
@echo "Cleaning up..."
@rm -rf $(BUILD_DIR)
@echo "Cleanup complete."

#PHONY targets are not files
.PHONY: all sync build run test ci install clean
12 changes: 11 additions & 1 deletion cmd/gitx/main.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
package main

import (
"errors"
"fmt"
"log"

tea "github.com/charmbracelet/bubbletea"
"github.com/gitxtui/gitx/internal/tui"
zone "github.com/lrstanley/bubblezone"
)

func main() {
zone.NewGlobal()
defer zone.Close()

app := tui.NewApp()
if err := app.Run(); err != nil {
log.Fatalf("Error running application: %v", err)
if !errors.Is(err, tea.ErrProgramKilled) {
log.Fatalf("Error running application: %v", err)
}
}
fmt.Println("Bye from gitx! :)")
}
20 changes: 13 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ go 1.24

toolchain go1.24.5

require github.com/charmbracelet/lipgloss v1.1.0
require (
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/lipgloss v1.1.0
github.com/fsnotify/fsnotify v1.9.0
github.com/lrstanley/bubblezone v1.0.0
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/colorprofile v0.3.2 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Expand All @@ -19,14 +25,14 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sync v0.16.0 // indirect
)

require (
github.com/charmbracelet/bubbletea v1.3.6
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
)
36 changes: 24 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/lrstanley/bubblezone v1.0.0 h1:bIpUaBilD42rAQwlg/4u5aTqVAt6DSRKYZuSdmkr8UA=
github.com/lrstanley/bubblezone v1.0.0/go.mod h1:kcTekA8HE/0Ll2bWzqHlhA2c513KDNLW7uDfDP4Mly8=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
Expand All @@ -35,11 +47,11 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
168 changes: 168 additions & 0 deletions internal/git/branch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package git

import (
"fmt"
"strings"
)

// Branch represents a git branch with its metadata.
type Branch struct {
Name string
IsCurrent bool
LastCommit string
}

// GetBranches fetches all local branches, their last commit time, and sorts them.
func (g *GitCommands) GetBranches() ([]*Branch, error) {
// This format gives us: <relative_commit_date> <tab> <branch_name> <tab> <is_current_indicator>
format := "%(committerdate:relative)\t%(refname:short)\t%(HEAD)"
args := []string{"for-each-ref", "--sort=-committerdate", "refs/heads/", fmt.Sprintf("--format=%s", format)}

cmd := ExecCommand("git", args...)
output, err := cmd.CombinedOutput()
if err != nil {
return nil, err
}

lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(lines) == 1 && lines[0] == "" {
return []*Branch{}, nil // No branches found
}

var branches []*Branch
var currentBranch *Branch

for _, line := range lines {
parts := strings.Split(line, "\t")
if len(parts) < 2 {
continue
}

isCurrent := len(parts) == 3 && parts[2] == "*"
branch := &Branch{
Name: parts[1],
IsCurrent: isCurrent,
LastCommit: formatRelativeDate(parts[0]),
}

if isCurrent {
currentBranch = branch
} else {
branches = append(branches, branch)
}
}

// Prepend the current branch to the list to ensure it's always at the top.
if currentBranch != nil {
branches = append([]*Branch{currentBranch}, branches...)
}

return branches, nil
}

// formatRelativeDate converts git's "X units ago" to a shorter format.
func formatRelativeDate(dateStr string) string {
parts := strings.Split(dateStr, " ")
if len(parts) < 2 {
return dateStr // Return original if format is unexpected
}

val := parts[0]
unit := parts[1]

switch {
case strings.HasPrefix(unit, "second"):
return val + "s"
case strings.HasPrefix(unit, "minute"):
return val + "m"
case strings.HasPrefix(unit, "hour"):
return val + "h"
case strings.HasPrefix(unit, "day"):
return val + "d"
case strings.HasPrefix(unit, "week"):
return val + "w"
case strings.HasPrefix(unit, "month"):
return val + "M"
case strings.HasPrefix(unit, "year"):
return val + "y"
default:
return dateStr
}
}

// BranchOptions specifies the options for managing branches.
type BranchOptions struct {
Create bool
Delete bool
Name string
}

// ManageBranch creates or deletes branches.
func (g *GitCommands) ManageBranch(options BranchOptions) (string, error) {
args := []string{"branch"}

if options.Delete {
if options.Name == "" {
return "", fmt.Errorf("branch name is required for deletion")
}
args = append(args, "-d", options.Name)
} else if options.Create {
if options.Name == "" {
return "", fmt.Errorf("branch name is required for creation")
}
args = append(args, options.Name)
}

cmd := ExecCommand("git", args...)
output, err := cmd.CombinedOutput()
if err != nil {
return string(output), fmt.Errorf("branch operation failed: %v", err)
}

return string(output), nil
}

// Checkout switches branches or restores working tree files.
func (g *GitCommands) Checkout(branchName string) (string, error) {
if branchName == "" {
return "", fmt.Errorf("branch name is required")
}

cmd := ExecCommand("git", "checkout", branchName)
output, err := cmd.CombinedOutput()
if err != nil {
return string(output), fmt.Errorf("failed to checkout branch: %v", err)
}

return string(output), nil
}

// Switch switches to a specified branch.
func (g *GitCommands) Switch(branchName string) (string, error) {
if branchName == "" {
return "", fmt.Errorf("branch name is required")
}

cmd := ExecCommand("git", "switch", branchName)
output, err := cmd.CombinedOutput()
if err != nil {
return string(output), fmt.Errorf("failed to switch branch: %v", err)
}

return string(output), nil
}

// RenameBranch renames a branch.
func (g *GitCommands) RenameBranch(oldName, newName string) (string, error) {
if oldName == "" || newName == "" {
return "", fmt.Errorf("both old and new branch names are required")
}

cmd := ExecCommand("git", "branch", "-m", oldName, newName)
output, err := cmd.CombinedOutput()
if err != nil {
return string(output), fmt.Errorf("failed to rename branch: %v", err)
}

return string(output), nil
}
Loading