Skip to content

Commit dd18222

Browse files
authored
support add/remove respository and worktrees concurrently and config watcher (#32)
* refector WorkTreeLink and use appropriate field names * stop mirror loop using context cancel * use standard function name * add graceful shutdown and worktree cleanup * support adding respository and worktrees concurrently * support removing respository and worktrees concurrently * add git-mirror config watcher * updated go and dependencies * fix test * update test version * fix race detection test * produce consistent test results
1 parent 4d0e7cb commit dd18222

File tree

17 files changed

+704
-155
lines changed

17 files changed

+704
-155
lines changed

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
pkg-test:
1212
strategy:
1313
matrix:
14-
go-version: [1.21.x, 1.22.x, 1.23.x]
14+
go-version: [1.23.x, 1.24.x]
1515
os: [ubuntu-latest]
1616
runs-on: ${{ matrix.os }}
1717
steps:

config.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"os"
6+
"path"
7+
"time"
8+
9+
"github.com/utilitywarehouse/git-mirror/pkg/giturl"
10+
"github.com/utilitywarehouse/git-mirror/pkg/mirror"
11+
"gopkg.in/yaml.v3"
12+
)
13+
14+
const (
15+
defaultGitGC = "always"
16+
defaultInterval = 30 * time.Second
17+
defaultMirrorTimeout = 2 * time.Minute
18+
defaultSSHKeyPath = "/etc/git-secret/ssh"
19+
defaultSSHKnownHostsPath = "/etc/git-secret/known_hosts"
20+
)
21+
22+
var defaultRoot = path.Join(os.TempDir(), "git-mirror", "src")
23+
24+
// WatchConfig polls the config file every interval and reloads if modified
25+
func WatchConfig(path string, interval time.Duration, onChange func(*mirror.RepoPoolConfig)) {
26+
var lastModTime time.Time
27+
28+
// Load initial config
29+
config, err := parseConfigFile(path)
30+
if err != nil {
31+
logger.Error("failed to load config", "err", err)
32+
} else {
33+
onChange(config)
34+
}
35+
36+
ticker := time.NewTicker(interval)
37+
defer ticker.Stop()
38+
39+
for range ticker.C {
40+
fileInfo, err := os.Stat(path)
41+
if err != nil {
42+
logger.Error("Error checking config file", "err", err)
43+
continue
44+
}
45+
46+
modTime := fileInfo.ModTime()
47+
if modTime.After(lastModTime) {
48+
logger.Info("config file modified, reloading...")
49+
lastModTime = modTime
50+
51+
newConfig, err := parseConfigFile(path)
52+
if err != nil {
53+
logger.Error("failed to reload config", "err", err)
54+
} else {
55+
onChange(newConfig)
56+
}
57+
}
58+
}
59+
}
60+
61+
func ensureConfig(repoPool *mirror.RepoPool, newConfig *mirror.RepoPoolConfig) {
62+
63+
// add default values
64+
applyGitDefaults(newConfig)
65+
66+
// validate and apply defaults to new config before compare
67+
if err := newConfig.ValidateAndApplyDefaults(); err != nil {
68+
logger.Error("failed to validate new config", "err", err)
69+
return
70+
}
71+
72+
newRepos, removedRepos := diffRepositories(repoPool, newConfig)
73+
for _, repo := range removedRepos {
74+
if err := repoPool.RemoveRepository(repo); err != nil {
75+
logger.Error("failed to remove repository", "remote", repo, "err", err)
76+
}
77+
}
78+
for _, repo := range newRepos {
79+
if err := repoPool.AddRepository(repo); err != nil {
80+
logger.Error("failed to add new repository", "remote", repo.Remote, "err", err)
81+
}
82+
}
83+
84+
// find matched repos and check for worktree diffs
85+
for _, newRepoConf := range newConfig.Repositories {
86+
repo, err := repoPool.Repository(newRepoConf.Remote)
87+
if err != nil {
88+
continue
89+
}
90+
91+
newWTs, removedWTs := diffWorktrees(repo, &newRepoConf)
92+
93+
// 1st remove then add new in case new one has same link with diff reference
94+
for _, wt := range removedWTs {
95+
if err := repoPool.RemoveWorktreeLink(newRepoConf.Remote, wt); err != nil {
96+
logger.Error("failed to remove worktree", "remote", newRepoConf.Remote, "link", wt, "err", err)
97+
}
98+
}
99+
for _, wt := range newWTs {
100+
if err := repoPool.AddWorktreeLink(newRepoConf.Remote, wt); err != nil {
101+
logger.Error("failed to add worktree", "remote", newRepoConf.Remote, "link", wt.Link, "err", err)
102+
}
103+
}
104+
}
105+
}
106+
107+
func applyGitDefaults(mirrorConf *mirror.RepoPoolConfig) {
108+
if mirrorConf.Defaults.Root == "" {
109+
mirrorConf.Defaults.Root = defaultRoot
110+
}
111+
112+
if mirrorConf.Defaults.GitGC == "" {
113+
mirrorConf.Defaults.GitGC = defaultGitGC
114+
}
115+
116+
if mirrorConf.Defaults.Interval == 0 {
117+
mirrorConf.Defaults.Interval = defaultInterval
118+
}
119+
120+
if mirrorConf.Defaults.MirrorTimeout == 0 {
121+
mirrorConf.Defaults.MirrorTimeout = defaultMirrorTimeout
122+
}
123+
124+
if mirrorConf.Defaults.Auth.SSHKeyPath == "" {
125+
mirrorConf.Defaults.Auth.SSHKeyPath = defaultSSHKeyPath
126+
}
127+
128+
if mirrorConf.Defaults.Auth.SSHKnownHostsPath == "" {
129+
mirrorConf.Defaults.Auth.SSHKnownHostsPath = defaultSSHKnownHostsPath
130+
}
131+
}
132+
133+
func parseConfigFile(path string) (*mirror.RepoPoolConfig, error) {
134+
yamlFile, err := os.ReadFile(path)
135+
if err != nil {
136+
return nil, err
137+
}
138+
conf := &mirror.RepoPoolConfig{}
139+
err = yaml.Unmarshal(yamlFile, conf)
140+
if err != nil {
141+
return nil, err
142+
}
143+
return conf, nil
144+
}
145+
146+
func diffRepositories(repoPool *mirror.RepoPool, newConfig *mirror.RepoPoolConfig) (
147+
newRepos []mirror.RepositoryConfig,
148+
removedRepos []string,
149+
) {
150+
for _, newRepo := range newConfig.Repositories {
151+
if _, err := repoPool.Repository(newRepo.Remote); errors.Is(err, mirror.ErrNotExist) {
152+
newRepos = append(newRepos, newRepo)
153+
}
154+
}
155+
156+
for _, currentRepoURL := range repoPool.RepositoriesRemote() {
157+
var found bool
158+
for _, newRepo := range newConfig.Repositories {
159+
if currentRepoURL == giturl.NormaliseURL(newRepo.Remote) {
160+
found = true
161+
break
162+
}
163+
}
164+
if !found {
165+
removedRepos = append(removedRepos, currentRepoURL)
166+
}
167+
}
168+
169+
return
170+
}
171+
172+
func diffWorktrees(repo *mirror.Repository, newRepoConf *mirror.RepositoryConfig) (
173+
newWTCs []mirror.WorktreeConfig,
174+
removedWTs []string,
175+
) {
176+
currentWTLinks := repo.WorktreeLinks()
177+
178+
for _, newWTC := range newRepoConf.Worktrees {
179+
if _, ok := currentWTLinks[newWTC.Link]; !ok {
180+
newWTCs = append(newWTCs, newWTC)
181+
}
182+
}
183+
184+
// for existing worktree
185+
for cLink, wt := range currentWTLinks {
186+
var found bool
187+
for _, newWTC := range newRepoConf.Worktrees {
188+
if newWTC.Link == cLink {
189+
// wt link name is matching so make sure other
190+
// config match as well if not replace it
191+
if !wt.Equals(newWTC) {
192+
newWTCs = append(newWTCs, newWTC)
193+
break
194+
}
195+
found = true
196+
break
197+
}
198+
}
199+
if !found {
200+
removedWTs = append(removedWTs, cLink)
201+
}
202+
}
203+
204+
return
205+
}

0 commit comments

Comments
 (0)