mirror of
https://github.com/yarlson/lnk.git
synced 2025-09-01 18:02:34 +02:00
fix(status): detect and report uncommitted changes in repository status, fixes #5
This commit is contained in:
@@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -424,6 +425,49 @@ func (suite *CLITestSuite) TestSameBasenameFilesBug() {
|
||||
suite.Equal(contentB, string(restoredContentB), "Restored second file should have original content")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestStatusDirtyRepo() {
|
||||
// Initialize repository
|
||||
err := suite.runCommand("init")
|
||||
suite.Require().NoError(err)
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Add and commit a file
|
||||
testFile := filepath.Join(suite.tempDir, "a")
|
||||
err = os.WriteFile(testFile, []byte("abc"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.runCommand("add", testFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Add a remote so status works
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
cmd := exec.Command("git", "remote", "add", "origin", "https://github.com/test/dotfiles.git")
|
||||
cmd.Dir = lnkDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Status should show clean but ahead
|
||||
err = suite.runCommand("status")
|
||||
suite.NoError(err)
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "1 commit ahead")
|
||||
suite.NotContains(output, "uncommitted changes")
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Now edit the managed file (simulating the issue scenario)
|
||||
err = os.WriteFile(testFile, []byte("def"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Status should now detect dirty state and NOT say "up to date"
|
||||
err = suite.runCommand("status")
|
||||
suite.NoError(err)
|
||||
output = suite.stdout.String()
|
||||
suite.Contains(output, "Repository has uncommitted changes")
|
||||
suite.NotContains(output, "Repository is up to date")
|
||||
suite.Contains(output, "lnk push")
|
||||
}
|
||||
|
||||
func TestCLISuite(t *testing.T) {
|
||||
suite.Run(t, new(CLITestSuite))
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ func newStatusCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "📊 Show repository sync status",
|
||||
Long: "Display how many commits ahead/behind the local repository is relative to the remote.",
|
||||
Long: "Display how many commits ahead/behind the local repository is relative to the remote and check for uncommitted changes.",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
lnk := core.NewLnk()
|
||||
@@ -20,37 +20,74 @@ func newStatusCmd() *cobra.Command {
|
||||
return fmt.Errorf("failed to get status: %w", err)
|
||||
}
|
||||
|
||||
if status.Ahead == 0 && status.Behind == 0 {
|
||||
printf(cmd, "✅ \033[1;32mRepository is up to date\033[0m\n")
|
||||
printf(cmd, " 📡 Synced with \033[36m%s\033[0m\n", status.Remote)
|
||||
} else {
|
||||
printf(cmd, "📊 \033[1mRepository Status\033[0m\n")
|
||||
printf(cmd, " 📡 Remote: \033[36m%s\033[0m\n", status.Remote)
|
||||
printf(cmd, "\n")
|
||||
|
||||
if status.Ahead > 0 {
|
||||
commitText := "commit"
|
||||
if status.Ahead > 1 {
|
||||
commitText = "commits"
|
||||
}
|
||||
printf(cmd, " ⬆️ \033[1;33m%d %s ahead\033[0m - ready to push\n", status.Ahead, commitText)
|
||||
}
|
||||
if status.Behind > 0 {
|
||||
commitText := "commit"
|
||||
if status.Behind > 1 {
|
||||
commitText = "commits"
|
||||
}
|
||||
printf(cmd, " ⬇️ \033[1;31m%d %s behind\033[0m - run \033[1mlnk pull\033[0m\n", status.Behind, commitText)
|
||||
}
|
||||
|
||||
if status.Ahead > 0 && status.Behind == 0 {
|
||||
printf(cmd, "\n💡 Run \033[1mlnk push\033[0m to sync your changes")
|
||||
} else if status.Behind > 0 {
|
||||
printf(cmd, "\n💡 Run \033[1mlnk pull\033[0m to get latest changes")
|
||||
}
|
||||
if status.Dirty {
|
||||
displayDirtyStatus(cmd, status)
|
||||
return nil
|
||||
}
|
||||
|
||||
if status.Ahead == 0 && status.Behind == 0 {
|
||||
displayUpToDateStatus(cmd, status)
|
||||
return nil
|
||||
}
|
||||
|
||||
displaySyncStatus(cmd, status)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func displayDirtyStatus(cmd *cobra.Command, status *core.StatusInfo) {
|
||||
printf(cmd, "⚠️ \033[1;33mRepository has uncommitted changes\033[0m\n")
|
||||
printf(cmd, " 📡 Remote: \033[36m%s\033[0m\n", status.Remote)
|
||||
|
||||
if status.Ahead == 0 && status.Behind == 0 {
|
||||
printf(cmd, "\n💡 Run \033[1mgit add && git commit\033[0m in \033[36m~/.config/lnk\033[0m or \033[1mlnk push\033[0m to commit changes\n")
|
||||
return
|
||||
}
|
||||
|
||||
printf(cmd, "\n")
|
||||
displayAheadBehindInfo(cmd, status, true)
|
||||
printf(cmd, "\n💡 Run \033[1mgit add && git commit\033[0m in \033[36m~/.config/lnk\033[0m or \033[1mlnk push\033[0m to commit changes\n")
|
||||
}
|
||||
|
||||
func displayUpToDateStatus(cmd *cobra.Command, status *core.StatusInfo) {
|
||||
printf(cmd, "✅ \033[1;32mRepository is up to date\033[0m\n")
|
||||
printf(cmd, " 📡 Synced with \033[36m%s\033[0m\n", status.Remote)
|
||||
}
|
||||
|
||||
func displaySyncStatus(cmd *cobra.Command, status *core.StatusInfo) {
|
||||
printf(cmd, "📊 \033[1mRepository Status\033[0m\n")
|
||||
printf(cmd, " 📡 Remote: \033[36m%s\033[0m\n", status.Remote)
|
||||
printf(cmd, "\n")
|
||||
|
||||
displayAheadBehindInfo(cmd, status, false)
|
||||
|
||||
if status.Ahead > 0 && status.Behind == 0 {
|
||||
printf(cmd, "\n💡 Run \033[1mlnk push\033[0m to sync your changes\n")
|
||||
} else if status.Behind > 0 {
|
||||
printf(cmd, "\n💡 Run \033[1mlnk pull\033[0m to get latest changes\n")
|
||||
}
|
||||
}
|
||||
|
||||
func displayAheadBehindInfo(cmd *cobra.Command, status *core.StatusInfo, isDirty bool) {
|
||||
if status.Ahead > 0 {
|
||||
commitText := getCommitText(status.Ahead)
|
||||
if isDirty {
|
||||
printf(cmd, " ⬆️ \033[1;33m%d %s ahead\033[0m (excluding uncommitted changes)\n", status.Ahead, commitText)
|
||||
} else {
|
||||
printf(cmd, " ⬆️ \033[1;33m%d %s ahead\033[0m - ready to push\n", status.Ahead, commitText)
|
||||
}
|
||||
}
|
||||
|
||||
if status.Behind > 0 {
|
||||
commitText := getCommitText(status.Behind)
|
||||
printf(cmd, " ⬇️ \033[1;31m%d %s behind\033[0m - run \033[1mlnk pull\033[0m\n", status.Behind, commitText)
|
||||
}
|
||||
}
|
||||
|
||||
func getCommitText(count int) string {
|
||||
if count == 1 {
|
||||
return "commit"
|
||||
}
|
||||
return "commits"
|
||||
}
|
||||
|
@@ -350,6 +350,7 @@ type StatusInfo struct {
|
||||
Ahead int
|
||||
Behind int
|
||||
Remote string
|
||||
Dirty bool
|
||||
}
|
||||
|
||||
// Status returns the repository sync status
|
||||
@@ -368,6 +369,7 @@ func (l *Lnk) Status() (*StatusInfo, error) {
|
||||
Ahead: gitStatus.Ahead,
|
||||
Behind: gitStatus.Behind,
|
||||
Remote: gitStatus.Remote,
|
||||
Dirty: gitStatus.Dirty,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@@ -480,6 +480,42 @@ func (suite *CoreTestSuite) TestSameBasenameSequentialAdd() {
|
||||
suite.Require().NoError(err, "Second .bashrc should be removable")
|
||||
}
|
||||
|
||||
// Test dirty repository status detection
|
||||
func (suite *CoreTestSuite) TestStatusDetectsDirtyRepo() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add and commit a file
|
||||
testFile := filepath.Join(suite.tempDir, "a")
|
||||
err = os.WriteFile(testFile, []byte("abc"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Add(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add a remote so status works
|
||||
err = suite.lnk.AddRemote("origin", "https://github.com/test/dotfiles.git")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check status - should be clean but ahead of remote
|
||||
status, err := suite.lnk.Status()
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(1, status.Ahead)
|
||||
suite.Equal(0, status.Behind)
|
||||
suite.False(status.Dirty, "Repository should not be dirty after commit")
|
||||
|
||||
// Now edit the managed file (simulating the issue scenario)
|
||||
err = os.WriteFile(testFile, []byte("def"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check status again - should detect dirty state
|
||||
status, err = suite.lnk.Status()
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(1, status.Ahead)
|
||||
suite.Equal(0, status.Behind)
|
||||
suite.True(status.Dirty, "Repository should be dirty after editing managed file")
|
||||
}
|
||||
|
||||
func TestCoreSuite(t *testing.T) {
|
||||
suite.Run(t, new(CoreTestSuite))
|
||||
}
|
||||
|
@@ -305,6 +305,7 @@ type StatusInfo struct {
|
||||
Ahead int
|
||||
Behind int
|
||||
Remote string
|
||||
Dirty bool
|
||||
}
|
||||
|
||||
// GetStatus returns the repository status relative to remote
|
||||
@@ -315,6 +316,12 @@ func (g *Git) GetStatus() (*StatusInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check for uncommitted changes
|
||||
dirty, err := g.HasChanges()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check for uncommitted changes: %w", err)
|
||||
}
|
||||
|
||||
// Get the remote tracking branch
|
||||
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
|
||||
cmd.Dir = g.repoPath
|
||||
@@ -327,6 +334,7 @@ func (g *Git) GetStatus() (*StatusInfo, error) {
|
||||
Ahead: g.getAheadCount(remoteBranch),
|
||||
Behind: 0, // Can't be behind if no upstream
|
||||
Remote: remoteBranch,
|
||||
Dirty: dirty,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -336,6 +344,7 @@ func (g *Git) GetStatus() (*StatusInfo, error) {
|
||||
Ahead: g.getAheadCount(remoteBranch),
|
||||
Behind: g.getBehindCount(remoteBranch),
|
||||
Remote: remoteBranch,
|
||||
Dirty: dirty,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user