26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
162 lines
3.9 KiB
Go
162 lines
3.9 KiB
Go
package intent
|
|
|
|
import (
|
|
"context"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/kunchenguid/no-mistakes/internal/winproc"
|
|
)
|
|
|
|
// resolveHome returns the home directory to use, preferring an explicit
|
|
// override (used in tests) over the OS-reported value.
|
|
func resolveHome(override string) (string, error) {
|
|
if override != "" {
|
|
return override, nil
|
|
}
|
|
return os.UserHomeDir()
|
|
}
|
|
|
|
// canonicalPath returns the path with symlinks evaluated and cleaned. It
|
|
// silently falls back to filepath.Clean(abs) if EvalSymlinks fails (e.g.
|
|
// the path does not exist), since both sides of the comparison receive
|
|
// the same fallback treatment.
|
|
func canonicalPath(p string) string {
|
|
if p == "" {
|
|
return ""
|
|
}
|
|
abs, err := filepath.Abs(p)
|
|
if err != nil {
|
|
abs = p
|
|
}
|
|
if resolved, err := filepath.EvalSymlinks(abs); err == nil {
|
|
return filepath.Clean(resolved)
|
|
}
|
|
return filepath.Clean(abs)
|
|
}
|
|
|
|
// pathsEqual compares two paths after canonicalization.
|
|
func pathsEqual(a, b string) bool {
|
|
return canonicalPath(a) == canonicalPath(b)
|
|
}
|
|
|
|
type repoMatcher struct {
|
|
origin string
|
|
ids map[string]repoIdentity
|
|
}
|
|
|
|
type repoIdentity struct {
|
|
canonical string
|
|
commonDir string
|
|
remote string
|
|
}
|
|
|
|
func newRepoMatcher(ctx context.Context, originCWD string) *repoMatcher {
|
|
m := &repoMatcher{
|
|
origin: canonicalPath(originCWD),
|
|
ids: make(map[string]repoIdentity),
|
|
}
|
|
if m.origin != "" {
|
|
m.ids[m.origin] = gitRepoIdentity(ctx, originCWD)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (m *repoMatcher) matches(ctx context.Context, cwd string) bool {
|
|
if m == nil || m.origin == "" {
|
|
return true
|
|
}
|
|
candidate := canonicalPath(cwd)
|
|
if candidate == "" {
|
|
return false
|
|
}
|
|
if candidate == m.origin {
|
|
return true
|
|
}
|
|
origin := m.ids[m.origin]
|
|
candidateID, ok := m.ids[candidate]
|
|
if !ok {
|
|
candidateID = gitRepoIdentity(ctx, cwd)
|
|
m.ids[candidate] = candidateID
|
|
}
|
|
if origin.commonDir != "" && candidateID.commonDir != "" && origin.commonDir == candidateID.commonDir {
|
|
return true
|
|
}
|
|
return origin.remote != "" && candidateID.remote != "" && origin.remote == candidateID.remote
|
|
}
|
|
|
|
func gitRepoIdentity(ctx context.Context, dir string) repoIdentity {
|
|
id := repoIdentity{canonical: canonicalPath(dir)}
|
|
if id.canonical == "" {
|
|
return id
|
|
}
|
|
if commonDir := gitOutput(ctx, dir, "rev-parse", "--git-common-dir"); commonDir != "" {
|
|
if !filepath.IsAbs(commonDir) {
|
|
commonDir = filepath.Join(dir, commonDir)
|
|
}
|
|
id.commonDir = canonicalPath(commonDir)
|
|
}
|
|
remote := gitOutput(ctx, dir, "remote", "get-url", "origin")
|
|
if remote == "" {
|
|
if first := firstLine(gitOutput(ctx, dir, "remote")); first != "" {
|
|
remote = gitOutput(ctx, dir, "remote", "get-url", first)
|
|
}
|
|
}
|
|
id.remote = normalizeGitRemote(remote)
|
|
return id
|
|
}
|
|
|
|
func gitOutput(ctx context.Context, dir string, args ...string) string {
|
|
cmd := exec.CommandContext(ctx, "git", append([]string{"-C", dir}, args...)...)
|
|
winproc.Harden(cmd)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func normalizeGitRemote(remote string) string {
|
|
remote = strings.TrimSpace(remote)
|
|
if remote == "" {
|
|
return ""
|
|
}
|
|
if u, err := url.Parse(remote); err == nil && u.Host != "" {
|
|
return cleanRemoteParts(u.Host, u.Path)
|
|
}
|
|
if at := strings.Index(remote, "@"); at >= 0 {
|
|
rest := remote[at+1:]
|
|
if colon := strings.Index(rest, ":"); colon >= 0 {
|
|
return cleanRemoteParts(rest[:colon], rest[colon+1:])
|
|
}
|
|
}
|
|
if filepath.IsAbs(remote) {
|
|
return canonicalPath(remote)
|
|
}
|
|
return cleanRemoteParts("", remote)
|
|
}
|
|
|
|
func cleanRemoteParts(host, path string) string {
|
|
host = strings.ToLower(strings.TrimSpace(host))
|
|
path = strings.Trim(strings.TrimSpace(path), "/")
|
|
path = strings.TrimSuffix(path, ".git")
|
|
path = strings.ToLower(path)
|
|
if host == "" {
|
|
return path
|
|
}
|
|
if path == "" {
|
|
return host
|
|
}
|
|
return host + "/" + path
|
|
}
|
|
|
|
func firstLine(s string) string {
|
|
if i := strings.IndexByte(s, '\n'); i >= 0 {
|
|
return strings.TrimSpace(s[:i])
|
|
}
|
|
return strings.TrimSpace(s)
|
|
}
|