mirror of
https://github.com/yarlson/lnk.git
synced 2025-09-24 21:11:27 +02:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4a275ce4ca | ||
|
69c1038f3e | ||
|
c670ac1fd8 | ||
|
27196e3341 | ||
|
84c507828d | ||
|
d02f112200 |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -6,6 +6,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@@ -52,6 +52,9 @@ lnk init -r git@github.com:user/dotfiles.git
|
|||||||
# Add files/directories
|
# Add files/directories
|
||||||
lnk add ~/.vimrc ~/.config/nvim ~/.gitconfig
|
lnk add ~/.vimrc ~/.config/nvim ~/.gitconfig
|
||||||
|
|
||||||
|
# List managed files
|
||||||
|
lnk list
|
||||||
|
|
||||||
# Check status
|
# Check status
|
||||||
lnk status
|
lnk status
|
||||||
|
|
||||||
@@ -99,6 +102,7 @@ lnk pull # auto-creates symlinks
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
vim ~/.vimrc # edit normally
|
vim ~/.vimrc # edit normally
|
||||||
|
lnk list # see what's managed
|
||||||
lnk status # check what changed
|
lnk status # check what changed
|
||||||
lnk push "new plugins" # commit & push
|
lnk push "new plugins" # commit & push
|
||||||
```
|
```
|
||||||
@@ -108,6 +112,7 @@ lnk push "new plugins" # commit & push
|
|||||||
- `lnk init [-r remote]` - Create repo
|
- `lnk init [-r remote]` - Create repo
|
||||||
- `lnk add <files>` - Move files to repo, create symlinks
|
- `lnk add <files>` - Move files to repo, create symlinks
|
||||||
- `lnk rm <files>` - Move files back, remove symlinks
|
- `lnk rm <files>` - Move files back, remove symlinks
|
||||||
|
- `lnk list` - List files managed by lnk
|
||||||
- `lnk status` - Git status + sync info
|
- `lnk status` - Git status + sync info
|
||||||
- `lnk push [msg]` - Stage all, commit, push
|
- `lnk push [msg]` - Stage all, commit, push
|
||||||
- `lnk pull` - Pull + restore missing symlinks
|
- `lnk pull` - Pull + restore missing symlinks
|
||||||
@@ -115,10 +120,8 @@ lnk push "new plugins" # commit & push
|
|||||||
## Technical bits
|
## Technical bits
|
||||||
|
|
||||||
- **Single binary** (~8MB, no deps)
|
- **Single binary** (~8MB, no deps)
|
||||||
- **Atomic operations** (rollback on failure)
|
|
||||||
- **Relative symlinks** (portable)
|
- **Relative symlinks** (portable)
|
||||||
- **XDG compliant** (`~/.config/lnk`)
|
- **XDG compliant** (`~/.config/lnk`)
|
||||||
- **20 integration tests**
|
|
||||||
|
|
||||||
## Alternatives
|
## Alternatives
|
||||||
|
|
||||||
|
43
cmd/list.go
Normal file
43
cmd/list.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/yarlson/lnk/internal/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newListCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "📋 List files managed by lnk",
|
||||||
|
Long: "Display all files and directories currently managed by lnk.",
|
||||||
|
SilenceUsage: true,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
lnk := core.NewLnk()
|
||||||
|
managedItems, err := lnk.List()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list managed items: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(managedItems) == 0 {
|
||||||
|
printf(cmd, "📋 \033[1mNo files currently managed by lnk\033[0m\n")
|
||||||
|
printf(cmd, " 💡 Use \033[1mlnk add <file>\033[0m to start managing files\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(cmd, "📋 \033[1mFiles managed by lnk\033[0m (\033[36m%d item", len(managedItems))
|
||||||
|
if len(managedItems) > 1 {
|
||||||
|
printf(cmd, "s")
|
||||||
|
}
|
||||||
|
printf(cmd, "\033[0m):\n\n")
|
||||||
|
|
||||||
|
for _, item := range managedItems {
|
||||||
|
printf(cmd, " 🔗 \033[36m%s\033[0m\n", item)
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(cmd, "\n💡 Use \033[1mlnk status\033[0m to check sync status\n")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@@ -38,6 +38,7 @@ That's it.
|
|||||||
rootCmd.AddCommand(newInitCmd())
|
rootCmd.AddCommand(newInitCmd())
|
||||||
rootCmd.AddCommand(newAddCmd())
|
rootCmd.AddCommand(newAddCmd())
|
||||||
rootCmd.AddCommand(newRemoveCmd())
|
rootCmd.AddCommand(newRemoveCmd())
|
||||||
|
rootCmd.AddCommand(newListCmd())
|
||||||
rootCmd.AddCommand(newStatusCmd())
|
rootCmd.AddCommand(newStatusCmd())
|
||||||
rootCmd.AddCommand(newPushCmd())
|
rootCmd.AddCommand(newPushCmd())
|
||||||
rootCmd.AddCommand(newPullCmd())
|
rootCmd.AddCommand(newPullCmd())
|
||||||
|
104
cmd/root_test.go
104
cmd/root_test.go
@@ -3,6 +3,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -162,6 +163,60 @@ func (suite *CLITestSuite) TestStatusCommand() {
|
|||||||
suite.Contains(err.Error(), "no remote configured")
|
suite.Contains(err.Error(), "no remote configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *CLITestSuite) TestListCommand() {
|
||||||
|
// Test list without init - should fail
|
||||||
|
err := suite.runCommand("list")
|
||||||
|
suite.Error(err)
|
||||||
|
suite.Contains(err.Error(), "Lnk repository not initialized")
|
||||||
|
|
||||||
|
// Initialize first
|
||||||
|
err = suite.runCommand("init")
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Test list with no managed files
|
||||||
|
err = suite.runCommand("list")
|
||||||
|
suite.NoError(err)
|
||||||
|
output := suite.stdout.String()
|
||||||
|
suite.Contains(output, "No files currently managed by lnk")
|
||||||
|
suite.Contains(output, "lnk add <file>")
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Add a file
|
||||||
|
testFile := filepath.Join(suite.tempDir, ".bashrc")
|
||||||
|
err = os.WriteFile(testFile, []byte("export PATH=/usr/local/bin:$PATH"), 0644)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
err = suite.runCommand("add", testFile)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Test list with one managed file
|
||||||
|
err = suite.runCommand("list")
|
||||||
|
suite.NoError(err)
|
||||||
|
output = suite.stdout.String()
|
||||||
|
suite.Contains(output, "Files managed by lnk")
|
||||||
|
suite.Contains(output, "1 item")
|
||||||
|
suite.Contains(output, ".bashrc")
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Add another file
|
||||||
|
testFile2 := filepath.Join(suite.tempDir, ".vimrc")
|
||||||
|
err = os.WriteFile(testFile2, []byte("set number"), 0644)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
err = suite.runCommand("add", testFile2)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Test list with multiple managed files
|
||||||
|
err = suite.runCommand("list")
|
||||||
|
suite.NoError(err)
|
||||||
|
output = suite.stdout.String()
|
||||||
|
suite.Contains(output, "Files managed by lnk")
|
||||||
|
suite.Contains(output, "2 items")
|
||||||
|
suite.Contains(output, ".bashrc")
|
||||||
|
suite.Contains(output, ".vimrc")
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *CLITestSuite) TestErrorHandling() {
|
func (suite *CLITestSuite) TestErrorHandling() {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -206,6 +261,12 @@ func (suite *CLITestSuite) TestErrorHandling() {
|
|||||||
wantErr: false,
|
wantErr: false,
|
||||||
outContains: "Moves a file to the lnk repository",
|
outContains: "Moves a file to the lnk repository",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "list help",
|
||||||
|
args: []string{"list", "--help"},
|
||||||
|
wantErr: false,
|
||||||
|
outContains: "Display all files and directories",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -424,6 +485,49 @@ func (suite *CLITestSuite) TestSameBasenameFilesBug() {
|
|||||||
suite.Equal(contentB, string(restoredContentB), "Restored second file should have original content")
|
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) {
|
func TestCLISuite(t *testing.T) {
|
||||||
suite.Run(t, new(CLITestSuite))
|
suite.Run(t, new(CLITestSuite))
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ func newStatusCmd() *cobra.Command {
|
|||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "status",
|
Use: "status",
|
||||||
Short: "📊 Show repository sync 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,
|
SilenceUsage: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
lnk := core.NewLnk()
|
lnk := core.NewLnk()
|
||||||
@@ -20,37 +20,74 @@ func newStatusCmd() *cobra.Command {
|
|||||||
return fmt.Errorf("failed to get status: %w", err)
|
return fmt.Errorf("failed to get status: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.Ahead == 0 && status.Behind == 0 {
|
if status.Dirty {
|
||||||
printf(cmd, "✅ \033[1;32mRepository is up to date\033[0m\n")
|
displayDirtyStatus(cmd, status)
|
||||||
printf(cmd, " 📡 Synced with \033[36m%s\033[0m\n", status.Remote)
|
return nil
|
||||||
} 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.Ahead == 0 && status.Behind == 0 {
|
||||||
|
displayUpToDateStatus(cmd, status)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
displaySyncStatus(cmd, status)
|
||||||
return nil
|
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"
|
||||||
|
}
|
||||||
|
@@ -69,9 +69,6 @@ func getRelativePath(absPath string) (string, error) {
|
|||||||
if strings.HasPrefix(relPath, "..") {
|
if strings.HasPrefix(relPath, "..") {
|
||||||
// Use absolute path but remove leading slash and drive letter (for cross-platform)
|
// Use absolute path but remove leading slash and drive letter (for cross-platform)
|
||||||
cleanPath := strings.TrimPrefix(absPath, "/")
|
cleanPath := strings.TrimPrefix(absPath, "/")
|
||||||
if len(cleanPath) > 1 && cleanPath[1] == ':' {
|
|
||||||
// Windows drive letter, keep as is
|
|
||||||
}
|
|
||||||
return cleanPath, nil
|
return cleanPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,6 +350,7 @@ type StatusInfo struct {
|
|||||||
Ahead int
|
Ahead int
|
||||||
Behind int
|
Behind int
|
||||||
Remote string
|
Remote string
|
||||||
|
Dirty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status returns the repository sync status
|
// Status returns the repository sync status
|
||||||
@@ -371,6 +369,7 @@ func (l *Lnk) Status() (*StatusInfo, error) {
|
|||||||
Ahead: gitStatus.Ahead,
|
Ahead: gitStatus.Ahead,
|
||||||
Behind: gitStatus.Behind,
|
Behind: gitStatus.Behind,
|
||||||
Remote: gitStatus.Remote,
|
Remote: gitStatus.Remote,
|
||||||
|
Dirty: gitStatus.Dirty,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,6 +428,22 @@ func (l *Lnk) Pull() ([]string, error) {
|
|||||||
return restored, nil
|
return restored, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns the list of files and directories currently managed by lnk
|
||||||
|
func (l *Lnk) List() ([]string, error) {
|
||||||
|
// Check if repository is initialized
|
||||||
|
if !l.git.IsGitRepository() {
|
||||||
|
return nil, fmt.Errorf("❌ Lnk repository not initialized\n 💡 Run \033[1mlnk init\033[0m first")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get managed items from .lnk file
|
||||||
|
managedItems, err := l.getManagedItems()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get managed items: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return managedItems, nil
|
||||||
|
}
|
||||||
|
|
||||||
// RestoreSymlinks finds all managed items from .lnk file and ensures they have proper symlinks
|
// RestoreSymlinks finds all managed items from .lnk file and ensures they have proper symlinks
|
||||||
func (l *Lnk) RestoreSymlinks() ([]string, error) {
|
func (l *Lnk) RestoreSymlinks() ([]string, error) {
|
||||||
var restored []string
|
var restored []string
|
||||||
|
@@ -480,6 +480,112 @@ func (suite *CoreTestSuite) TestSameBasenameSequentialAdd() {
|
|||||||
suite.Require().NoError(err, "Second .bashrc should be removable")
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test list functionality
|
||||||
|
func (suite *CoreTestSuite) TestListManagedItems() {
|
||||||
|
// Test list without init - should fail
|
||||||
|
_, err := suite.lnk.List()
|
||||||
|
suite.Error(err)
|
||||||
|
suite.Contains(err.Error(), "Lnk repository not initialized")
|
||||||
|
|
||||||
|
// Initialize repository
|
||||||
|
err = suite.lnk.Init()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Test list with no managed files
|
||||||
|
items, err := suite.lnk.List()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.Empty(items)
|
||||||
|
|
||||||
|
// Add a file
|
||||||
|
testFile := filepath.Join(suite.tempDir, ".bashrc")
|
||||||
|
content := "export PATH=$PATH:/usr/local/bin"
|
||||||
|
err = os.WriteFile(testFile, []byte(content), 0644)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
err = suite.lnk.Add(testFile)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Test list with one managed file
|
||||||
|
items, err = suite.lnk.List()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.Len(items, 1)
|
||||||
|
suite.Contains(items[0], ".bashrc")
|
||||||
|
|
||||||
|
// Add a directory
|
||||||
|
testDir := filepath.Join(suite.tempDir, ".config")
|
||||||
|
err = os.MkdirAll(testDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
configFile := filepath.Join(testDir, "app.conf")
|
||||||
|
err = os.WriteFile(configFile, []byte("setting=value"), 0644)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
err = suite.lnk.Add(testDir)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Test list with multiple managed items
|
||||||
|
items, err = suite.lnk.List()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.Len(items, 2)
|
||||||
|
|
||||||
|
// Check that both items are present
|
||||||
|
found := make(map[string]bool)
|
||||||
|
for _, item := range items {
|
||||||
|
if strings.Contains(item, ".bashrc") {
|
||||||
|
found[".bashrc"] = true
|
||||||
|
}
|
||||||
|
if strings.Contains(item, ".config") {
|
||||||
|
found[".config"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suite.True(found[".bashrc"], "Should contain .bashrc")
|
||||||
|
suite.True(found[".config"], "Should contain .config")
|
||||||
|
|
||||||
|
// Remove one item and verify list is updated
|
||||||
|
err = suite.lnk.Remove(testFile)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
items, err = suite.lnk.List()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.Len(items, 1)
|
||||||
|
suite.Contains(items[0], ".config")
|
||||||
|
}
|
||||||
|
|
||||||
func TestCoreSuite(t *testing.T) {
|
func TestCoreSuite(t *testing.T) {
|
||||||
suite.Run(t, new(CoreTestSuite))
|
suite.Run(t, new(CoreTestSuite))
|
||||||
}
|
}
|
||||||
|
@@ -305,6 +305,7 @@ type StatusInfo struct {
|
|||||||
Ahead int
|
Ahead int
|
||||||
Behind int
|
Behind int
|
||||||
Remote string
|
Remote string
|
||||||
|
Dirty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStatus returns the repository status relative to remote
|
// GetStatus returns the repository status relative to remote
|
||||||
@@ -315,6 +316,12 @@ func (g *Git) GetStatus() (*StatusInfo, error) {
|
|||||||
return nil, err
|
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
|
// Get the remote tracking branch
|
||||||
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
|
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
|
||||||
cmd.Dir = g.repoPath
|
cmd.Dir = g.repoPath
|
||||||
@@ -327,6 +334,7 @@ func (g *Git) GetStatus() (*StatusInfo, error) {
|
|||||||
Ahead: g.getAheadCount(remoteBranch),
|
Ahead: g.getAheadCount(remoteBranch),
|
||||||
Behind: 0, // Can't be behind if no upstream
|
Behind: 0, // Can't be behind if no upstream
|
||||||
Remote: remoteBranch,
|
Remote: remoteBranch,
|
||||||
|
Dirty: dirty,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +344,7 @@ func (g *Git) GetStatus() (*StatusInfo, error) {
|
|||||||
Ahead: g.getAheadCount(remoteBranch),
|
Ahead: g.getAheadCount(remoteBranch),
|
||||||
Behind: g.getBehindCount(remoteBranch),
|
Behind: g.getBehindCount(remoteBranch),
|
||||||
Remote: remoteBranch,
|
Remote: remoteBranch,
|
||||||
|
Dirty: dirty,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user