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

CyberDuck79 / duckfile / 17529287440

07 Sep 2025 01:36PM UTC coverage: 76.052% (-0.1%) from 76.188%
17529287440

Pull #63

github

CyberDuck79
feat(template): add submodules support with recursive fetch and tests
Pull Request #63: feat(template): add submodules support with recursive fetch and tests

12 of 20 new or added lines in 3 files covered. (60.0%)

2 existing lines in 1 file now uncovered.

2477 of 3257 relevant lines covered (76.05%)

8.9 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) {
2✔
16
        workdir := filepath.Join(cacheDir, "repo") // 1-repo MVP, improve later
2✔
17

2✔
18
        // Already cloned?
2✔
19
        if _, err := exec.Command("test", "-d", filepath.Join(workdir, ".git")).CombinedOutput(); err == nil {
3✔
20
                log.Infof("Repository already exists at %s, updating...", workdir)
1✔
21
                // Fetch the desired ref and checkout FETCH_HEAD (detached)
1✔
22
                log.Debugf("Fetching ref %s from %s", ref, repo)
1✔
23
                if out, err := exec.Command("git", "-C", workdir, "fetch", "--depth", "1", "origin", ref).CombinedOutput(); err != nil {
1✔
24
                        return "", fmt.Errorf("git fetch failed: %v: %s", err, string(out))
×
25
                }
×
26
                log.Debugf("Checking out ref %s", ref)
1✔
27
                if out, err := exec.Command("git", "-C", workdir, "checkout", "--force", "--detach", "FETCH_HEAD").CombinedOutput(); err != nil {
1✔
28
                        return "", fmt.Errorf("git checkout failed: %v: %s", err, string(out))
×
29
                }
×
30
                if submodules {
1✔
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)
1✔
37
        } else {
1✔
38
                log.Infof("Cloning repository %s to %s", repo, workdir)
1✔
39
                // Fresh clone, then force checkout the ref (supports branch, tag, or commit)
1✔
40
                cloneArgs := []string{"clone", "--depth", "1"}
1✔
41
                if submodules {
2✔
42
                        cloneArgs = append(cloneArgs, "--recurse-submodules")
1✔
43
                }
1✔
44
                cloneArgs = append(cloneArgs, repo, workdir)
1✔
45
                if out, err := exec.Command("git", cloneArgs...).CombinedOutput(); err != nil {
1✔
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)
1✔
50
                if out, err := exec.Command("git", "-C", workdir, "fetch", "--depth", "1", "origin", ref).CombinedOutput(); err != nil {
1✔
51
                        return "", fmt.Errorf("git fetch failed: %v: %s", err, string(out))
×
52
                }
×
53
                log.Debugf("Checking out ref %s", ref)
1✔
54
                if out, err := exec.Command("git", "-C", workdir, "checkout", "--force", "--detach", "FETCH_HEAD").CombinedOutput(); err != nil {
1✔
55
                        return "", fmt.Errorf("git checkout failed: %v: %s", err, string(out))
×
56
                }
×
57
                if submodules {
2✔
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)
1✔
64
        }
65
        return workdir, nil
2✔
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) {
4✔
71
        out, err := exec.Command("git", "-C", workdir, "rev-parse", "HEAD").CombinedOutput()
4✔
72
        if err != nil {
6✔
73
                return "", fmt.Errorf("git rev-parse HEAD failed: %v: %s", err, string(out))
2✔
74
        }
2✔
75
        hash := strings.TrimSpace(string(out))
2✔
76
        if len(hash) != 40 {
2✔
77
                return "", fmt.Errorf("invalid commit hash length: got %d characters, expected 40", len(hash))
×
78
        }
×
79
        return hash, nil
2✔
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) {
4✔
86
        log.Debugf("Checking remote commit hash for %s@%s", repo, ref)
4✔
87
        // Use ls-remote to get the commit hash without cloning/fetching
4✔
88
        out, err := exec.Command("git", "ls-remote", repo, ref).CombinedOutput()
4✔
89
        if err != nil {
7✔
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))
1✔
94
        if output == "" {
1✔
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")
1✔
100
        for _, line := range lines {
2✔
101
                parts := strings.Split(line, "\t")
1✔
102
                if len(parts) >= 2 {
2✔
103
                        hash := strings.TrimSpace(parts[0])
1✔
104
                        if len(hash) == 40 && isValidCommitHash(hash) {
2✔
105
                                log.Debugf("Remote commit hash for %s@%s: %s", repo, ref, hash[:8])
1✔
106
                                return hash, nil
1✔
107
                        }
1✔
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 {
14✔
123
        if len(hash) != 40 {
15✔
124
                return false
1✔
125
        }
1✔
126
        matched, _ := regexp.MatchString("^[a-fA-F0-9]{40}$", hash)
13✔
127
        return matched
13✔
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

© 2026 Coveralls, Inc