Skip to content

Commit 3ad64fb

Browse files
feat: add help panel and show minimal controls by default
1 parent 3c59b63 commit 3ad64fb

File tree

7 files changed

+133
-102
lines changed

7 files changed

+133
-102
lines changed

internal/git/git.go

Lines changed: 68 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,48 @@ import (
77
"strings"
88
)
99

10-
// FileItem representa un archivo en el repositorio
10+
// FileItem represents a file in the repository, including its status and staging information.
11+
// Name is the file path, Status indicates the Git status, Staged is true if the file is staged,
12+
// and Selected can be used by UI clients to mark the file.
1113
type FileItem struct {
1214
Name string
1315
Status string
1416
Staged bool
1517
Selected bool
1618
}
1719

18-
// IsGitRepository verifica si el directorio actual es un repositorio Git
20+
// IsGitRepository checks if the current directory is within a Git repository.
21+
// It runs 'git rev-parse --git-dir' and returns true if no error occurs.
1922
func IsGitRepository() bool {
2023
_, err := exec.Command("git", "rev-parse", "--git-dir").Output()
2124
return err == nil
2225
}
2326

24-
// InitRepository inicializa un nuevo repositorio Git
27+
// InitRepository initializes a new Git repository in the current directory.
28+
// It runs 'git init' and returns any execution error.
2529
func InitRepository() error {
2630
cmd := exec.Command("git", "init")
2731
return cmd.Run()
2832
}
2933

30-
// GetModifiedFiles obtiene la lista de archivos modificados
34+
// GetModifiedFiles returns a slice of FileItem for all modified files in the working tree.
35+
// It detects staged files via 'git diff --cached --name-status' and all changes via 'git status --porcelain'.
3136
func GetModifiedFiles() ([]FileItem, error) {
32-
// Obtener archivos staged
33-
stagedCmd := exec.Command("git", "diff", "--cached", "--name-status")
34-
stagedOutput, _ := stagedCmd.Output()
37+
// Map of filenames that are staged
3538
stagedFiles := make(map[string]bool)
36-
39+
stagedOutput, _ := exec.Command("git", "diff", "--cached", "--name-status").Output()
3740
if len(stagedOutput) > 0 {
3841
lines := strings.Split(strings.TrimSpace(string(stagedOutput)), "\n")
3942
for _, line := range lines {
40-
if line != "" {
41-
parts := strings.Fields(line)
42-
if len(parts) >= 2 {
43-
stagedFiles[parts[1]] = true
44-
}
43+
parts := strings.Fields(line)
44+
if len(parts) >= 2 {
45+
stagedFiles[parts[1]] = true
4546
}
4647
}
4748
}
4849

49-
// Obtener todos los archivos modificados
50-
cmd := exec.Command("git", "status", "--porcelain")
51-
output, err := cmd.Output()
50+
// Get status of all modified files
51+
output, err := exec.Command("git", "status", "--porcelain").Output()
5252
if err != nil {
5353
return nil, err
5454
}
@@ -61,12 +61,10 @@ func GetModifiedFiles() ([]FileItem, error) {
6161
continue
6262
}
6363

64-
// status puede estar en la primera o segunda columna
6564
status := strings.TrimSpace(line[:2])
6665
filename := strings.TrimSpace(line[3:])
6766

68-
// Algunos nombres de archivo pueden contener espacios. Usa Fields para mayor precisión.
69-
// Nota: git status --porcelain v1 separa status y nombre con exactamente dos caracteres.
67+
// Handle filenames containing spaces
7068
if fields := strings.Fields(line); len(fields) >= 2 {
7169
filename = strings.Join(fields[1:], " ")
7270
}
@@ -82,17 +80,16 @@ func GetModifiedFiles() ([]FileItem, error) {
8280
return files, nil
8381
}
8482

85-
// GetBranches obtiene la lista de ramas y la rama actual
83+
// GetBranches returns a slice of branch names and the currently checked-out branch.
84+
// It runs 'git branch' and parses the output.
8685
func GetBranches() ([]string, string) {
87-
cmd := exec.Command("git", "branch")
88-
output, err := cmd.Output()
86+
output, err := exec.Command("git", "branch").Output()
8987
if err != nil {
90-
return []string{}, ""
88+
return nil, ""
9189
}
9290

9391
var branches []string
9492
var current string
95-
9693
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
9794
for _, line := range lines {
9895
line = strings.TrimSpace(line)
@@ -107,143 +104,120 @@ func GetBranches() ([]string, string) {
107104
return branches, current
108105
}
109106

110-
// GetRemotes obtiene la lista de remotes configurados
107+
// GetRemotes retrieves configured Git remotes and their URLs.
108+
// It runs 'git remote -v' and returns unique remotes.
111109
func GetRemotes() ([]string, error) {
112-
cmd := exec.Command("git", "remote", "-v")
113-
output, err := cmd.Output()
110+
output, err := exec.Command("git", "remote", "-v").Output()
114111
if err != nil {
115112
return nil, err
116113
}
117114

118115
var remotes []string
119-
if len(output) > 0 {
120-
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
121-
seen := make(map[string]bool)
122-
for _, line := range lines {
123-
if line != "" {
124-
parts := strings.Fields(line)
125-
if len(parts) >= 2 && !seen[parts[0]] {
126-
remotes = append(remotes, parts[0]+" -> "+parts[1])
127-
seen[parts[0]] = true
128-
}
129-
}
116+
seen := make(map[string]bool)
117+
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
118+
for _, line := range lines {
119+
parts := strings.Fields(line)
120+
if len(parts) >= 2 && !seen[parts[0]] {
121+
remotes = append(remotes, fmt.Sprintf("%s -> %s", parts[0], parts[1]))
122+
seen[parts[0]] = true
130123
}
131124
}
132125

133126
return remotes, nil
134127
}
135128

136-
// Add añade un archivo al staging area
129+
// Add stages the specified file using 'git add'.
137130
func Add(filename string) error {
138-
cmd := exec.Command("git", "add", filename)
139-
return cmd.Run()
131+
return exec.Command("git", "add", filename).Run()
140132
}
141133

142-
// Reset quita un archivo del staging area
134+
// Reset un-stages the specified file using 'git reset HEAD'.
143135
func Reset(filename string) error {
144-
cmd := exec.Command("git", "reset", "HEAD", filename)
145-
return cmd.Run()
136+
return exec.Command("git", "reset", "HEAD", filename).Run()
146137
}
147138

148-
// Commit realiza un commit con el mensaje especificado
139+
// Commit creates a new commit with the given message using 'git commit -m'.
149140
func Commit(message string) error {
150-
cmd := exec.Command("git", "commit", "-m", message)
151-
return cmd.Run()
141+
return exec.Command("git", "commit", "-m", message).Run()
152142
}
153143

154-
// Push sube los cambios al repositorio remoto
144+
// Push sends committed changes to the remote repository using 'git push'.
155145
func Push() error {
156-
cmd := exec.Command("git", "push")
157-
return cmd.Run()
146+
return exec.Command("git", "push").Run()
158147
}
159148

160-
// Checkout cambia a la rama especificada
149+
// Checkout switches to the specified branch using 'git checkout'.
161150
func Checkout(branch string) error {
162-
cmd := exec.Command("git", "checkout", branch)
163-
return cmd.Run()
151+
return exec.Command("git", "checkout", branch).Run()
164152
}
165153

166-
// AddRemote añade un remote al repositorio
154+
// AddRemote adds a new remote with the given name and URL using 'git remote add'.
167155
func AddRemote(name, url string) error {
168-
cmd := exec.Command("git", "remote", "add", name, url)
169-
return cmd.Run()
156+
return exec.Command("git", "remote", "add", name, url).Run()
170157
}
171158

172-
// RemoveRemote elimina un remote del repositorio
159+
// RemoveRemote removes the specified remote using 'git remote remove'.
173160
func RemoveRemote(name string) error {
174-
cmd := exec.Command("git", "remote", "remove", name)
175-
return cmd.Run()
161+
return exec.Command("git", "remote", "remove", name).Run()
176162
}
177163

178-
// Fetch obtiene todos los cambios y ramas del repositorio remoto
164+
// Fetch retrieves all updates from the remote repository using 'git fetch --all'.
179165
func Fetch() error {
180-
output, err := exec.Command("git", "fetch", "-a").CombinedOutput()
166+
output, err := exec.Command("git", "fetch", "--all").CombinedOutput()
181167
if err != nil {
182-
return fmt.Errorf("error al hacer fetch: %v - %s", err, string(output))
168+
return fmt.Errorf("fetch failed: %v - %s", err, string(output))
183169
}
184170
return nil
185171
}
186172

187-
// Pull obtiene e integra los cambios del repositorio remoto
173+
// Pull fetches and integrates changes from the remote repository using 'git pull'.
188174
func Pull() error {
189175
output, err := exec.Command("git", "pull").CombinedOutput()
190176
if err != nil {
191-
return fmt.Errorf("error al hacer pull: %v - %s", err, string(output))
177+
return fmt.Errorf("pull failed: %v - %s", err, string(output))
192178
}
193179
return nil
194180
}
195181

196-
// CreateBranch crea una nueva rama
182+
// CreateBranch creates and checks out a new branch using 'git checkout -b'.
197183
func CreateBranch(name string) error {
198-
cmd := exec.Command("git", "checkout", "-b", name)
199-
return cmd.Run()
184+
return exec.Command("git", "checkout", "-b", name).Run()
200185
}
201186

202-
// DeleteBranch elimina una rama
187+
// DeleteBranch deletes the specified branch using 'git branch -d'.
203188
func DeleteBranch(name string) error {
204-
cmd := exec.Command("git", "branch", "-d", name)
205-
return cmd.Run()
189+
return exec.Command("git", "branch", "-d", name).Run()
206190
}
207191

208-
// DiscardChanges descarta los cambios de un archivo
192+
// DiscardChanges reverts changes to the specified file.
193+
// If the file is untracked, it is removed; otherwise, changes are reset using 'git checkout --'.
209194
func DiscardChanges(filename string) error {
210-
// Verificar si el archivo está siendo rastreado por Git
211-
checkTracked := exec.Command("git", "ls-files", "--error-unmatch", filename)
212-
if err := checkTracked.Run(); err != nil {
213-
// El archivo NO está siendo rastreado, lo eliminamos
214-
fmt.Println("Archivo NO rastreado, intentando eliminar:", filename)
215-
if err := os.Remove(filename); err != nil {
216-
return fmt.Errorf("no se pudo eliminar archivo no rastreado: %w", err)
195+
// Check if file is tracked
196+
if err := exec.Command("git", "ls-files", "--error-unmatch", filename).Run(); err != nil {
197+
// File is untracked: remove it
198+
if removeErr := os.Remove(filename); removeErr != nil {
199+
return fmt.Errorf("failed to remove untracked file: %w", removeErr)
217200
}
218-
fmt.Println("Archivo eliminado correctamente")
219201
return nil
220202
}
221203

222-
// El archivo está rastreado, se descartan los cambios
223-
fmt.Println("Archivo rastreado, descartando cambios con git checkout")
224-
discard := exec.Command("git", "checkout", "--", filename)
225-
if err := discard.Run(); err != nil {
226-
return fmt.Errorf("error al descartar cambios con git: %w", err)
204+
// File is tracked: discard changes
205+
if err := exec.Command("git", "checkout", "--", filename).Run(); err != nil {
206+
return fmt.Errorf("failed to discard changes: %w", err)
227207
}
228-
229-
fmt.Println("Cambios descartados correctamente")
230208
return nil
231209
}
232210

233-
// HasRemoteChanges verifica si hay commits pendientes de pull desde el remoto
211+
// HasRemoteChanges checks if the local branch is behind its remote counterpart.
212+
// It fetches updates and counts commits between HEAD and origin/branch.
234213
func HasRemoteChanges(branch string) (bool, error) {
235-
// Ejecuta git fetch para actualizar refs
236-
err := Fetch()
237-
if err != nil {
214+
if err := Fetch(); err != nil {
238215
return false, err
239216
}
240-
241-
cmd := exec.Command("git", "rev-list", "--count", fmt.Sprintf("HEAD..origin/%s", branch))
242-
output, err := cmd.Output()
217+
output, err := exec.Command("git", "rev-list", "--count", fmt.Sprintf("HEAD..origin/%s", branch)).Output()
243218
if err != nil {
244219
return false, err
245220
}
246-
247221
count := strings.TrimSpace(string(output))
248222
return count != "0", nil
249223
}

internal/tui/model/model.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const (
1717
AddRemoteView
1818
NewBranchView
1919
ConfirmDialog
20+
HelpView
2021
)
2122

2223
type Model struct {
@@ -39,6 +40,7 @@ type Model struct {
3940
IsPulling bool
4041
NewBranchName string
4142
HasRemoteChanges bool
43+
ShowHelpPanel bool
4244

4345
DialogType string
4446
DialogTarget string
@@ -70,6 +72,7 @@ func InitialModel() Model {
7072
IsPulling: false,
7173
NewBranchName: "",
7274
HasRemoteChanges: hasRemoteChanges,
75+
ShowHelpPanel: false,
7376
DialogType: "",
7477
DialogTarget: "",
7578
}

internal/tui/render.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
func Render(m model.Model) string {
1616
var sb strings.Builder
1717

18-
// Título
1918
sb.WriteString(styles.TitleStyle.Render(branding.RenderTitle()) + "\n\n")
2019
sb.WriteString(fmt.Sprintf(" current branch: %s\n\n",
2120
styles.HeaderStyle.Render(m.CurrentBranch),
@@ -37,9 +36,10 @@ func Render(m model.Model) string {
3736
sb.WriteString(view.RenderNewBranchView(m))
3837
case model.ConfirmDialog:
3938
sb.WriteString(view.RenderConfirmDialog(m))
39+
case model.HelpView:
40+
sb.WriteString(view.RenderHelpView())
4041
}
4142

42-
// Mensajes de estado
4343
if m.Message != "" {
4444
sb.WriteString("\n")
4545
switch m.MessageType {

internal/tui/styles/styles.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,9 @@ var (
117117
Foreground(lipgloss.Color(LightGray)).
118118
Background(lipgloss.Color("235")).
119119
Padding(0, 1).
120-
Width(100) // Ajustar según el ancho de tu terminal
120+
Width(100)
121121

122-
// Para el layout horizontal
123122
MainContentStyle = lipgloss.NewStyle().
124123
Width(80).
125-
Height(25) // Ajustar según la altura de tu terminal
124+
Height(25)
126125
)

internal/tui/update/update.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func Update(m model.Model, msg tea.Msg) (model.Model, tea.Cmd) {
9393
// Allow exiting with "q" only when we are NOT in an input-focused view.
9494
// When the user is typing (e.g. commit message, new branch name, adding remotes)
9595
// the character should be treated as normal text instead of a quit signal.
96-
if m.CurrentView == model.FileView || m.CurrentView == model.BranchView || m.CurrentView == model.RemoteView || m.CurrentView == model.ConfirmDialog {
96+
if m.CurrentView == model.FileView || m.CurrentView == model.BranchView || m.CurrentView == model.RemoteView || m.CurrentView == model.ConfirmDialog || m.CurrentView == model.HelpView {
9797
return m, tea.Quit
9898
}
9999
// Otherwise, fall through so the rune is processed as normal input.
@@ -313,12 +313,24 @@ func Update(m model.Model, msg tea.Msg) (model.Model, tea.Cmd) {
313313
m.MessageType = "info"
314314
return m, tea.Batch(performPull(), spinner())
315315
}
316+
case "A":
317+
m.Message = "Advanced features (logs, merge, stash, rebase) are coming soon"
318+
m.MessageType = "info"
319+
return m, nil
316320
case "x":
317321
if len(m.Files) > 0 {
318322
m.DialogType = "discard_changes"
319323
m.DialogTarget = m.Files[m.Cursor].Name
320324
m.CurrentView = model.ConfirmDialog
321325
}
326+
case "?":
327+
if m.CurrentView == model.FileView {
328+
m.CurrentView = model.HelpView
329+
return m, nil
330+
} else if m.CurrentView == model.HelpView {
331+
m.CurrentView = model.FileView
332+
return m, nil
333+
}
322334
}
323335
}
324336

0 commit comments

Comments
 (0)