Skip to content
Open
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
58 changes: 58 additions & 0 deletions cmd_check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package main

import (
"fmt"
"strings"
"sync"

"github.com/x-motemen/ghq/cmdutil"
"github.com/urfave/cli/v2"
)

func doCheck(c *cli.Context) error {
var (
w = c.App.Writer
vcsBackend = c.String("vcs")
)

var (
repos []*LocalRepository
mu sync.Mutex
)
if err := walkLocalRepositories(vcsBackend, func(repo *LocalRepository) {
mu.Lock()
defer mu.Unlock()
repos = append(repos, repo)
}); err != nil {
return fmt.Errorf("failed to get repositories: %w", err)
}

for _, repo := range repos {
out, err := cmdutil.RunAndGetOutput("git", "-C", repo.FullPath, "status", "--porcelain")
if err != nil {
// Handle cases where the directory is not a git repository
continue
}
stashes, _ := cmdutil.RunAndGetOutput("git", "-C", repo.FullPath, "stash", "list")

if strings.TrimSpace(out) == "" && strings.TrimSpace(stashes) == "" {
continue
}

fmt.Fprintln(w, repo.RelPath)
if strings.TrimSpace(out) != "" {
fmt.Fprintln(w, " Uncommitted changes:")
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
fmt.Fprintln(w, " "+line)
}
}
if strings.TrimSpace(stashes) != "" {
fmt.Fprintln(w, " Stashes:")
for _, line := range strings.Split(strings.TrimSpace(stashes), "\n") {
fmt.Fprintln(w, " "+line)
}
}
}

return nil
}
55 changes: 55 additions & 0 deletions cmd_check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/x-motemen/ghq/cmdutil"
)

func TestCommandCheck(t *testing.T) {
tmpdir := newTempDir(t)
t.Setenv("GHQ_ROOT", tmpdir)

// Create a clean repository
cleanRepoPath := filepath.Join(tmpdir, "github.com", "user", "clean")
os.MkdirAll(cleanRepoPath, 0755)
cmdutil.Run("git", "-C", cleanRepoPath, "init")
cmdutil.Run("git", "-C", cleanRepoPath, "commit", "--allow-empty", "-m", "initial commit")

// Create a repository with uncommitted changes
dirtyRepoPath := filepath.Join(tmpdir, "github.com", "user", "dirty")
os.MkdirAll(dirtyRepoPath, 0755)
cmdutil.Run("git", "-C", dirtyRepoPath, "init")
os.WriteFile(filepath.Join(dirtyRepoPath, "file.txt"), []byte("hello"), 0644)

// Create a repository with a stash
stashedRepoPath := filepath.Join(tmpdir, "github.com", "user", "stashed")
os.MkdirAll(stashedRepoPath, 0755)
cmdutil.Run("git", "-C", stashedRepoPath, "init")
os.WriteFile(filepath.Join(stashedRepoPath, "file.txt"), []byte("hello"), 0644)
cmdutil.Run("git", "-C", stashedRepoPath, "add", "file.txt")
cmdutil.Run("git", "-C", stashedRepoPath, "stash")

out, _, err := capture(func() {
newApp().Run([]string{"ghq", "check"})
})

if err != nil {
t.Errorf("error should be nil, but: %v", err)
}

if strings.Contains(out, "clean") {
t.Errorf("clean repository should not be listed")
}

if !strings.Contains(out, "dirty") {
t.Errorf("dirty repository should be listed")
}

if !strings.Contains(out, "stashed") {
t.Errorf("stashed repository should be listed")
}
}
10 changes: 10 additions & 0 deletions cmdutil/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ func RunInDirSilently(dir, command string, args ...string) error {
return RunCommand(cmd, true)
}

// RunAndGetOutput runs the command and returns its output
func RunAndGetOutput(command string, args ...string) (string, error) {
cmd := exec.Command(command, args...)
output, err := cmd.CombinedOutput()
if err != nil {
return "", &RunError{cmd, err}
}
return string(output), nil
}

// RunFunc for the type command execution
type RunFunc func(*exec.Cmd) error

Expand Down
40 changes: 24 additions & 16 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ var commands = []*cli.Command{
commandRm,
commandRoot,
commandCreate,
commandCheck,
}

var commandGet = &cli.Command{
Name: "get",
Name: "get",
Aliases: []string{"clone"},
Usage: "Clone/sync with a remote repository",
Usage: "Clone/sync with a remote repository",
Description: `
Clone a repository under ghq root directory. If the repository is
already cloned to local, nothing will happen unless '-u' ('--update')
Expand All @@ -40,7 +41,7 @@ var commandGet = &cli.Command{
&cli.BoolFlag{Name: "parallel", Aliases: []string{"P"}, Usage: "Import parallelly"},
&cli.BoolFlag{Name: "bare", Usage: "Do a bare clone"},
&cli.StringFlag{
Name: "partial",
Name: "partial",
Usage: "Do a partial clone. Can specify either \"blobless\" or \"treeless\"",
Action: func(ctx *cli.Context, v string) error {
expected := []string{"blobless", "treeless"}
Expand All @@ -53,7 +54,7 @@ var commandGet = &cli.Command{
}

var commandList = &cli.Command{
Name: "list",
Name: "list",
Usage: "List local repositories",
Description: `
List locally cloned repositories. If a query argument is given, only
Expand All @@ -71,27 +72,33 @@ var commandList = &cli.Command{
},
}

var commandCheck = &cli.Command{
Name: "check",
Usage: "Check for uncommitted changes, stashes, and untracked files",
Action: doCheck,
}

var commandRm = &cli.Command{
Name: "rm",
Usage: "Remove local repository",
Name: "rm",
Usage: "Remove local repository",
Action: doRm,
Flags: []cli.Flag{
&cli.BoolFlag{Name: "dry-run", Usage: "Do not remove actually"},
},
}

var commandRoot = &cli.Command{
Name: "root",
Usage: "Show repositories' root",
Name: "root",
Usage: "Show repositories' root",
Action: doRoot,
Flags: []cli.Flag{
&cli.BoolFlag{Name: "all", Usage: "Show all roots"},
},
}

var commandCreate = &cli.Command{
Name: "create",
Usage: "Create a new repository",
Name: "create",
Usage: "Create a new repository",
Action: doCreate,
Flags: []cli.Flag{
&cli.StringFlag{Name: "vcs", Usage: "Specify `vcs` backend explicitly"},
Expand All @@ -100,16 +107,17 @@ var commandCreate = &cli.Command{
}

type commandDoc struct {
Parent string
Arguments string
Parent string
Arguments string
}

var commandDocs = map[string]commandDoc{
"get": {"", "[-u] [-p] [--shallow] [--vcs <vcs>] [--look] [--silent] [--branch <branch>] [--no-recursive] [--bare] [--partial blobless|treeless] <repository URL>|<project>|<user>/<project>|<host>/<user>/<project>"},
"list": {"", "[-p] [-e] [<query>]"},
"get": {"", "[-u] [-p] [--shallow] [--vcs <vcs>] [--look] [--silent] [--branch <branch>] [--no-recursive] [--bare] [--partial blobless|treeless] <repository URL>|<project>|<user>/<project>|<host>/<user>/<project>"},
"list": {"", "[-p] [-e] [<query>]"},
"check": {"", ""},
"create": {"", "<project>|<user>/<project>|<host>/<user>/<project>"},
"rm": {"", "<project>|<user>/<project>|<host>/<user>/<project>"},
"root": {"", "[-all]"},
"rm": {"", "<project>|<user>/<project>|<host>/<user>/<project>"},
"root": {"", "[-all]"},
}

// Makes template conditionals to generate per-command documents.
Expand Down