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

CyberDuck79 / duckfile / 17530388730

07 Sep 2025 03:21PM UTC coverage: 79.757% (+3.6%) from 76.188%
17530388730

Pull #63

github

CyberDuck79
test(coverage): attempt to exclude some file and function from coverage
Pull Request #63: feat(template): add submodules support with recursive fetch and tests

16 of 22 new or added lines in 4 files covered. (72.73%)

6 existing lines in 2 files now uncovered.

2427 of 3043 relevant lines covered (79.76%)

9.6 hits per line

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

75.86
/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, submodules bool) (string, error) {
4✔
16
        workdir := filepath.Join(cacheDir, "repo") // 1-repo MVP, improve later
4✔
17

4✔
18
        // Already cloned?
4✔
19
        if _, err := exec.Command("test", "-d", filepath.Join(workdir, ".git")).CombinedOutput(); err == nil {
6✔
20
                log.Infof("Repository already exists at %s, updating...", workdir)
2✔
21
                // Fetch the desired ref and checkout FETCH_HEAD (detached)
2✔
22
                log.Debugf("Fetching ref %s from %s", ref, repo)
2✔
23
                if out, err := exec.Command("git", "-C", workdir, "fetch", "--depth", "1", "origin", ref).CombinedOutput(); err != nil {
2✔
24
                        return "", fmt.Errorf("git fetch failed: %v: %s", err, string(out))
×
25
                }
×
26
                log.Debugf("Checking out ref %s", ref)
2✔
27
                if out, err := exec.Command("git", "-C", workdir, "checkout", "--force", "--detach", "FETCH_HEAD").CombinedOutput(); err != nil {
2✔
28
                        return "", fmt.Errorf("git checkout failed: %v: %s", err, string(out))
×
29
                }
×
30
                if submodules {
2✔
NEW
31
                        log.Debugf("Updating submodules for %s", workdir)
×
NEW
32
                        if out, err := exec.Command("git", "-C", workdir, "submodule", "update", "--init", "--recursive").CombinedOutput(); err != nil {
×
NEW
33
                                return "", fmt.Errorf("git submodule update failed: %v: %s", err, string(out))
×
NEW
34
                        }
×
35
                }
36
                log.Infof("Successfully updated repository to %s", ref)
2✔
37
        } else {
2✔
38
                log.Infof("Cloning repository %s to %s", repo, workdir)
2✔
39
                // Fresh clone, then force checkout the ref (supports branch, tag, or commit)
2✔
40
                cloneArgs := []string{"clone", "--depth", "1"}
2✔
41
                if submodules {
3✔
42
                        cloneArgs = append(cloneArgs, "--recurse-submodules")
1✔
43
                }
1✔
44
                cloneArgs = append(cloneArgs, repo, workdir)
2✔
45
                if out, err := exec.Command("git", cloneArgs...).CombinedOutput(); err != nil {
2✔
46
                        return "", fmt.Errorf("git clone failed: %v: %s", err, string(out))
×
47
                }
×
48
                // Ensure we have the ref and check it out detached
49
                log.Debugf("Fetching ref %s", ref)
2✔
50
                if out, err := exec.Command("git", "-C", workdir, "fetch", "--depth", "1", "origin", ref).CombinedOutput(); err != nil {
2✔
51
                        return "", fmt.Errorf("git fetch failed: %v: %s", err, string(out))
×
52
                }
×
53
                log.Debugf("Checking out ref %s", ref)
2✔
54
                if out, err := exec.Command("git", "-C", workdir, "checkout", "--force", "--detach", "FETCH_HEAD").CombinedOutput(); err != nil {
2✔
55
                        return "", fmt.Errorf("git checkout failed: %v: %s", err, string(out))
×
56
                }
×
57
                if submodules {
3✔
58
                        log.Debugf("Updating submodules for %s", workdir)
1✔
59
                        if out, err := exec.Command("git", "-C", workdir, "submodule", "update", "--init", "--recursive").CombinedOutput(); err != nil {
1✔
NEW
60
                                return "", fmt.Errorf("git submodule update failed: %v: %s", err, string(out))
×
NEW
61
                        }
×
62
                }
63
                log.Infof("Successfully cloned and checked out %s", ref)
2✔
64
        }
65
        return workdir, nil
4✔
66
}
67

68
// GetCurrentCommitHash returns the commit hash of the currently checked out ref in the given directory.
69
// Returns the full 40-character SHA-1 hash.
70
func GetCurrentCommitHash(workdir string) (string, error) {
6✔
71
        out, err := exec.Command("git", "-C", workdir, "rev-parse", "HEAD").CombinedOutput()
6✔
72
        if err != nil {
8✔
73
                return "", fmt.Errorf("git rev-parse HEAD failed: %v: %s", err, string(out))
2✔
74
        }
2✔
75
        hash := strings.TrimSpace(string(out))
4✔
76
        if len(hash) != 40 {
4✔
77
                return "", fmt.Errorf("invalid commit hash length: got %d characters, expected 40", len(hash))
×
78
        }
×
79
        return hash, nil
4✔
80
}
81

82
// GetRemoteCommitHash fetches the remote ref and returns its commit hash without checking it out.
83
// This function is used to check if the remote has changed since the last cache.
84
// If network fails, returns an error that can be handled gracefully by the caller.
85
func GetRemoteCommitHash(repo, ref string) (string, error) {
5✔
86
        log.Debugf("Checking remote commit hash for %s@%s", repo, ref)
5✔
87
        // Use ls-remote to get the commit hash without cloning/fetching
5✔
88
        out, err := exec.Command("git", "ls-remote", repo, ref).CombinedOutput()
5✔
89
        if err != nil {
8✔
90
                return "", fmt.Errorf("git ls-remote failed (network or repository error): %v: %s", err, string(out))
3✔
91
        }
3✔
92

93
        output := strings.TrimSpace(string(out))
2✔
94
        if output == "" {
2✔
UNCOV
95
                return "", fmt.Errorf("ref %q not found in repository %q", ref, repo)
×
UNCOV
96
        }
×
97

98
        // Parse output: "commit_hash\trefs/heads/branch" or "commit_hash\tHEAD"
99
        lines := strings.Split(output, "\n")
2✔
100
        for _, line := range lines {
4✔
101
                parts := strings.Split(line, "\t")
2✔
102
                if len(parts) >= 2 {
4✔
103
                        hash := strings.TrimSpace(parts[0])
2✔
104
                        if len(hash) == 40 && isValidCommitHash(hash) {
4✔
105
                                log.Debugf("Remote commit hash for %s@%s: %s", repo, ref, hash[:8])
2✔
106
                                return hash, nil
2✔
107
                        }
2✔
108
                }
109
        }
110

111
        return "", fmt.Errorf("could not parse commit hash from ls-remote output: %s", output)
×
112
}
113

114
// IsCommitHash checks if the given ref is already a commit hash (40-character hex string).
115
// This is used to validate configuration - if ref is already a commit hash,
116
// commit hash tracking doesn't make sense since commit hashes don't change.
117
func IsCommitHash(ref string) bool {
17✔
118
        return len(ref) == 40 && isValidCommitHash(ref)
17✔
119
}
17✔
120

121
// isValidCommitHash checks if a string is a valid 40-character hexadecimal hash
122
func isValidCommitHash(hash string) bool {
15✔
123
        if len(hash) != 40 {
16✔
124
                return false
1✔
125
        }
1✔
126
        matched, _ := regexp.MatchString("^[a-fA-F0-9]{40}$", hash)
14✔
127
        return matched
14✔
128
}
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

© 2025 Coveralls, Inc