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:
|
||||
branches: [ main ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
@@ -52,6 +52,9 @@ lnk init -r git@github.com:user/dotfiles.git
|
||||
# Add files/directories
|
||||
lnk add ~/.vimrc ~/.config/nvim ~/.gitconfig
|
||||
|
||||
# List managed files
|
||||
lnk list
|
||||
|
||||
# Check status
|
||||
lnk status
|
||||
|
||||
@@ -99,6 +102,7 @@ lnk pull # auto-creates symlinks
|
||||
|
||||
```bash
|
||||
vim ~/.vimrc # edit normally
|
||||
lnk list # see what's managed
|
||||
lnk status # check what changed
|
||||
lnk push "new plugins" # commit & push
|
||||
```
|
||||
@@ -108,6 +112,7 @@ lnk push "new plugins" # commit & push
|
||||
- `lnk init [-r remote]` - Create repo
|
||||
- `lnk add <files>` - Move files to repo, create symlinks
|
||||
- `lnk rm <files>` - Move files back, remove symlinks
|
||||
- `lnk list` - List files managed by lnk
|
||||
- `lnk status` - Git status + sync info
|
||||
- `lnk push [msg]` - Stage all, commit, push
|
||||
- `lnk pull` - Pull + restore missing symlinks
|
||||
@@ -115,10 +120,8 @@ lnk push "new plugins" # commit & push
|
||||
## Technical bits
|
||||
|
||||
- **Single binary** (~8MB, no deps)
|
||||
- **Atomic operations** (rollback on failure)
|
||||
- **Relative symlinks** (portable)
|
||||
- **XDG compliant** (`~/.config/lnk`)
|
||||
- **20 integration tests**
|
||||
|
||||
## 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(newAddCmd())
|
||||
rootCmd.AddCommand(newRemoveCmd())
|
||||
rootCmd.AddCommand(newListCmd())
|
||||
rootCmd.AddCommand(newStatusCmd())
|
||||
rootCmd.AddCommand(newPushCmd())
|
||||
rootCmd.AddCommand(newPullCmd())
|
||||
|
104
cmd/root_test.go
104
cmd/root_test.go
@@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -162,6 +163,60 @@ func (suite *CLITestSuite) TestStatusCommand() {
|
||||
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() {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -206,6 +261,12 @@ func (suite *CLITestSuite) TestErrorHandling() {
|
||||
wantErr: false,
|
||||
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 {
|
||||
@@ -424,6 +485,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"
|
||||
}
|
||||
|
@@ -69,9 +69,6 @@ func getRelativePath(absPath string) (string, error) {
|
||||
if strings.HasPrefix(relPath, "..") {
|
||||
// Use absolute path but remove leading slash and drive letter (for cross-platform)
|
||||
cleanPath := strings.TrimPrefix(absPath, "/")
|
||||
if len(cleanPath) > 1 && cleanPath[1] == ':' {
|
||||
// Windows drive letter, keep as is
|
||||
}
|
||||
return cleanPath, nil
|
||||
}
|
||||
|
||||
@@ -353,6 +350,7 @@ type StatusInfo struct {
|
||||
Ahead int
|
||||
Behind int
|
||||
Remote string
|
||||
Dirty bool
|
||||
}
|
||||
|
||||
// Status returns the repository sync status
|
||||
@@ -371,6 +369,7 @@ func (l *Lnk) Status() (*StatusInfo, error) {
|
||||
Ahead: gitStatus.Ahead,
|
||||
Behind: gitStatus.Behind,
|
||||
Remote: gitStatus.Remote,
|
||||
Dirty: gitStatus.Dirty,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -429,6 +428,22 @@ func (l *Lnk) Pull() ([]string, error) {
|
||||
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
|
||||
func (l *Lnk) RestoreSymlinks() ([]string, error) {
|
||||
var restored []string
|
||||
|
@@ -480,6 +480,112 @@ 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")
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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