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
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,10 @@ jobs:
- name: Install GPG
run: sudo apt-get update && sudo apt-get install -y gnupg

- name: Configure Git
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"

- name: Run tests
run: cd pass && go test ./...
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ shell/gitprompt
shell/gitprompt-linux-amd64
shell/gitprompt-linux-arm64
shell/gitprompt-windows-arm64.exe
shell/pass
*/*.exe
25 changes: 25 additions & 0 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ Windows-compatible `pass` (password-store) replacement in Go. Manages GPG-encryp

- GPG-encrypted password storage
- Git integration for version control
- **New: Git status and operations** (`pass git`, `pass git push`, `pass git update`)
- Interactive fuzzy search with TUI (Bubble Tea)
- Git status displayed in TUI
- Cross-platform (Windows, Linux, macOS)
- Copy to clipboard support
- Edit existing passwords with your favorite editor
Expand Down Expand Up @@ -47,6 +49,7 @@ go test ./...

### Usage
```bash
# Password management
pass ls # List passwords (files only)
pass find gmail # Search
pass email/gmail.com/user # Show password
Expand All @@ -57,6 +60,14 @@ pass edit # Interactive fuzzy search to edit
pass # Interactive fuzzy search TUI
pass -c # Interactive fuzzy search with clipboard copy
pass rm # Interactive fuzzy search to remove

# Git integration (new!)
pass git # Show git status of password store
pass git -v # Show detailed git status
pass git push # Push changes to remote
pass git update # Pull changes from remote
pass git init # Initialize git repository
pass git status # Alias for 'pass git'
```

### TUI
Expand All @@ -69,6 +80,20 @@ When running `pass`, `pass -c`, `pass rm`, or `pass edit` without arguments, an
- Press Enter to select (show, copy, remove, or edit based on command)
- Press Esc, Ctrl+C, Ctrl+D, or Ctrl+Q to exit

**Git operations in TUI:**
- Git status is displayed in the header with colors (e.g., `Git: main <` in yellow)
- `Ctrl+P` - Push changes to remote
- `Ctrl+U` - Pull changes from remote
- `Ctrl+R` - Refresh git status

**Status symbols and colors:**
- `*` = uncommitted changes (yellow)
- `!` = merge conflicts (red)
- `>` = ahead of remote (yellow)
- `<` = behind remote (yellow)
- `<>` = diverged - both ahead and behind (yellow)
- `=` = up to date (green)

### Configuration

- `PASSWORD_STORE_DIR`: Custom password store location (default: `~/.password-store`)
Expand Down
Empty file modified pass/build.sh
100644 β†’ 100755
Empty file.
211 changes: 211 additions & 0 deletions pass/cmd/git.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package cmd

import (
"fmt"
"os"

"github.com/mandu/tools/pass/pkg/git"
"github.com/spf13/cobra"
)

// GitStatus represents the git status display information
type GitStatus struct {
IsGitRepo bool
Branch string
Remote string
TrackingBranch string
Ahead int
Behind int
HasUncommitted bool
IsClean bool
Error error
}

var gitCmd = &cobra.Command{
Use: "git [command]",
Short: "Manage git repository for the password store",
Long: `Manage the git repository for your password store.

This command provides git integration for the password store, allowing you
to check the sync status and perform git operations.

If called without a subcommand, it shows the current git status.

Usage:
pass git Show git status
pass git push Push changes to remote
pass git update Pull changes from remote
pass git status Show git status (alias for 'pass git')

Examples:
pass git # Show git status
pass git push # Push changes to remote
pass git update # Pull changes from remote
pass git -v # Show detailed git status`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
// If no subcommand, show status
return runGitStatus(cmd, args)
},
}

var gitStatusCmd = &cobra.Command{
Use: "status",
Short: "Show git status of the password store",
Long: "Show the git status of the password store, including branch, sync status, and uncommitted changes.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runGitStatus(cmd, args)
},
}

var gitPushCmd = &cobra.Command{
Use: "push",
Short: "Push changes to the remote repository",
Long: `Push local changes to the configured remote repository.

This command pushes all committed changes to the remote repository.
Uncommitted changes will not be pushed.

Examples:
pass git push # Push changes to remote`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
storeDir := GetPasswordStoreDir()

// Check if store exists
if _, err := os.Stat(storeDir); os.IsNotExist(err) {
return fmt.Errorf("pass: password store does not exist. Use 'pass insert' to create it.")
}

// Check if it's a git repo
if _, err := os.Stat(storeDir + "/.git"); os.IsNotExist(err) {
return fmt.Errorf("pass: password store is not a git repository. Use 'pass git init' or initialize git manually.")
}

fmt.Println("Pushing changes to remote...")
if err := git.Push(storeDir); err != nil {
return fmt.Errorf("pass: git push failed: %v", err)
}
fmt.Println("Successfully pushed changes to remote.")
return nil
},
}

var gitUpdateCmd = &cobra.Command{
Use: "update",
Short: "Pull changes from the remote repository",
Long: `Pull changes from the configured remote repository.

This command fetches changes from the remote and merges them into your
local password store.

Examples:
pass git update # Pull changes from remote`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
storeDir := GetPasswordStoreDir()

// Check if store exists
if _, err := os.Stat(storeDir); os.IsNotExist(err) {
return fmt.Errorf("pass: password store does not exist. Use 'pass insert' to create it.")
}

// Check if it's a git repo
if _, err := os.Stat(storeDir + "/.git"); os.IsNotExist(err) {
return fmt.Errorf("pass: password store is not a git repository. Use 'pass git init' or initialize git manually.")
}

fmt.Println("Pulling changes from remote...")
if err := git.Update(storeDir); err != nil {
return fmt.Errorf("pass: git update failed: %v", err)
}
fmt.Println("Successfully updated from remote.")
return nil
},
}

var gitInitCmd = &cobra.Command{
Use: "init",
Short: "Initialize a git repository in the password store",
Long: `Initialize a git repository in the password store directory.

This command creates a git repository in your password store directory
if one doesn't already exist.

Examples:
pass git init # Initialize git repository`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
storeDir := GetPasswordStoreDir()

// Create store directory if it doesn't exist
if err := os.MkdirAll(storeDir, 0700); err != nil {
return fmt.Errorf("pass: failed to create password store directory: %v", err)
}

fmt.Println("Initializing git repository...")
if err := git.InitGitRepo(storeDir); err != nil {
return fmt.Errorf("pass: git init failed: %v", err)
}
fmt.Println("Successfully initialized git repository in password store.")
return nil
},
}

// runGitStatus runs the git status command
func runGitStatus(cmd *cobra.Command, args []string) error {
storeDir := GetPasswordStoreDir()

// Check if store exists
if _, err := os.Stat(storeDir); os.IsNotExist(err) {
return fmt.Errorf("pass: password store does not exist. Use 'pass insert' to create it.")
}

// Get git status
status := git.GetGitStatus(storeDir)

// Check for verbose flag
verbose, _ := cmd.Flags().GetBool("verbose")

if status.Error != nil {
if !status.IsGitRepo {
return fmt.Errorf("pass: password store is not a git repository.\nHint: Run 'pass git init' to initialize git.")
}
return fmt.Errorf("pass: failed to get git status: %v", status.Error)
}

if !status.IsGitRepo {
return fmt.Errorf("pass: password store is not a git repository.\nHint: Run 'pass git init' to initialize git.")
}

if verbose {
// Show detailed status with commit counts
fmt.Println(status.DetailedString())
} else {
// Show compact status with simple symbols only
fmt.Printf("Git status: %s\n", status.String())

// Only show "Up to date" message, no commit counts
if status.IsClean && !status.HasUncommitted && status.Ahead == 0 && status.Behind == 0 {
fmt.Println(" Up to date")
}
}

return nil
}

// addGitCmd adds the git command and its subcommands to the root command
func addGitCmd() {
// Add verbose flag to git command
gitCmd.PersistentFlags().BoolP("verbose", "v", false, "Show detailed git status information")

// Add subcommands
gitCmd.AddCommand(gitStatusCmd)
gitCmd.AddCommand(gitPushCmd)
gitCmd.AddCommand(gitUpdateCmd)
gitCmd.AddCommand(gitInitCmd)

// Add git command to root
rootCmd.AddCommand(gitCmd)
}
Loading
Loading