summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2026-01-02 00:40:21 +0700
committerShulhan <ms@kilabit.info>2026-01-06 14:56:07 +0700
commit18be916ab6f8911fd23d8f0a91f5a26f3c07f636 (patch)
treeac6d36924c851e81cd539c4b7b3ee0d307c5df74
parent988ff4596d6fb600d0c94aeed41b5b17f1677032 (diff)
downloadpakakeh.go-18be916ab6f8911fd23d8f0a91f5a26f3c07f636.tar.xz
lib/git: add Git type with method IsIgnored
The Git type is for working with single git repository. The [Git.IsIgnored] method is to check if the `path` is ignored by git. This is processed by matching it with all of the pattern in the ".gitignore" file from the path directory and its parent until the root of Git repository.
-rw-r--r--lib/git/git.go70
-rw-r--r--lib/git/git_example_test.go42
-rw-r--r--lib/git/git_test.go26
-rw-r--r--lib/git/testdata/IsIgnored/.gitignore8
-rw-r--r--lib/git/testdata/IsIgnored/foo/hello.go4
-rw-r--r--lib/git/testdata/IsIgnored/vendor/empty2
-rw-r--r--lib/git/testdata/New/fail_no_git/.gitignore2
7 files changed, 149 insertions, 5 deletions
diff --git a/lib/git/git.go b/lib/git/git.go
index 71240221..02877281 100644
--- a/lib/git/git.go
+++ b/lib/git/git.go
@@ -1,6 +1,5 @@
-// SPDX-FileCopyrightText: 2018 M. Shulhan <ms@kilabit.info>
-//
// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2018 M. Shulhan <ms@kilabit.info>
// Package git provide a wrapper for git command line interface.
package git
@@ -13,6 +12,7 @@ import (
"os"
"os/exec"
"path/filepath"
+ "strings"
"git.sr.ht/~shulhan/pakakeh.go/lib/ini"
)
@@ -28,6 +28,39 @@ var (
_stderr io.Writer = os.Stderr
)
+// Git is a type for working with single git repository.
+type Git struct {
+ listIgnore map[string]*Gitignore
+
+ // absDir define the absolute path to the repository.
+ absDir string
+}
+
+// New start working on repository in directory `dir`.
+// If the `dir` does not contains ".git" directory it will return an error,
+// even if the parent directory may contains the ".git" directory.
+func New(dir string) (git *Git, err error) {
+ var logp = `New`
+ var fi os.FileInfo
+ var pathDotGit = filepath.Join(dir, `.git`)
+ fi, err = os.Stat(pathDotGit)
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %q is not a git repository`, logp, dir)
+ }
+ if !fi.IsDir() {
+ return nil, fmt.Errorf(`%s: %q is not a git repository`, logp, dir)
+ }
+ dir, err = filepath.Abs(dir)
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+ git = &Git{
+ absDir: dir,
+ listIgnore: map[string]*Gitignore{},
+ }
+ return git, nil
+}
+
// CheckoutRevision will set the HEAD to specific revision on specific branch.
// Any untracked files and directories will be removed before checking out
// existing branch or creating new branch.
@@ -188,6 +221,39 @@ func GetTag(repoDir, revision string) (tag string, err error) {
return tag, nil
}
+// IsIgnored return true if the `path` is ignored by git.
+// This is processed by matching it with all of the patterns in the
+// ".gitignore" file inside the path directory and its parent, until the root
+// of Git repository.
+func (git *Git) IsIgnored(path string) (b bool) {
+ path = strings.TrimSpace(path)
+ if path == `` {
+ return true
+ }
+ // Traverse each directory from bottom to the top of git directory to
+ // load ".gitignore" file and match it with path.
+ var absPath = filepath.Join(git.absDir, path)
+ var dirGitignore = filepath.Dir(absPath)
+ var name = strings.TrimPrefix(absPath, dirGitignore)
+ name = strings.TrimLeft(name, `/`)
+ for strings.HasPrefix(dirGitignore, git.absDir) {
+ var ign *Gitignore
+ var ok bool
+ ign, ok = git.listIgnore[dirGitignore]
+ if !ok {
+ ign, _ = LoadGitignore(dirGitignore)
+ git.listIgnore[dirGitignore] = ign
+ }
+ if ign != nil && ign.IsIgnored(name) {
+ return true
+ }
+ dirGitignore = filepath.Dir(dirGitignore)
+ name = strings.TrimPrefix(absPath, dirGitignore)
+ name = strings.TrimLeft(name, `/`)
+ }
+ return false
+}
+
// LatestCommit get the latest commit hash in short format from "ref".
// If ref is empty, its default to "origin/master".
func LatestCommit(repoDir, ref string) (commit string, err error) {
diff --git a/lib/git/git_example_test.go b/lib/git/git_example_test.go
new file mode 100644
index 00000000..0b55a251
--- /dev/null
+++ b/lib/git/git_example_test.go
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
+
+package git_test
+
+import (
+ "fmt"
+ "log"
+
+ "git.sr.ht/~shulhan/pakakeh.go/lib/git"
+)
+
+func ExampleGit_IsIgnored() {
+ var agit *git.Git
+ var err error
+ agit, err = git.New(`testdata/IsIgnored`)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ var listPath = []string{
+ ``,
+ `vendor`,
+ `vendor/dummy`,
+ `hello.html`,
+ `hello.go`,
+ `foo/hello.go`,
+ }
+ var path string
+ var got bool
+ for _, path = range listPath {
+ got = agit.IsIgnored(path)
+ fmt.Printf("%q: %t\n", path, got)
+ }
+ // Output:
+ // "": true
+ // "vendor": true
+ // "vendor/dummy": true
+ // "hello.html": true
+ // "hello.go": false
+ // "foo/hello.go": false
+}
diff --git a/lib/git/git_test.go b/lib/git/git_test.go
index bcc63a95..70a9ccca 100644
--- a/lib/git/git_test.go
+++ b/lib/git/git_test.go
@@ -1,6 +1,5 @@
-// Copyright 2018, Shulhan <ms@kilabit.info>. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2018 M. Shulhan <ms@kilabit.info>
package git
@@ -44,6 +43,27 @@ func TestMain(m *testing.M) {
os.Exit(s)
}
+func TestNew(t *testing.T) {
+ type testCase struct {
+ dir string
+ expError string
+ }
+ var listCase = []testCase{{
+ dir: `testdata/New/fail_no_git`,
+ expError: `New: "testdata/New/fail_no_git" is not a git repository`,
+ }, {
+ dir: `testdata/New/fail_not_dir`,
+ expError: `New: "testdata/New/fail_not_dir" is not a git repository`,
+ }}
+
+ var err error
+ var tcase testCase
+ for _, tcase = range listCase {
+ _, err = New(tcase.dir)
+ test.Assert(t, `error`, tcase.expError, err.Error())
+ }
+}
+
func TestClone(t *testing.T) {
cases := []struct {
desc, dest, expErr, expStderr, expStdout string
diff --git a/lib/git/testdata/IsIgnored/.gitignore b/lib/git/testdata/IsIgnored/.gitignore
new file mode 100644
index 00000000..15989de8
--- /dev/null
+++ b/lib/git/testdata/IsIgnored/.gitignore
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
+
+# comment
+ # comment
+ vendor/ # ignore vendor directory, but not vendor file.
+/hello.* # ignore hello at root, but not foo/hello.go.
+!hello.go
diff --git a/lib/git/testdata/IsIgnored/foo/hello.go b/lib/git/testdata/IsIgnored/foo/hello.go
new file mode 100644
index 00000000..b61e7105
--- /dev/null
+++ b/lib/git/testdata/IsIgnored/foo/hello.go
@@ -0,0 +1,4 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
+
+package foo
diff --git a/lib/git/testdata/IsIgnored/vendor/empty b/lib/git/testdata/IsIgnored/vendor/empty
new file mode 100644
index 00000000..62f13a9e
--- /dev/null
+++ b/lib/git/testdata/IsIgnored/vendor/empty
@@ -0,0 +1,2 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
diff --git a/lib/git/testdata/New/fail_no_git/.gitignore b/lib/git/testdata/New/fail_no_git/.gitignore
new file mode 100644
index 00000000..62f13a9e
--- /dev/null
+++ b/lib/git/testdata/New/fail_no_git/.gitignore
@@ -0,0 +1,2 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>