mirror of
https://github.com/yarlson/lnk.git
synced 2025-09-24 21:11:27 +02:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
57839c795e | ||
|
dc524607fa | ||
|
9bf2e70d13 | ||
|
65db5fe738 | ||
|
43b68bc071 | ||
|
ab97fa86dc |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -46,4 +46,3 @@ desktop.ini
|
||||
goreleaser/
|
||||
*.md
|
||||
!/README.md
|
||||
!/RELEASE.md
|
||||
|
49
README.md
49
README.md
@@ -26,7 +26,6 @@ curl -sSL https://raw.githubusercontent.com/yarlson/lnk/main/install.sh | bash
|
||||
|
||||
```bash
|
||||
# Homebrew (macOS/Linux)
|
||||
brew tap yarlson/lnk
|
||||
brew install lnk
|
||||
```
|
||||
|
||||
@@ -55,6 +54,9 @@ lnk init -r git@github.com:user/dotfiles.git
|
||||
# Skip automatic bootstrap
|
||||
lnk init -r git@github.com:user/dotfiles.git --no-bootstrap
|
||||
|
||||
# Force initialization (WARNING: overwrites existing managed files)
|
||||
lnk init -r git@github.com:user/dotfiles.git --force
|
||||
|
||||
# Run bootstrap script manually
|
||||
lnk bootstrap
|
||||
```
|
||||
@@ -104,6 +106,33 @@ After: ~/.ssh/config -> ~/.config/lnk/laptop.lnk/.ssh/config (symlink)
|
||||
|
||||
Your files live in `~/.config/lnk` (a Git repo). Common files go in the root, host-specific files go in `<host>.lnk/` subdirectories. Lnk creates symlinks back to original locations. Edit files normally, use Git normally.
|
||||
|
||||
## Safety Features
|
||||
|
||||
Lnk includes built-in safety checks to prevent accidental data loss:
|
||||
|
||||
### Data Loss Prevention
|
||||
|
||||
```bash
|
||||
# This will be blocked if you already have managed files
|
||||
lnk init -r git@github.com:user/dotfiles.git
|
||||
# ❌ Directory ~/.config/lnk already contains managed files
|
||||
# 💡 Use 'lnk pull' to update from remote instead of 'lnk init -r'
|
||||
|
||||
# Use pull instead to safely update
|
||||
lnk pull
|
||||
|
||||
# Or force if you understand the risks (shows warning only when needed)
|
||||
lnk init -r git@github.com:user/dotfiles.git --force
|
||||
# ⚠️ Using --force flag: This will overwrite existing managed files
|
||||
# 💡 Only use this if you understand the risks
|
||||
```
|
||||
|
||||
### Smart Warnings
|
||||
|
||||
- **Contextual alerts**: Warnings only appear when there are actually managed files to overwrite
|
||||
- **Clear guidance**: Error messages suggest the correct command to use
|
||||
- **Force override**: Advanced users can bypass safety checks when needed
|
||||
|
||||
## Bootstrap Support
|
||||
|
||||
Lnk automatically runs bootstrap scripts when cloning dotfiles repositories, making it easy to set up your development environment. Just add a `bootstrap.sh` file to your dotfiles repo.
|
||||
@@ -277,7 +306,7 @@ lnk pull # Get updates (work config won't affe
|
||||
|
||||
## Commands
|
||||
|
||||
- `lnk init [-r remote] [--no-bootstrap]` - Create repo (runs bootstrap automatically)
|
||||
- `lnk init [-r remote] [--no-bootstrap] [--force]` - Create repo (runs bootstrap automatically)
|
||||
- `lnk add [--host HOST] [--recursive] [--dry-run] <files>...` - Move files to repo, create symlinks
|
||||
- `lnk rm [--host HOST] <files>` - Move files back, remove symlinks
|
||||
- `lnk list [--host HOST] [--all]` - List files managed by lnk
|
||||
@@ -294,6 +323,7 @@ lnk pull # Get updates (work config won't affe
|
||||
- `--all` - Show all configurations (common + all hosts) when listing
|
||||
- `-r, --remote URL` - Clone from remote URL when initializing
|
||||
- `--no-bootstrap` - Skip automatic execution of bootstrap script after cloning
|
||||
- `--force` - Force initialization even if directory contains managed files (WARNING: overwrites existing content)
|
||||
|
||||
### Add Command Examples
|
||||
|
||||
@@ -322,17 +352,18 @@ lnk add --host work ~/.gitconfig ~/.ssh/config ~/.npmrc
|
||||
- **Bulk operations** (multiple files, atomic transactions)
|
||||
- **Recursive processing** (directory contents individually)
|
||||
- **Preview mode** (dry-run for safety)
|
||||
- **Data loss prevention** (safety checks with contextual warnings)
|
||||
- **Git-native** (standard Git repo, no special formats)
|
||||
|
||||
## Alternatives
|
||||
|
||||
| Tool | Complexity | Why choose it |
|
||||
| ------- | ---------- | -------------------------------------------------------------------------- |
|
||||
| **lnk** | Minimal | Just works, no config, Git-native, multihost, bootstrap, bulk ops, dry-run |
|
||||
| chezmoi | High | Templates, encryption, cross-platform |
|
||||
| yadm | Medium | Git power user, encryption |
|
||||
| dotbot | Low | YAML config, basic features |
|
||||
| stow | Low | Perl, symlink only |
|
||||
| Tool | Complexity | Why choose it |
|
||||
| ------- | ---------- | ----------------------------------------------------------------------------------------- |
|
||||
| **lnk** | Minimal | Just works, no config, Git-native, multihost, bootstrap, bulk ops, dry-run, safety checks |
|
||||
| chezmoi | High | Templates, encryption, cross-platform |
|
||||
| yadm | Medium | Git power user, encryption |
|
||||
| dotbot | Low | YAML config, basic features |
|
||||
| stow | Low | Perl, symlink only |
|
||||
|
||||
## Contributing
|
||||
|
||||
|
190
RELEASE.md
190
RELEASE.md
@@ -1,190 +0,0 @@
|
||||
# Release Process
|
||||
|
||||
This document describes how to create releases for the lnk project using GoReleaser.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Push access to the main repository
|
||||
- Git tags pushed to GitHub trigger releases automatically
|
||||
- GoReleaser is configured in `.goreleaser.yml`
|
||||
- GitHub Actions will handle the release process
|
||||
- Access to the [homebrew-lnk](https://github.com/yarlson/homebrew-lnk) tap repository
|
||||
- **Personal Access Token** set up as `HOMEBREW_TAP_TOKEN` secret (see Setup section)
|
||||
|
||||
## Setup (One-time)
|
||||
|
||||
### GitHub Personal Access Token
|
||||
|
||||
For GoReleaser to update the Homebrew formula, you need a Personal Access Token:
|
||||
|
||||
1. Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
|
||||
2. Click "Generate new token" → "Generate new token (classic)"
|
||||
3. Name: "GoReleaser Homebrew Access"
|
||||
4. Scopes: Select `repo` (Full control of private repositories)
|
||||
5. Generate and copy the token
|
||||
6. In your `yarlson/lnk` repository:
|
||||
- Go to Settings → Secrets and variables → Actions
|
||||
- Add new repository secret: `HOMEBREW_TAP_TOKEN`
|
||||
- Paste the token as the value
|
||||
|
||||
This allows GoReleaser to automatically update the Homebrew formula in [homebrew-lnk](https://github.com/yarlson/homebrew-lnk).
|
||||
|
||||
## Creating a Release
|
||||
|
||||
### 1. Ensure everything is ready
|
||||
|
||||
```bash
|
||||
# Run all quality checks
|
||||
make check
|
||||
|
||||
# Test GoReleaser configuration
|
||||
make goreleaser-check
|
||||
|
||||
# Test build process
|
||||
make goreleaser-snapshot
|
||||
```
|
||||
|
||||
### 2. Create and push a version tag
|
||||
|
||||
```bash
|
||||
# Create a new tag (replace x.y.z with actual version)
|
||||
git tag -a v1.0.0 -m "Release v1.0.0"
|
||||
|
||||
# Push the tag to trigger the release
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
### 3. Monitor the release
|
||||
|
||||
- GitHub Actions will automatically build and release when the tag is pushed
|
||||
- Check the [Actions tab](https://github.com/yarlson/lnk/actions) for build status
|
||||
- The release will appear in [GitHub Releases](https://github.com/yarlson/lnk/releases)
|
||||
- The Homebrew formula will be automatically updated in [homebrew-lnk](https://github.com/yarlson/homebrew-lnk)
|
||||
|
||||
## What GoReleaser Does
|
||||
|
||||
1. **Builds binaries** for multiple platforms:
|
||||
|
||||
- Linux (amd64, arm64)
|
||||
- macOS (amd64, arm64)
|
||||
- Windows (amd64)
|
||||
|
||||
2. **Creates archives** with consistent naming:
|
||||
|
||||
- `lnk_Linux_x86_64.tar.gz`
|
||||
- `lnk_Darwin_arm64.tar.gz`
|
||||
- etc.
|
||||
|
||||
3. **Generates checksums** for verification
|
||||
|
||||
4. **Creates GitHub release** with:
|
||||
|
||||
- Automatic changelog from conventional commits
|
||||
- Installation instructions
|
||||
- Download links for all platforms
|
||||
|
||||
5. **Updates Homebrew formula** automatically in the [homebrew-lnk](https://github.com/yarlson/homebrew-lnk) tap
|
||||
|
||||
## Manual Release (if needed)
|
||||
|
||||
If you need to create a release manually:
|
||||
|
||||
```bash
|
||||
# Export GitHub token
|
||||
export GITHUB_TOKEN="your_token_here"
|
||||
|
||||
# Create release (requires a git tag)
|
||||
goreleaser release --clean
|
||||
```
|
||||
|
||||
## Testing Releases Locally
|
||||
|
||||
```bash
|
||||
# Test the build process without releasing
|
||||
make goreleaser-snapshot
|
||||
|
||||
# Built artifacts will be in dist/
|
||||
ls -la dist/
|
||||
|
||||
# Test a binary
|
||||
./dist/lnk_<platform>/lnk --version
|
||||
```
|
||||
|
||||
## Installation Methods
|
||||
|
||||
After a release is published, users can install lnk using multiple methods:
|
||||
|
||||
### 1. Shell Script (Recommended)
|
||||
|
||||
```bash
|
||||
curl -sSL https://raw.githubusercontent.com/yarlson/lnk/main/install.sh | bash
|
||||
```
|
||||
|
||||
### 2. Homebrew (macOS/Linux)
|
||||
|
||||
```bash
|
||||
brew tap yarlson/lnk
|
||||
brew install lnk
|
||||
```
|
||||
|
||||
### 3. Manual Download
|
||||
|
||||
```bash
|
||||
# Download from GitHub releases
|
||||
wget https://github.com/yarlson/lnk/releases/latest/download/lnk_Linux_x86_64.tar.gz
|
||||
tar -xzf lnk_Linux_x86_64.tar.gz
|
||||
sudo mv lnk /usr/local/bin/
|
||||
```
|
||||
|
||||
## Version Numbering
|
||||
|
||||
We use [Semantic Versioning](https://semver.org/):
|
||||
|
||||
- `v1.0.0` - Major release (breaking changes)
|
||||
- `v1.1.0` - Minor release (new features, backward compatible)
|
||||
- `v1.1.1` - Patch release (bug fixes)
|
||||
|
||||
## Changelog
|
||||
|
||||
GoReleaser automatically generates changelogs from git commits using conventional commit format:
|
||||
|
||||
- `feat:` - New features
|
||||
- `fix:` - Bug fixes
|
||||
- `docs:` - Documentation changes (excluded from changelog)
|
||||
- `test:` - Test changes (excluded from changelog)
|
||||
- `ci:` - CI changes (excluded from changelog)
|
||||
|
||||
## Homebrew Tap
|
||||
|
||||
The Homebrew formula is automatically maintained in the [homebrew-lnk](https://github.com/yarlson/homebrew-lnk) repository. When a new release is created:
|
||||
|
||||
1. GoReleaser automatically creates/updates the formula
|
||||
2. The formula is committed to the tap repository
|
||||
3. Users can immediately install the new version via `brew install yarlson/lnk/lnk`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Release failed to create
|
||||
|
||||
1. Check that the tag follows the format `vX.Y.Z`
|
||||
2. Ensure GitHub Actions has proper permissions
|
||||
3. Check the Actions log for detailed error messages
|
||||
|
||||
### Missing binaries in release
|
||||
|
||||
1. Verify GoReleaser configuration: `make goreleaser-check`
|
||||
2. Test build locally: `make goreleaser-snapshot`
|
||||
3. Check the build matrix in `.goreleaser.yml`
|
||||
|
||||
### Changelog is empty
|
||||
|
||||
1. Ensure commits follow conventional commit format
|
||||
2. Check that there are commits since the last tag
|
||||
3. Verify changelog configuration in `.goreleaser.yml`
|
||||
|
||||
### Homebrew formula not updated
|
||||
|
||||
1. Check that the GITHUB_TOKEN has access to the homebrew-lnk repository
|
||||
2. Verify the repository name and owner in `.goreleaser.yml`
|
||||
3. Check the release workflow logs for Homebrew-related errors
|
||||
4. Ensure the homebrew-lnk repository exists and is accessible
|
11
cmd/init.go
11
cmd/init.go
@@ -15,9 +15,17 @@ func newInitCmd() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
remote, _ := cmd.Flags().GetString("remote")
|
||||
noBootstrap, _ := cmd.Flags().GetBool("no-bootstrap")
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
lnk := core.NewLnk()
|
||||
if err := lnk.InitWithRemote(remote); err != nil {
|
||||
|
||||
// Show warning when force is used and there are managed files to overwrite
|
||||
if force && remote != "" && lnk.HasUserContent() {
|
||||
printf(cmd, "⚠️ \033[33mUsing --force flag: This will overwrite existing managed files\033[0m\n")
|
||||
printf(cmd, " 💡 Only use this if you understand the risks\n\n")
|
||||
}
|
||||
|
||||
if err := lnk.InitWithRemoteForce(remote, force); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -69,5 +77,6 @@ func newInitCmd() *cobra.Command {
|
||||
|
||||
cmd.Flags().StringP("remote", "r", "", "Clone from remote URL instead of creating empty repository")
|
||||
cmd.Flags().Bool("no-bootstrap", false, "Skip automatic execution of bootstrap script after cloning")
|
||||
cmd.Flags().Bool("force", false, "Force initialization even if directory contains managed files (WARNING: This will overwrite existing content)")
|
||||
return cmd
|
||||
}
|
||||
|
467
cmd/root_test.go
467
cmd/root_test.go
@@ -6,7 +6,9 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@@ -1338,6 +1340,471 @@ func (suite *CLITestSuite) TestUpdatedHelpText() {
|
||||
suite.Contains(addHelpOutput, "without making changes", "Should explain dry-run flag")
|
||||
}
|
||||
|
||||
// Task 3.1: Tests for force flag functionality
|
||||
func (suite *CLITestSuite) TestInitCmd_ForceFlag_BypassesSafetyCheck() {
|
||||
// Setup: Create .lnk file to simulate existing content
|
||||
lnkDir := filepath.Join(suite.tempDir, ".config", "lnk")
|
||||
err := os.MkdirAll(lnkDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Initialize git repo first
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = lnkDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
lnkFile := filepath.Join(lnkDir, ".lnk")
|
||||
err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a dummy remote directory for testing
|
||||
remoteDir := filepath.Join(suite.tempDir, "remote")
|
||||
err = os.MkdirAll(remoteDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
cmd = exec.Command("git", "init", "--bare")
|
||||
cmd.Dir = remoteDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Execute init command with --force flag
|
||||
err = suite.runCommand("init", "-r", remoteDir, "--force")
|
||||
suite.NoError(err, "Force flag should bypass safety check")
|
||||
|
||||
// Verify output shows warning
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "force", "Should show force warning")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestInitCmd_NoForceFlag_RespectsSafetyCheck() {
|
||||
// Setup: Create .lnk file to simulate existing content
|
||||
lnkDir := filepath.Join(suite.tempDir, ".config", "lnk")
|
||||
err := os.MkdirAll(lnkDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Initialize git repo first
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = lnkDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
lnkFile := filepath.Join(lnkDir, ".lnk")
|
||||
err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a dummy remote directory for testing
|
||||
remoteDir := filepath.Join(suite.tempDir, "remote")
|
||||
err = os.MkdirAll(remoteDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
cmd = exec.Command("git", "init", "--bare")
|
||||
cmd.Dir = remoteDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Execute init command without --force flag - should fail
|
||||
err = suite.runCommand("init", "-r", remoteDir)
|
||||
suite.Error(err, "Should respect safety check without force flag")
|
||||
suite.Contains(err.Error(), "already contains managed files")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestInitCmd_ForceFlag_ShowsWarning() {
|
||||
// Setup: Create .lnk file to simulate existing content
|
||||
lnkDir := filepath.Join(suite.tempDir, ".config", "lnk")
|
||||
err := os.MkdirAll(lnkDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Initialize git repo first
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = lnkDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
lnkFile := filepath.Join(lnkDir, ".lnk")
|
||||
err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a dummy remote directory for testing
|
||||
remoteDir := filepath.Join(suite.tempDir, "remote")
|
||||
err = os.MkdirAll(remoteDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
cmd = exec.Command("git", "init", "--bare")
|
||||
cmd.Dir = remoteDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Execute init command with --force flag
|
||||
err = suite.runCommand("init", "-r", remoteDir, "--force")
|
||||
suite.NoError(err, "Force flag should bypass safety check")
|
||||
|
||||
// Verify output shows appropriate warning
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "⚠️", "Should show warning emoji")
|
||||
suite.Contains(output, "overwrite", "Should warn about overwriting")
|
||||
}
|
||||
|
||||
// Task 4.1: Integration tests for end-to-end workflows
|
||||
func (suite *CLITestSuite) TestE2E_InitAddInit_PreventDataLoss() {
|
||||
// Run: lnk init
|
||||
err := suite.runCommand("init")
|
||||
suite.Require().NoError(err)
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Create and add test file
|
||||
testFile := filepath.Join(suite.tempDir, ".testfile")
|
||||
err = os.WriteFile(testFile, []byte("important content"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
err = suite.runCommand("add", testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create dummy remote for testing
|
||||
remoteDir := filepath.Join(suite.tempDir, "remote")
|
||||
err = os.MkdirAll(remoteDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
cmd := exec.Command("git", "init", "--bare")
|
||||
cmd.Dir = remoteDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Run: lnk init -r <remote> → should FAIL
|
||||
err = suite.runCommand("init", "-r", remoteDir)
|
||||
suite.Error(err, "Should prevent data loss")
|
||||
suite.Contains(err.Error(), "already contains managed files")
|
||||
|
||||
// Verify testfile still exists and is managed
|
||||
suite.FileExists(testFile)
|
||||
info, err := os.Lstat(testFile)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink, "File should still be symlink")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestE2E_FreshInit_Success() {
|
||||
// Create dummy remote for testing
|
||||
remoteDir := filepath.Join(suite.tempDir, "remote")
|
||||
err := os.MkdirAll(remoteDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
cmd := exec.Command("git", "init", "--bare")
|
||||
cmd.Dir = remoteDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Fresh init with remote should succeed
|
||||
err = suite.runCommand("init", "-r", remoteDir)
|
||||
suite.NoError(err, "Fresh init should succeed")
|
||||
|
||||
// Verify repository was created
|
||||
lnkDir := filepath.Join(suite.tempDir, ".config", "lnk")
|
||||
suite.DirExists(lnkDir)
|
||||
gitDir := filepath.Join(lnkDir, ".git")
|
||||
suite.DirExists(gitDir)
|
||||
|
||||
// Verify success message
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "Initialized lnk repository")
|
||||
suite.Contains(output, "Cloned from:")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestE2E_ForceInit_OverwritesContent() {
|
||||
// Setup: init and add content first
|
||||
err := suite.runCommand("init")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
testFile := filepath.Join(suite.tempDir, ".testfile")
|
||||
err = os.WriteFile(testFile, []byte("original content"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
err = suite.runCommand("add", testFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Create dummy remote for testing
|
||||
remoteDir := filepath.Join(suite.tempDir, "remote")
|
||||
err = os.MkdirAll(remoteDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
cmd := exec.Command("git", "init", "--bare")
|
||||
cmd.Dir = remoteDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Force init should succeed and show warning
|
||||
err = suite.runCommand("init", "-r", remoteDir, "--force")
|
||||
suite.NoError(err, "Force init should succeed")
|
||||
|
||||
// Verify warning was shown
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "⚠️", "Should show warning")
|
||||
suite.Contains(output, "overwrite", "Should warn about overwriting")
|
||||
suite.Contains(output, "Initialized lnk repository")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestE2E_ErrorMessage_SuggestsCorrectCommand() {
|
||||
// Setup: init and add content first
|
||||
err := suite.runCommand("init")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
testFile := filepath.Join(suite.tempDir, ".testfile")
|
||||
err = os.WriteFile(testFile, []byte("important content"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
err = suite.runCommand("add", testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Try init with remote - should fail with helpful message
|
||||
err = suite.runCommand("init", "-r", "https://github.com/test/dotfiles.git")
|
||||
suite.Error(err, "Should fail with helpful error")
|
||||
|
||||
// Verify error message suggests correct alternative
|
||||
suite.Contains(err.Error(), "already contains managed files", "Should explain the problem")
|
||||
suite.Contains(err.Error(), "lnk pull", "Should suggest pull command")
|
||||
suite.Contains(err.Error(), "instead of", "Should explain the alternative")
|
||||
suite.Contains(err.Error(), "lnk init -r", "Should show the problematic command")
|
||||
}
|
||||
|
||||
// Task 6.1: Regression tests to ensure existing functionality unchanged
|
||||
func (suite *CLITestSuite) TestRegression_FreshInit_UnchangedBehavior() {
|
||||
// Test that fresh init (no existing content) works exactly as before
|
||||
err := suite.runCommand("init")
|
||||
suite.NoError(err, "Fresh init should work unchanged")
|
||||
|
||||
// Verify same output format and behavior
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "Initialized empty lnk repository")
|
||||
suite.Contains(output, "Location:")
|
||||
|
||||
// Verify repository structure is created correctly
|
||||
lnkDir := filepath.Join(suite.tempDir, ".config", "lnk")
|
||||
suite.DirExists(lnkDir)
|
||||
gitDir := filepath.Join(lnkDir, ".git")
|
||||
suite.DirExists(gitDir)
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestRegression_ExistingWorkflows_StillWork() {
|
||||
// Test that all existing workflows continue to function
|
||||
|
||||
// 1. Normal init → add → list → remove workflow
|
||||
err := suite.runCommand("init")
|
||||
suite.NoError(err, "Init should work")
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Create and 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.NoError(err, "Add should work")
|
||||
suite.stdout.Reset()
|
||||
|
||||
// List files
|
||||
err = suite.runCommand("list")
|
||||
suite.NoError(err, "List should work")
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, ".bashrc", "Should list added file")
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Remove file
|
||||
err = suite.runCommand("rm", testFile)
|
||||
suite.NoError(err, "Remove should work")
|
||||
|
||||
// Verify file is restored as regular file
|
||||
info, err := os.Lstat(testFile)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "File should be regular after remove")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestRegression_GitOperations_Unaffected() {
|
||||
// Test that Git operations continue to work normally
|
||||
err := suite.runCommand("init")
|
||||
suite.NoError(err)
|
||||
|
||||
// Add a file to create commits
|
||||
testFile := filepath.Join(suite.tempDir, ".vimrc")
|
||||
err = os.WriteFile(testFile, []byte("set number"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.runCommand("add", testFile)
|
||||
suite.NoError(err)
|
||||
|
||||
// Verify Git repository structure and commits are normal
|
||||
lnkDir := filepath.Join(suite.tempDir, ".config", "lnk")
|
||||
|
||||
// Check that commits are created normally
|
||||
cmd := exec.Command("git", "log", "--oneline", "--format=%s")
|
||||
cmd.Dir = lnkDir
|
||||
output, err := cmd.Output()
|
||||
suite.NoError(err, "Git log should work")
|
||||
|
||||
commits := string(output)
|
||||
suite.Contains(commits, "lnk: added .vimrc", "Should have normal commit message")
|
||||
|
||||
// Check that git status works
|
||||
cmd = exec.Command("git", "status", "--porcelain")
|
||||
cmd.Dir = lnkDir
|
||||
statusOutput, err := cmd.Output()
|
||||
suite.NoError(err, "Git status should work")
|
||||
suite.Empty(strings.TrimSpace(string(statusOutput)), "Working directory should be clean")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestRegression_PerformanceImpact_Minimal() {
|
||||
// Test that the new safety checks don't significantly impact performance
|
||||
|
||||
// Simple performance check: ensure a single init completes quickly
|
||||
start := time.Now()
|
||||
err := suite.runCommand("init")
|
||||
elapsed := time.Since(start)
|
||||
|
||||
suite.NoError(err, "Init should succeed")
|
||||
suite.Less(elapsed, 2*time.Second, "Init should complete quickly")
|
||||
|
||||
// Test safety check performance on existing repository
|
||||
suite.stdout.Reset()
|
||||
start = time.Now()
|
||||
err = suite.runCommand("init", "-r", "dummy-url")
|
||||
elapsed = time.Since(start)
|
||||
|
||||
// Should fail quickly due to safety check (not hang)
|
||||
suite.Error(err, "Should fail due to safety check")
|
||||
suite.Less(elapsed, 1*time.Second, "Safety check should be fast")
|
||||
}
|
||||
|
||||
// Task 7.1: Tests for help documentation
|
||||
func (suite *CLITestSuite) TestInitCommand_HelpText_MentionsForceFlag() {
|
||||
err := suite.runCommand("init", "--help")
|
||||
suite.NoError(err)
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "--force", "Help should mention force flag")
|
||||
suite.Contains(output, "overwrite", "Help should explain force behavior")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestInitCommand_HelpText_ExplainsDataProtection() {
|
||||
err := suite.runCommand("init", "--help")
|
||||
suite.NoError(err)
|
||||
output := suite.stdout.String()
|
||||
|
||||
// Should explain what the command does
|
||||
suite.Contains(output, "Creates", "Should explain what init does")
|
||||
suite.Contains(output, "lnk directory", "Should mention lnk directory")
|
||||
|
||||
// Should warn about the force flag risks
|
||||
suite.Contains(output, "WARNING", "Should warn about force flag risks")
|
||||
suite.Contains(output, "overwrite existing content", "Should mention overwrite risk")
|
||||
}
|
||||
|
||||
// TestPushPullWithDifferentBranches tests push/pull operations with different default branch names
|
||||
func (suite *CLITestSuite) TestPushPullWithDifferentBranches() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
branchName string
|
||||
setupRemote func(remoteDir string) error
|
||||
}{
|
||||
{
|
||||
name: "master branch",
|
||||
branchName: "master",
|
||||
setupRemote: func(remoteDir string) error {
|
||||
cmd := exec.Command("git", "init", "--bare", "--initial-branch=master")
|
||||
cmd.Dir = remoteDir
|
||||
return cmd.Run()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "main branch",
|
||||
branchName: "main",
|
||||
setupRemote: func(remoteDir string) error {
|
||||
cmd := exec.Command("git", "init", "--bare", "--initial-branch=main")
|
||||
cmd.Dir = remoteDir
|
||||
return cmd.Run()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom branch",
|
||||
branchName: "develop",
|
||||
setupRemote: func(remoteDir string) error {
|
||||
cmd := exec.Command("git", "init", "--bare", "--initial-branch=develop")
|
||||
cmd.Dir = remoteDir
|
||||
return cmd.Run()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
// Create a separate temp directory for this test case
|
||||
testDir, err := os.MkdirTemp("", "lnk-push-pull-test-*")
|
||||
suite.Require().NoError(err)
|
||||
defer func() { _ = os.RemoveAll(testDir) }()
|
||||
|
||||
// Save current dir and change to test dir
|
||||
originalDir, err := os.Getwd()
|
||||
suite.Require().NoError(err)
|
||||
defer func() { _ = os.Chdir(originalDir) }()
|
||||
|
||||
err = os.Chdir(testDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Set HOME to test directory
|
||||
suite.T().Setenv("HOME", testDir)
|
||||
suite.T().Setenv("XDG_CONFIG_HOME", testDir)
|
||||
|
||||
// Create remote repository
|
||||
remoteDir := filepath.Join(testDir, "remote.git")
|
||||
err = os.MkdirAll(remoteDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = tc.setupRemote(remoteDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Initialize lnk with remote
|
||||
err = suite.runCommand("init", "--remote", remoteDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Switch to the test branch if not main/master (since init creates main by default)
|
||||
if tc.branchName != "main" {
|
||||
lnkDir := filepath.Join(testDir, "lnk")
|
||||
cmd := exec.Command("git", "checkout", "-b", tc.branchName)
|
||||
cmd.Dir = lnkDir
|
||||
_, err = cmd.CombinedOutput()
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
|
||||
// Add a test file
|
||||
testFile := filepath.Join(testDir, ".testrc")
|
||||
err = os.WriteFile(testFile, []byte("test config"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.runCommand("add", testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test push operation
|
||||
err = suite.runCommand("push", "test push with "+tc.branchName)
|
||||
suite.Require().NoError(err, "Push should work with %s branch", tc.branchName)
|
||||
|
||||
// Create another test directory to simulate pulling from another machine
|
||||
pullTestDir, err := os.MkdirTemp("", "lnk-pull-test-*")
|
||||
suite.Require().NoError(err)
|
||||
defer func() { _ = os.RemoveAll(pullTestDir) }()
|
||||
|
||||
err = os.Chdir(pullTestDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Set HOME for pull test
|
||||
suite.T().Setenv("HOME", pullTestDir)
|
||||
suite.T().Setenv("XDG_CONFIG_HOME", pullTestDir)
|
||||
|
||||
// Clone and test pull
|
||||
err = suite.runCommand("init", "--remote", remoteDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.runCommand("pull")
|
||||
suite.Require().NoError(err, "Pull should work with %s branch", tc.branchName)
|
||||
|
||||
// Verify the file was pulled correctly
|
||||
lnkDir := filepath.Join(pullTestDir, "lnk")
|
||||
pulledFile := filepath.Join(lnkDir, ".testrc")
|
||||
suite.FileExists(pulledFile, "File should exist after pull with %s branch", tc.branchName)
|
||||
|
||||
content, err := os.ReadFile(pulledFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal("test config", string(content), "File content should match after pull with %s branch", tc.branchName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLISuite(t *testing.T) {
|
||||
suite.Run(t, new(CLITestSuite))
|
||||
}
|
||||
|
@@ -46,6 +46,34 @@ func NewLnk(opts ...Option) *Lnk {
|
||||
return lnk
|
||||
}
|
||||
|
||||
// HasUserContent checks if the repository contains managed files
|
||||
// by looking for .lnk tracker files (common or host-specific)
|
||||
func (l *Lnk) HasUserContent() bool {
|
||||
// Check for common tracker file
|
||||
commonTracker := filepath.Join(l.repoPath, ".lnk")
|
||||
if _, err := os.Stat(commonTracker); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for host-specific tracker files if host is set
|
||||
if l.host != "" {
|
||||
hostTracker := filepath.Join(l.repoPath, fmt.Sprintf(".lnk.%s", l.host))
|
||||
if _, err := os.Stat(hostTracker); err == nil {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// If no specific host is set, check for any host-specific tracker files
|
||||
// This handles cases where we want to detect any managed content
|
||||
pattern := filepath.Join(l.repoPath, ".lnk.*")
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err == nil && len(matches) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCurrentHostname returns the current system hostname
|
||||
func GetCurrentHostname() (string, error) {
|
||||
hostname, err := os.Hostname()
|
||||
@@ -119,7 +147,18 @@ func (l *Lnk) Init() error {
|
||||
|
||||
// InitWithRemote initializes the lnk repository, optionally cloning from a remote
|
||||
func (l *Lnk) InitWithRemote(remoteURL string) error {
|
||||
return l.InitWithRemoteForce(remoteURL, false)
|
||||
}
|
||||
|
||||
// InitWithRemoteForce initializes the lnk repository with optional force override
|
||||
func (l *Lnk) InitWithRemoteForce(remoteURL string, force bool) error {
|
||||
if remoteURL != "" {
|
||||
// Safety check: prevent data loss by checking for existing managed files
|
||||
if l.HasUserContent() {
|
||||
if !force {
|
||||
return fmt.Errorf("❌ Directory \033[31m%s\033[0m already contains managed files\n 💡 Use 'lnk pull' to update from remote instead of 'lnk init -r'", l.repoPath)
|
||||
}
|
||||
}
|
||||
// Clone from remote
|
||||
return l.Clone(remoteURL)
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -1432,6 +1433,170 @@ func (suite *CoreTestSuite) TestPreviewAddValidation() {
|
||||
suite.Contains(err.Error(), "already managed", "Error should mention already managed")
|
||||
}
|
||||
|
||||
// Task 1.1: Tests for HasUserContent() method
|
||||
func (suite *CoreTestSuite) TestHasUserContent_WithCommonTracker_ReturnsTrue() {
|
||||
// Initialize lnk repository
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create .lnk file to simulate existing content
|
||||
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
|
||||
err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Call HasUserContent()
|
||||
hasContent := suite.lnk.HasUserContent()
|
||||
suite.True(hasContent, "Should detect common tracker file")
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestHasUserContent_WithHostTracker_ReturnsTrue() {
|
||||
// Initialize lnk repository
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create host-specific lnk instance
|
||||
hostLnk := NewLnk(WithHost("testhost"))
|
||||
|
||||
// Create .lnk.hostname file to simulate host-specific content
|
||||
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk.testhost")
|
||||
err = os.WriteFile(lnkFile, []byte(".vimrc\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Call HasUserContent()
|
||||
hasContent := hostLnk.HasUserContent()
|
||||
suite.True(hasContent, "Should detect host-specific tracker file")
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestHasUserContent_WithBothTrackers_ReturnsTrue() {
|
||||
// Initialize lnk repository
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create both common and host-specific tracker files
|
||||
commonLnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
|
||||
err = os.WriteFile(commonLnkFile, []byte(".bashrc\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
hostLnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk.testhost")
|
||||
err = os.WriteFile(hostLnkFile, []byte(".vimrc\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test with common instance
|
||||
hasContent := suite.lnk.HasUserContent()
|
||||
suite.True(hasContent, "Should detect common tracker file")
|
||||
|
||||
// Test with host-specific instance
|
||||
hostLnk := NewLnk(WithHost("testhost"))
|
||||
hasContent = hostLnk.HasUserContent()
|
||||
suite.True(hasContent, "Should detect host-specific tracker file")
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestHasUserContent_EmptyDirectory_ReturnsFalse() {
|
||||
// Initialize lnk repository
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Call HasUserContent() on empty repository
|
||||
hasContent := suite.lnk.HasUserContent()
|
||||
suite.False(hasContent, "Should return false for empty repository")
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestHasUserContent_NonTrackerFiles_ReturnsFalse() {
|
||||
// Initialize lnk repository
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create non-tracker files
|
||||
randomFile := filepath.Join(suite.tempDir, "lnk", "random.txt")
|
||||
err = os.WriteFile(randomFile, []byte("some content"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
configFile := filepath.Join(suite.tempDir, "lnk", ".gitignore")
|
||||
err = os.WriteFile(configFile, []byte("*.log"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Call HasUserContent()
|
||||
hasContent := suite.lnk.HasUserContent()
|
||||
suite.False(hasContent, "Should return false when only non-tracker files exist")
|
||||
}
|
||||
|
||||
// Task 2.1: Tests for enhanced InitWithRemote() safety check
|
||||
func (suite *CoreTestSuite) TestInitWithRemote_HasUserContent_ReturnsError() {
|
||||
// Initialize and add content first
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create .lnk file to simulate existing content
|
||||
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
|
||||
err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Try InitWithRemote - should fail
|
||||
err = suite.lnk.InitWithRemote("https://github.com/test/dotfiles.git")
|
||||
suite.Error(err, "Should fail when user content exists")
|
||||
suite.Contains(err.Error(), "already contains managed files")
|
||||
suite.Contains(err.Error(), "lnk pull")
|
||||
|
||||
// Verify .lnk file still exists (no deletion occurred)
|
||||
suite.FileExists(lnkFile)
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestInitWithRemote_EmptyDirectory_Success() {
|
||||
// Create a dummy remote directory for testing
|
||||
remoteDir := filepath.Join(suite.tempDir, "remote")
|
||||
err := os.MkdirAll(remoteDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Initialize a bare git repository as remote
|
||||
cmd := exec.Command("git", "init", "--bare")
|
||||
cmd.Dir = remoteDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// InitWithRemote should succeed on empty directory
|
||||
err = suite.lnk.InitWithRemote(remoteDir)
|
||||
suite.NoError(err, "Should succeed when no user content exists")
|
||||
|
||||
// Verify repository was cloned
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
suite.DirExists(lnkDir)
|
||||
gitDir := filepath.Join(lnkDir, ".git")
|
||||
suite.DirExists(gitDir)
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestInitWithRemote_NoRemoteURL_BypassesSafetyCheck() {
|
||||
// Initialize and add content first
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create .lnk file to simulate existing content
|
||||
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
|
||||
err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// InitWithRemote with empty URL should bypass safety check (this is local init)
|
||||
err = suite.lnk.InitWithRemote("")
|
||||
suite.NoError(err, "Should bypass safety check when no remote URL provided")
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestInitWithRemote_ErrorMessage_ContainsSuggestedCommand() {
|
||||
// Initialize and add content first
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create host-specific content
|
||||
hostLnk := NewLnk(WithHost("testhost"))
|
||||
hostLnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk.testhost")
|
||||
err = os.WriteFile(hostLnkFile, []byte(".vimrc\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Try InitWithRemote - should fail with helpful message
|
||||
err = hostLnk.InitWithRemote("https://github.com/test/dotfiles.git")
|
||||
suite.Error(err, "Should fail when user content exists")
|
||||
suite.Contains(err.Error(), "lnk pull", "Should suggest pull command")
|
||||
suite.Contains(err.Error(), "instead of", "Should explain alternative")
|
||||
}
|
||||
|
||||
func TestCoreSuite(t *testing.T) {
|
||||
suite.Run(t, new(CoreTestSuite))
|
||||
}
|
||||
|
@@ -437,7 +437,7 @@ func (g *Git) Push() error {
|
||||
return &PushError{Reason: err.Error(), Err: err}
|
||||
}
|
||||
|
||||
cmd := exec.Command("git", "push", "-u", "origin", "main")
|
||||
cmd := exec.Command("git", "push", "-u", "origin")
|
||||
cmd.Dir = g.repoPath
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
@@ -456,7 +456,7 @@ func (g *Git) Pull() error {
|
||||
return &PullError{Reason: err.Error(), Err: err}
|
||||
}
|
||||
|
||||
cmd := exec.Command("git", "pull", "origin", "main")
|
||||
cmd := exec.Command("git", "pull", "origin")
|
||||
cmd.Dir = g.repoPath
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
Reference in New Issue
Block a user