Skip to content

Commit

Permalink
Add 'find-file' command
Browse files Browse the repository at this point in the history
Sometimes it is useful to find specific files inside repository
directories. This commit adds a `ggman find-file` command that allows to
search for files within repository directories.
  • Loading branch information
tkw1536 committed May 30, 2024
1 parent 5c33d3c commit 6ff8601
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 2 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ggman

<!-- spellchecker:words ggman ggroot shellrc ggcd ggclone ggshow ggcode ggnorm wrld cspec gopath godoc goprogram unsynced jessevdk struct POSIX pflag localgodoc -->
<!-- spellchecker:words ggman ggroot shellrc ggcd ggclone ggshow ggcode ggnorm wrld cspec gopath godoc goprogram unsynced jessevdk struct POSIX pflag localgodoc CANSPEC CANFILE worktree reclone testutil subpackage -->

![CI Status](https://github.com/tkw1536/ggman/workflows/CI/badge.svg)

Expand Down Expand Up @@ -383,7 +383,16 @@ git 2.28 introduced the `init.defaultBranch` option to set the name of the defau
However this does not affect existing repositories.

To find repositories with an old branch, the `ggman find-branch` command can be used.
It takes a single argument (a branch name), and finds all repositories that contain a branch with the given name.
It takes a single argument (a branch name), and finds all repositories that contain a branch with the given name.

### 'ggman find-file'

Sometimes it is useful to find specific files inside repository directories.
This can be used to e.g. detect repositories of a specific language.

For this purpose the `ggman find-file` command can be used.
It takes a single argument (a file name), and finds all repository directories that contain a file with the given path.
For example, use `ggman find-file package.json` to find all repositories with a `package.json`.

### 'ggman sweep'

Expand Down Expand Up @@ -426,6 +435,7 @@ ggman comes with the following builtin aliases:

### 1.21.0 (Upcoming)

- add `ggman find-file` command
- Various internal performance tweaks
- make spellchecker happy
- Update to `go 1.22`
Expand Down
92 changes: 92 additions & 0 deletions cmd/find_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package cmd

//spellchecker:words path filepath github ggman goprogram exit pkglib
import (
"path/filepath"

"github.com/tkw1536/ggman"
"github.com/tkw1536/ggman/env"
"github.com/tkw1536/goprogram/exit"
"github.com/tkw1536/pkglib/fsx"
)

//spellchecker:words positionals

// FindFile is the 'ggman find-file' command.
//
// The 'find-file' command lists all repositories that currently contain a file or directory with the provided name.
// The provided path may be relative to the root of the repository.
//
// --exit-code
//
// When provided, exit with code 1 if no repositories are found.
//
// --print-file
//
// Instead of listing the repository paths, print the filepath instead.
var FindFile ggman.Command = findFile{}

type findFile struct {
Positionals struct {
Path string `required:"1-1" positional-arg-name:"PATH" description:"name (or path) file to find"`
} `positional-args:"true"`
PrintFilePath bool `short:"p" long:"print-file" description:"instead of printing the repository paths, print the file paths"`
ExitCode bool `short:"e" long:"exit-code" description:"exit with status code 1 when no repositories with provided file exist"`
}

func (findFile) Description() ggman.Description {
return ggman.Description{
Command: "find-file",
Description: "list repositories containing a specific file",

Requirements: env.Requirement{
NeedsRoot: true,
},
}
}

func (f findFile) AfterParse() error {
if !filepath.IsLocal(f.Positionals.Path) {
return errFindFileNotLocal
}
return nil
}

var errFindFileCustom = exit.Error{
ExitCode: exit.ExitGeneric,
}

var errFindFileNotLocal = exit.Error{
ExitCode: exit.ExitCommandArguments,
Message: "path argument is not a local path",
}

func (f findFile) Run(context ggman.Context) error {
foundRepo := false
for _, repo := range context.Environment.Repos(true) {

candidate := filepath.Join(repo, f.Positionals.Path)
ok, err := fsx.Exists(candidate)
if err != nil {
panic(err)
}
if !ok {
continue
}

foundRepo = true
if f.PrintFilePath {
context.Println(candidate)
} else {
context.Println(repo)
}
}

// if we have --exit-code set and no results
// we need to exit with an error code
if f.ExitCode && !foundRepo {
return errFindFileCustom
}

return nil
}
124 changes: 124 additions & 0 deletions cmd/find_file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package cmd

//spellchecker:words path filepath testing github ggman internal mockenv
import (
"os"
"path/filepath"
"testing"

"github.com/tkw1536/ggman/internal/mockenv"
)

//spellchecker:words GGROOT workdir

func TestCommandFindFile(t *testing.T) {
mock := mockenv.NewMockEnv(t)

// with file 'example.txt'
{
clonePath := mock.Clone("https://github.com/hello/world.git", "github.com", "hello", "world")
if err := os.WriteFile(filepath.Join(clonePath, "example.txt"), nil, os.ModePerm); err != nil {
panic(err)
}
}

// with file 'example/example.txt'
{
clonePath := mock.Clone("[email protected]/repo", "server.com", "user", "repo")
if err := os.Mkdir(filepath.Join(clonePath, "example"), os.ModePerm|os.ModeDir); err != nil {
panic(err)
}
if err := os.WriteFile(filepath.Join(clonePath, "example", "example.txt"), nil, os.ModePerm); err != nil {
panic(err)
}

}

// with nothing
mock.Clone("https://gitlab.com/hello/world.git", "gitlab.com", "hello", "world")

tests := []struct {
name string
workdir string
args []string

wantCode uint8
wantStdout string
wantStderr string
}{
{
"find example.txt file",
"",
[]string{"find-file", "example.txt"},

0,
"${GGROOT github.com hello world}\n",
"",
},
{
"find example.txt file with paths",
"",
[]string{"find-file", "--print-file", "example.txt"},

0,
"${GGROOT github.com hello world example.txt}\n",
"",
},
{
"find example directory",
"",
[]string{"find-file", "example"},

0,
"${GGROOT server.com user repo}\n",
"",
},
{
"find example/example.txt file",
"",
[]string{"find-file", "example/example.txt"},

0,
"${GGROOT server.com user repo}\n",
"",
},
{
"don't find non-existent file",
"",
[]string{"find-file", "iDoNotExist.txt"},

0,
"",
"",
},
{
"don't find non-existent file with exit code",
"",
[]string{"find-file", "--exit-code", "iDoNotExist.txt"},

1,
"",
"",
},
{
"find existent file with exit code",
"",
[]string{"find-file", "--exit-code", "example.txt"},

0,
"${GGROOT github.com hello world}\n",
"",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
code, stdout, stderr := mock.Run(FindFile, tt.workdir, "", tt.args...)
if code != tt.wantCode {
t.Errorf("Code = %d, wantCode = %d", code, tt.wantCode)
}
mock.AssertOutput(t, "Stdout", stdout, tt.wantStdout)
mock.AssertOutput(t, "Stderr", stderr, tt.wantStderr)
})
}
}
1 change: 1 addition & 0 deletions cmd/ggman/ggman.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func init() {
cmd.Exec,
cmd.Fetch,
cmd.FindBranch,
cmd.FindFile,
cmd.Fix,
cmd.Here,
cmd.License,
Expand Down

0 comments on commit 6ff8601

Please sign in to comment.