• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CyberDuck79 / duckfile / 17051696605

18 Aug 2025 08:23PM UTC coverage: 72.923% (+0.9%) from 72.058%
17051696605

Pull #52

github

CyberDuck79
feat: extract and improve logging system

- Create new internal/log package with exported logging functions
- Update all CLI commands to use new log package for log level management
- Add informative logging to git operations (clone, fetch, checkout)
- Add debug logging to config loading and validation
- Improve .env file loading to use proper logging instead of silent mode
- Remove unnecessary logging wrapper files from run package
- Update run package to use log package directly

The logging system is now centralized and eliminates unnecessary indirection,
providing better user feedback for git operations, config loading,
and environment setup.
Pull Request #52: feat: Add .env file support and improve logging system

163 of 192 new or added lines in 9 files covered. (84.9%)

1 existing line in 1 file now uncovered.

1185 of 1625 relevant lines covered (72.92%)

9.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

34.72
/internal/git/git.go
1
package git
2

3
import (
4
        "fmt"
5
        "os/exec"
6
        "path/filepath"
7
        "regexp"
8
        "strings"
9

10
        "github.com/CyberDuck79/duckfile/internal/log"
11
)
12

13
// CloneInto clones/fetches repo@ref into cacheDir/repo and checks out the ref in the workdir.
14
// Returns the workdir path with the working tree set to the requested ref (detached HEAD).
15
func CloneInto(repo, ref, cacheDir string) (string, error) {
×
16
        workdir := filepath.Join(cacheDir, "repo") // 1-repo MVP, improve later
×
17

×
18
        // Already cloned?
×
19
        if _, err := exec.Command("test", "-d", filepath.Join(workdir, ".git")).CombinedOutput(); err == nil {
×
NEW
20
                log.Infof("Repository already exists at %s, updating...", workdir)
×
21
                // Fetch the desired ref and checkout FETCH_HEAD (detached)
×
NEW
22
                log.Debugf("Fetching ref %s from %s", ref, repo)
×
23
                if out, err := exec.Command("git", "-C", workdir, "fetch", "--depth", "1", "origin", ref).CombinedOutput(); err != nil {
×
24
                        return "", fmt.Errorf("git fetch failed: %v: %s", err, string(out))
×
25
                }
×
NEW
26
                log.Debugf("Checking out ref %s", ref)
×
27
                if out, err := exec.Command("git", "-C", workdir, "checkout", "--force", "--detach", "FETCH_HEAD").CombinedOutput(); err != nil {
×
28
                        return "", fmt.Errorf("git checkout failed: %v: %s", err, string(out))
×
29
                }
×
NEW
30
                log.Infof("Successfully updated repository to %s", ref)
×
31
        } else {
×
NEW
32
                log.Infof("Cloning repository %s to %s", repo, workdir)
×
33
                // Fresh clone, then force checkout the ref (supports branch, tag, or commit)
×
34
                if out, err := exec.Command("git", "clone", "--depth", "1", repo, workdir).CombinedOutput(); err != nil {
×
35
                        return "", fmt.Errorf("git clone failed: %v: %s", err, string(out))
×
36
                }
×
37
                // Ensure we have the ref and check it out detached
NEW
38
                log.Debugf("Fetching ref %s", ref)
×
39
                if out, err := exec.Command("git", "-C", workdir, "fetch", "--depth", "1", "origin", ref).CombinedOutput(); err != nil {
×
40
                        return "", fmt.Errorf("git fetch failed: %v: %s", err, string(out))
×
41
                }
×
NEW
42
                log.Debugf("Checking out ref %s", ref)
×
43
                if out, err := exec.Command("git", "-C", workdir, "checkout", "--force", "--detach", "FETCH_HEAD").CombinedOutput(); err != nil {
×
44
                        return "", fmt.Errorf("git checkout failed: %v: %s", err, string(out))
×
45
                }
×
NEW
46
                log.Infof("Successfully cloned and checked out %s", ref)
×
47
        }
48
        return workdir, nil
×
49
}
50

51
// GetCurrentCommitHash returns the commit hash of the currently checked out ref in the given directory.
52
// Returns the full 40-character SHA-1 hash.
53
func GetCurrentCommitHash(workdir string) (string, error) {
2✔
54
        out, err := exec.Command("git", "-C", workdir, "rev-parse", "HEAD").CombinedOutput()
2✔
55
        if err != nil {
4✔
56
                return "", fmt.Errorf("git rev-parse HEAD failed: %v: %s", err, string(out))
2✔
57
        }
2✔
58
        hash := strings.TrimSpace(string(out))
×
59
        if len(hash) != 40 {
×
60
                return "", fmt.Errorf("invalid commit hash length: got %d characters, expected 40", len(hash))
×
61
        }
×
62
        return hash, nil
×
63
}
64

65
// GetRemoteCommitHash fetches the remote ref and returns its commit hash without checking it out.
66
// This function is used to check if the remote has changed since the last cache.
67
// If network fails, returns an error that can be handled gracefully by the caller.
68
func GetRemoteCommitHash(repo, ref string) (string, error) {
3✔
69
        log.Debugf("Checking remote commit hash for %s@%s", repo, ref)
3✔
70
        // Use ls-remote to get the commit hash without cloning/fetching
3✔
71
        out, err := exec.Command("git", "ls-remote", repo, ref).CombinedOutput()
3✔
72
        if err != nil {
5✔
73
                return "", fmt.Errorf("git ls-remote failed (network or repository error): %v: %s", err, string(out))
2✔
74
        }
2✔
75

76
        output := strings.TrimSpace(string(out))
1✔
77
        if output == "" {
2✔
78
                return "", fmt.Errorf("ref %q not found in repository %q", ref, repo)
1✔
79
        }
1✔
80

81
        // Parse output: "commit_hash\trefs/heads/branch" or "commit_hash\tHEAD"
82
        lines := strings.Split(output, "\n")
×
83
        for _, line := range lines {
×
84
                parts := strings.Split(line, "\t")
×
85
                if len(parts) >= 2 {
×
86
                        hash := strings.TrimSpace(parts[0])
×
87
                        if len(hash) == 40 && isValidCommitHash(hash) {
×
NEW
88
                                log.Debugf("Remote commit hash for %s@%s: %s", repo, ref, hash[:8])
×
89
                                return hash, nil
×
90
                        }
×
91
                }
92
        }
93

94
        return "", fmt.Errorf("could not parse commit hash from ls-remote output: %s", output)
×
95
}
96

97
// IsCommitHash checks if the given ref is already a commit hash (40-character hex string).
98
// This is used to validate configuration - if ref is already a commit hash,
99
// commit hash tracking doesn't make sense since commit hashes don't change.
100
func IsCommitHash(ref string) bool {
17✔
101
        return len(ref) == 40 && isValidCommitHash(ref)
17✔
102
}
17✔
103

104
// isValidCommitHash checks if a string is a valid 40-character hexadecimal hash
105
func isValidCommitHash(hash string) bool {
13✔
106
        if len(hash) != 40 {
14✔
107
                return false
1✔
108
        }
1✔
109
        matched, _ := regexp.MatchString("^[a-fA-F0-9]{40}$", hash)
12✔
110
        return matched
12✔
111
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc