Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions cmd/global_remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
Copyright © 2026 laggu
*/
package cmd

import (
"github.com/laggu/git-volume/internal/gitvolume"
"github.com/spf13/cobra"
Comment on lines +7 to +8
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The import order does not follow the standard Go convention, which is also specified in the repository's style guide. Imports should be grouped by standard library, third-party libraries, and then local packages, with blank lines between groups.

	"github.com/spf13/cobra"

	"github.com/laggu/git-volume/internal/gitvolume"
References
  1. Rule fix: remove global directory overwrite #7 of the repository style guide (Imports: 표준 라이브러리, 서드파티 라이브러리, 로컬 패키지 순으로 그룹화합니다.) specifies that Go imports should be grouped in the order of: standard library, third-party libraries, and then local packages. (link)

)

// globalRemoveCmd represents the global remove command
var globalRemoveCmd = &cobra.Command{
Use: "remove <file>...",
Short: "Remove files from global storage",
Long: `Removes files or directories from the global git-volume storage (~/.git-volume).

Examples:
git volume global remove .env
git volume global remove secrets/api.key`,
Args: cobra.MinimumNArgs(1),
SilenceUsage: true,
Aliases: []string{"rm"},
RunE: func(cmd *cobra.Command, args []string) error {
gv, err := gitvolume.New(gitvolume.Options{Quiet: quiet})
if err != nil {
return err
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the repository's style guide, errors should be wrapped to add context. The error returned from gitvolume.New is returned directly. Please wrap it using fmt.Errorf to provide more information about where the error originated.

return fmt.Errorf("failed to initialize git-volume: %w", err)
References
  1. Rule feat: global add command #17 of the repository style guide (fmt.Errorf("...: %w", err)를 사용하여 에러에 컨텍스트를 추가(Wrap)합니다.) requires wrapping errors with fmt.Errorf and %w to add context, which improves traceability and debugging. (link)

}

return gv.GlobalRemove(args)
},
}

func init() {
globalCmd.AddCommand(globalRemoveCmd)
}
Binary file added gv
Binary file not shown.
87 changes: 87 additions & 0 deletions internal/gitvolume/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package gitvolume

import (
"errors"
"fmt"
"os"
"path/filepath"
)

// GlobalRemove removes files from the global git-volume directory
func (g *GitVolume) GlobalRemove(files []string) error {
if err := g.beforeAllRemove(); err != nil {
return err
}

var errs []error
for _, file := range files {
target, err := g.beforeRemove(file)
if err == nil {
err = g.remove(target)
}
g.afterRemove(file, err, &errs)
}

return g.afterAllRemove(errs)
}

func (g *GitVolume) beforeAllRemove() error {
if _, err := os.Stat(g.ctx.GlobalDir); os.IsNotExist(err) {
return fmt.Errorf("global storage not initialized")
}
return nil
}

func (g *GitVolume) afterAllRemove(errs []error) error {
if len(errs) > 0 && !g.quiet {
fmt.Printf("❌ Global remove completed with %d error(s)\n", len(errs))
}
return errors.Join(errs...)
}

func (g *GitVolume) beforeRemove(file string) (string, error) {
globalDir := g.ctx.GlobalDir

// Target path is directly relative to globalDir
targetPath := filepath.Join(globalDir, file)

// Security check: ensure targetPath is within globalDir
if err := verifyPathWithinBase(targetPath, globalDir); err != nil {
return "", fmt.Errorf("invalid path %s: %w", file, err)
}

// Check existence
if _, err := os.Lstat(targetPath); os.IsNotExist(err) {
return "", fmt.Errorf("not found: %s", file)
} else if err != nil {
return "", fmt.Errorf("failed to stat %s: %w", file, err)
}

return targetPath, nil
}

func (g *GitVolume) remove(file string) error {
// Remove
if err := os.RemoveAll(file); err != nil {
return fmt.Errorf("failed to remove: %w", err)
}

// Cleanup empty parents
cleanEmptyParents(filepath.Dir(file), g.ctx.GlobalDir)
Comment thread
laggu marked this conversation as resolved.

return nil
}

func (g *GitVolume) afterRemove(file string, err error, errs *[]error) {
if err != nil {
if !g.quiet {
fmt.Printf("❌ Failed to remove %s: %v\n", file, err)
}
*errs = append(*errs, err)
return
}

if !g.quiet {
fmt.Printf("✓ Removed %s\n", file)
}
}
60 changes: 60 additions & 0 deletions test/integration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,66 @@ else
fail "global list missing tree connectors"
fi

# -----------------------------------------------------------------------------
# Test: global remove
# -----------------------------------------------------------------------------
log "TEST" "Testing 'global remove' command..."

# Helper for global remove test
mkdir -p "$TEST_DIR/.git-volume/subdir"
touch "$TEST_DIR/.git-volume/file1"
touch "$TEST_DIR/.git-volume/subdir/file2"
touch "$TEST_DIR/.git-volume/subdir/file3"

# Test 1: Remove single file
"$GV_BIN" global remove file1
if [[ ! -f "$TEST_DIR/.git-volume/file1" ]]; then
pass "global remove removed single file"
else
fail "global remove failed to remove single file"
fi

# Test 2: Remove file in subdir
"$GV_BIN" global remove subdir/file2
if [[ ! -f "$TEST_DIR/.git-volume/subdir/file2" ]]; then
pass "global remove removed file in subdir"
else
fail "global remove failed to remove file in subdir"
fi

# Test 3: Ensure subdir still exists (file3 remains)
if [[ -d "$TEST_DIR/.git-volume/subdir" ]]; then
pass "global remove preserved non-empty subdir"
else
fail "global remove removed non-empty subdir"
fi

# Test 4: Remove last file in subdir (expect subdir cleanup)
"$GV_BIN" global remove subdir/file3
if [[ ! -f "$TEST_DIR/.git-volume/subdir/file3" ]]; then
if [[ ! -d "$TEST_DIR/.git-volume/subdir" ]]; then
pass "global remove cleanup empty parent directory"
else
fail "global remove failed to cleanup empty parent directory"
fi
else
fail "global remove failed to remove last file"
fi

# Test 5: Try to remove non-existent file
if ! "$GV_BIN" global remove non_existent >/dev/null 2>&1; then
pass "global remove failed for non-existent file (expected)"
else
fail "global remove succeeded for non-existent file (unexpected)"
fi

# Test 6: Path traversal attempt
if ! "$GV_BIN" global remove ../outside_file >/dev/null 2>&1; then
pass "global remove blocked path traversal (expected)"
else
fail "global remove allowed path traversal (unexpected)"
fi

# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
Expand Down