From 3d32c608aab488e51213fb46aba2cc5c84cf077d Mon Sep 17 00:00:00 2001 From: Ashok Siyani Date: Fri, 28 Mar 2025 10:08:20 +0000 Subject: [PATCH 1/3] add option to set root for worktree links --- pkg/mirror/config.go | 34 +++++++++++++++- pkg/mirror/config_test.go | 73 +++++++++++++++++++++++++++++------ pkg/mirror/repository.go | 12 +++++- pkg/mirror/repository_test.go | 8 ++-- 4 files changed, 110 insertions(+), 17 deletions(-) diff --git a/pkg/mirror/config.go b/pkg/mirror/config.go index 3fd27ed..ee74587 100644 --- a/pkg/mirror/config.go +++ b/pkg/mirror/config.go @@ -21,6 +21,13 @@ type DefaultConfig struct { // specified in repo config Root string `yaml:"root"` + // LinkRoot is the absolute path to the dir which is the root for the worktree links + // if link is a relative path it will be relative to this dir + // if link is not specified it will be constructed from repo name and worktree ref + // and it will be placed in this dir + // if not specified it will be same as root + LinkRoot string `yaml:"link_root"` + // Interval is time duration for how long to wait between mirrors Interval time.Duration `yaml:"interval"` @@ -46,6 +53,13 @@ type RepositoryConfig struct { // absolute path is not provided Root string `yaml:"root"` + // LinkRoot is the absolute path to the dir which is the root for the worktree links + // if link is a relative path it will be relative to this dir + // if link is not specified it will be constructed from repo name and worktree ref + // and it will be placed in this dir + // if not specified it will be same as root + LinkRoot string `yaml:"link_root"` + // Interval is time duration for how long to wait between mirrors Interval time.Duration `yaml:"interval"` @@ -67,7 +81,9 @@ type RepositoryConfig struct { // Worktree represents maintained worktree on given link. type WorktreeConfig struct { // Link is the path at which to create a symlink to the worktree dir - // if path is not absolute it will be created under repository root + // if path is not absolute it will be created under repository link_root + // if link is not specified it will be constructed from repo name and worktree ref + // and it will be placed in link_root dir Link string `yaml:"link"` // Ref represents the git reference of the worktree branch, tags or hash @@ -99,6 +115,12 @@ func (rpc *RepoPoolConfig) ValidateDefaults() error { } } + if dc.LinkRoot != "" { + if !filepath.IsAbs(dc.LinkRoot) { + errs = append(errs, fmt.Errorf("repository link_root '%s' must be absolute", dc.Root)) + } + } + if dc.Interval != 0 { if dc.Interval < minAllowedInterval { errs = append(errs, fmt.Errorf("provided interval between mirroring is too sort (%s), must be > %s", dc.Interval, minAllowedInterval)) @@ -132,12 +154,20 @@ func DefaultRepoDir(root string) string { // ApplyDefaults will add given default config to repository config if where needed func (rpc *RepoPoolConfig) ApplyDefaults() { + if rpc.Defaults.LinkRoot == "" { + rpc.Defaults.LinkRoot = rpc.Defaults.Root + } + for i := range rpc.Repositories { repo := &rpc.Repositories[i] if repo.Root == "" { repo.Root = rpc.Defaults.Root } + if repo.LinkRoot == "" { + repo.LinkRoot = rpc.Defaults.LinkRoot + } + if repo.Interval == 0 { repo.Interval = rpc.Defaults.Interval } @@ -170,7 +200,7 @@ func (rpc *RepoPoolConfig) ValidateLinkPaths() error { // add defaults before checking abs link paths for _, repo := range rpc.Repositories { for _, l := range repo.Worktrees { - absL := absLink(repo.Root, l.Link) + absL := absLink(repo.LinkRoot, l.Link) if ok := absLinks[absL]; ok { errs = append(errs, fmt.Errorf("links with overlapping abs path found name:%s path:%s", l.Link, absL)) diff --git a/pkg/mirror/config_test.go b/pkg/mirror/config_test.go index 5fb14d3..6ce7c6a 100644 --- a/pkg/mirror/config_test.go +++ b/pkg/mirror/config_test.go @@ -17,12 +17,14 @@ func TestRepoPoolConfig_ValidateDefaults(t *testing.T) { wantErr bool }{ {"empty", args{dc: DefaultConfig{}}, false}, - {"valid", args{dc: DefaultConfig{"/root", time.Second, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}}}, false}, - {"invalid_root", args{dc: DefaultConfig{"root", time.Second, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}}}, true}, - {"invalid_interval", args{dc: DefaultConfig{"/root", time.Millisecond, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}}}, true}, - {"invalid_timeout", args{dc: DefaultConfig{"/root", time.Second, time.Millisecond, "always", Auth{"/path/to/key", "/host"}}}, true}, - {"valid_gc", args{dc: DefaultConfig{"/root", time.Second, 2 * time.Second, "", Auth{"/path/to/key", "/host"}}}, false}, - {"invalid_gc", args{dc: DefaultConfig{"/root", time.Second, 2 * time.Second, "blah", Auth{"/path/to/key", "/host"}}}, true}, + {"valid", args{dc: DefaultConfig{"/root", "", time.Second, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}}}, false}, + {"valid_with_link_root", args{dc: DefaultConfig{"/root", "/link_root", time.Second, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}}}, false}, + {"invalid_root", args{dc: DefaultConfig{"root", "", time.Second, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}}}, true}, + {"invalid_link_root", args{dc: DefaultConfig{"/root", "link_root", time.Second, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}}}, true}, + {"invalid_interval", args{dc: DefaultConfig{"/root", "/link_root", time.Millisecond, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}}}, true}, + {"invalid_timeout", args{dc: DefaultConfig{"/root", "/link_root", time.Second, time.Millisecond, "always", Auth{"/path/to/key", "/host"}}}, true}, + {"valid_gc", args{dc: DefaultConfig{"/root", "/link_root", time.Second, 2 * time.Second, "", Auth{"/path/to/key", "/host"}}}, false}, + {"invalid_gc", args{dc: DefaultConfig{"/root", "/link_root", time.Second, 2 * time.Second, "blah", Auth{"/path/to/key", "/host"}}}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -48,7 +50,7 @@ func TestRepoPoolConfig_ApplyDefaults(t *testing.T) { {"all_def", RepoPoolConfig{ Defaults: DefaultConfig{ - "/root", time.Second, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}, + "/root", "/link_root", time.Second, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}, }, Repositories: []RepositoryConfig{ {Remote: "user@host.xz:path/to/repo1.git"}, @@ -56,6 +58,7 @@ func TestRepoPoolConfig_ApplyDefaults(t *testing.T) { { Remote: "user@host.xz:path/to/repo3.git", Root: "/another-root", + LinkRoot: "/another-link-root", Interval: 2 * time.Second, MirrorTimeout: 4 * time.Second, GitGC: "off", @@ -65,12 +68,13 @@ func TestRepoPoolConfig_ApplyDefaults(t *testing.T) { }, RepoPoolConfig{ Defaults: DefaultConfig{ - "/root", time.Second, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}, + "/root", "/link_root", time.Second, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}, }, Repositories: []RepositoryConfig{ { Remote: "user@host.xz:path/to/repo1.git", Root: "/root", + LinkRoot: "/link_root", Interval: time.Second, MirrorTimeout: 2 * time.Second, GitGC: "always", @@ -79,6 +83,7 @@ func TestRepoPoolConfig_ApplyDefaults(t *testing.T) { { Remote: "user@host.xz:path/to/repo2.git", Root: "/root", + LinkRoot: "/link_root", Interval: time.Second, MirrorTimeout: 2 * time.Second, GitGC: "always", @@ -87,6 +92,7 @@ func TestRepoPoolConfig_ApplyDefaults(t *testing.T) { { Remote: "user@host.xz:path/to/repo3.git", Root: "/another-root", + LinkRoot: "/another-link-root", Interval: 2 * time.Second, MirrorTimeout: 4 * time.Second, GitGC: "off", @@ -94,6 +100,31 @@ func TestRepoPoolConfig_ApplyDefaults(t *testing.T) { }, }}, }, + {"no_link_root_def", + RepoPoolConfig{ + Defaults: DefaultConfig{ + "/root", "", time.Second, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}, + }, + Repositories: []RepositoryConfig{ + {Remote: "user@host.xz:path/to/repo1.git"}, + }, + }, + RepoPoolConfig{ + Defaults: DefaultConfig{ + "/root", "/root", time.Second, 2 * time.Second, "always", Auth{"/path/to/key", "/host"}, + }, + Repositories: []RepositoryConfig{ + { + Remote: "user@host.xz:path/to/repo1.git", + Root: "/root", + LinkRoot: "/root", + Interval: time.Second, + MirrorTimeout: 2 * time.Second, + GitGC: "always", + Auth: Auth{SSHKeyPath: "/path/to/key", SSHKnownHostsPath: "/host"}, + }, + }}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -126,6 +157,26 @@ func TestRepoPoolConfig_ValidateLinkPaths(t *testing.T) { }, { Root: "/another-root", + LinkRoot: "/another-link-root", + Worktrees: []WorktreeConfig{{Link: "link1"}}, + }, + }, + }, + false, + }, { + "valid_with_link_root", + RepoPoolConfig{ + Defaults: DefaultConfig{LinkRoot: "/root"}, + Repositories: []RepositoryConfig{ + { + Worktrees: []WorktreeConfig{{Link: "link1"}, {Link: "link2"}}, + }, + { + Worktrees: []WorktreeConfig{{Link: "/diff-abs/link1"}, {Link: "link3"}}, + }, + { + Root: "/another-root", + LinkRoot: "/another-link-root", Worktrees: []WorktreeConfig{{Link: "link1"}}, }, }, @@ -134,7 +185,7 @@ func TestRepoPoolConfig_ValidateLinkPaths(t *testing.T) { }, { "same-link-name-diff-repo", RepoPoolConfig{ - Defaults: DefaultConfig{Root: "/root"}, + Defaults: DefaultConfig{LinkRoot: "/root"}, Repositories: []RepositoryConfig{ { Worktrees: []WorktreeConfig{{Link: "link1"}, {Link: "link2"}}, @@ -163,7 +214,7 @@ func TestRepoPoolConfig_ValidateLinkPaths(t *testing.T) { }, { "same-link-name-same-repo", RepoPoolConfig{ - Defaults: DefaultConfig{Root: "/root"}, + Defaults: DefaultConfig{LinkRoot: "/root"}, Repositories: []RepositoryConfig{ { Worktrees: []WorktreeConfig{{Link: "link1"}, {Link: "link1"}}, @@ -174,7 +225,7 @@ func TestRepoPoolConfig_ValidateLinkPaths(t *testing.T) { }, { "same-link-with-abs", RepoPoolConfig{ - Defaults: DefaultConfig{Root: "/root"}, + Defaults: DefaultConfig{LinkRoot: "/root"}, Repositories: []RepositoryConfig{ { Worktrees: []WorktreeConfig{{Link: "link1"}}, diff --git a/pkg/mirror/repository.go b/pkg/mirror/repository.go index faf98f9..dfd8608 100644 --- a/pkg/mirror/repository.go +++ b/pkg/mirror/repository.go @@ -54,6 +54,7 @@ type Repository struct { gitURL *giturl.URL // parsed remote git URL remote string // remote repo to mirror root string // absolute path to the root where repo directory created + linkRoot string // absolute path to the root where repo worktree links are published dir string // absolute path to the repo directory interval time.Duration // how long to wait between mirrors mirrorTimeout time.Duration // the total time allowed for the mirror loop @@ -86,6 +87,14 @@ func NewRepository(repoConf RepositoryConfig, envs []string, log *slog.Logger) ( return nil, fmt.Errorf("repository root '%s' must be absolute", repoConf.Root) } + if repoConf.LinkRoot != "" && !filepath.IsAbs(repoConf.LinkRoot) { + return nil, fmt.Errorf("repository link root set but path is not absolute '%s'", repoConf.Root) + } + + if repoConf.LinkRoot == "" { + repoConf.LinkRoot = repoConf.Root + } + if repoConf.Interval < minAllowedInterval { return nil, fmt.Errorf("provided interval between mirroring is too sort (%s), must be > %s", repoConf.Interval, minAllowedInterval) } @@ -111,6 +120,7 @@ func NewRepository(repoConf RepositoryConfig, envs []string, log *slog.Logger) ( gitURL: gURL, remote: remoteURL, root: repoConf.Root, + linkRoot: repoConf.LinkRoot, dir: repoDir, interval: repoConf.Interval, mirrorTimeout: repoConf.MirrorTimeout, @@ -144,7 +154,7 @@ func (r *Repository) AddWorktreeLink(wtc WorktreeConfig) error { return fmt.Errorf("worktree with given link already exits link:%s ref:%s", v.linkAbs, v.ref) } - linkAbs := absLink(r.root, wtc.Link) + linkAbs := absLink(r.linkRoot, wtc.Link) if wtc.Ref == "" { wtc.Ref = "HEAD" diff --git a/pkg/mirror/repository_test.go b/pkg/mirror/repository_test.go index 6921259..8b771cc 100644 --- a/pkg/mirror/repository_test.go +++ b/pkg/mirror/repository_test.go @@ -37,6 +37,7 @@ func TestNewRepo(t *testing.T) { gitURL: &giturl.URL{Scheme: "scp", User: "user", Host: "host.xz", Path: "path/to", Repo: "repo.git"}, remote: "user@host.xz:path/to/repo.git", root: "/tmp", + linkRoot: "/tmp", dir: "/tmp/repo-mirrors/repo.git", gitGC: "always", interval: 10 * time.Second, @@ -122,6 +123,7 @@ func TestRepo_AddWorktreeLink(t *testing.T) { r := &Repository{ gitURL: &giturl.URL{Scheme: "scp", User: "user", Host: "host.xz", Path: "path/to", Repo: "repo.git"}, root: "/tmp/root", + linkRoot: "/tmp/link-root", interval: 10 * time.Second, auth: nil, log: slog.Default(), @@ -155,9 +157,9 @@ func TestRepo_AddWorktreeLink(t *testing.T) { } // compare all worktree links want := map[string]*WorkTreeLink{ - "link": {link: "link", linkAbs: "/tmp/root/link", ref: "master", pathspecs: []string{}}, - "link2": {link: "link2", linkAbs: "/tmp/root/link2", ref: "other-branch", pathspecs: []string{"*.c", "path1", "path2/**/*.yaml"}}, - "link3": {link: "link3", linkAbs: "/tmp/root/link3", ref: "HEAD", pathspecs: []string{}}, + "link": {link: "link", linkAbs: "/tmp/link-root/link", ref: "master", pathspecs: []string{}}, + "link2": {link: "link2", linkAbs: "/tmp/link-root/link2", ref: "other-branch", pathspecs: []string{"*.c", "path1", "path2/**/*.yaml"}}, + "link3": {link: "link3", linkAbs: "/tmp/link-root/link3", ref: "HEAD", pathspecs: []string{}}, "/tmp/link": {link: "/tmp/link", linkAbs: "/tmp/link", ref: "tag", pathspecs: []string{}}, } if diff := cmp.Diff(want, r.workTreeLinks, cmpopts.IgnoreFields(WorkTreeLink{}, "log"), cmp.AllowUnexported(WorkTreeLink{})); diff != "" { From 62a719f1734e22987ceca690e0a8cb3e2e039097 Mon Sep 17 00:00:00 2001 From: Ashok Siyani Date: Fri, 28 Mar 2025 14:50:40 +0000 Subject: [PATCH 2/3] support empty worktree with links --- config_test.go | 13 ++- pkg/mirror/config.go | 81 +++++++++++++--- pkg/mirror/config_test.go | 195 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 270 insertions(+), 19 deletions(-) diff --git a/config_test.go b/config_test.go index 5e31baa..a8b77bf 100644 --- a/config_test.go +++ b/config_test.go @@ -110,9 +110,11 @@ func Test_diffWorktrees(t *testing.T) { Worktrees: []mirror.WorktreeConfig{ {Link: "link", Ref: "master", Pathspecs: nil}, {Link: "link2", Ref: "other-branch", Pathspecs: []string{"path1", "path2/**/*.yaml", "*.c"}}, + {Link: "", Ref: "master", Pathspecs: nil}, }, }, wantNewWTCs: []mirror.WorktreeConfig{ + {Link: "", Ref: "master", Pathspecs: nil}, {Link: "link", Ref: "master"}, {Link: "link2", Ref: "other-branch", Pathspecs: []string{"path1", "path2/**/*.yaml", "*.c"}}, }, @@ -127,6 +129,7 @@ func Test_diffWorktrees(t *testing.T) { {Link: "link", Ref: "master", Pathspecs: nil}, {Link: "link2", Ref: "other-branch", Pathspecs: []string{"path1", "path2/**/*.yaml", "*.c"}}, {Link: "link3", Ref: "other-branch", Pathspecs: []string{"path"}}, + {Link: "", Ref: "master", Pathspecs: nil}, }, }, newRepoConf: &mirror.RepositoryConfig{ @@ -136,14 +139,16 @@ func Test_diffWorktrees(t *testing.T) { {Link: "link", Ref: "master", Pathspecs: []string{"new-path"}}, {Link: "link2", Ref: "new-branch", Pathspecs: []string{"path1", "path2/**/*.yaml", "*.c"}}, {Link: "link3", Ref: "other-branch", Pathspecs: []string{"path", "new-path"}}, + {Link: "", Ref: "new-branch", Pathspecs: nil}, }, }, wantNewWTCs: []mirror.WorktreeConfig{ + {Link: "", Ref: "new-branch", Pathspecs: nil}, {Link: "link", Ref: "master", Pathspecs: []string{"new-path"}}, {Link: "link2", Ref: "new-branch", Pathspecs: []string{"path1", "path2/**/*.yaml", "*.c"}}, {Link: "link3", Ref: "other-branch", Pathspecs: []string{"path", "new-path"}}, }, - wantRemovedWTs: []string{"link", "link2", "link3"}, + wantRemovedWTs: []string{"link", "link2", "link3", "repo1/master"}, }, { name: "rearrange-path", @@ -182,9 +187,11 @@ func Test_diffWorktrees(t *testing.T) { Worktrees: []mirror.WorktreeConfig{ {Link: "link", Ref: "master", Pathspecs: nil}, {Link: "link3", Ref: "other-branch", Pathspecs: []string{"path1", "path2/**/*.yaml", "*.c"}}, + {Link: "", Ref: "master", Pathspecs: nil}, }, }, wantNewWTCs: []mirror.WorktreeConfig{ + {Ref: "master"}, {Link: "link3", Ref: "other-branch", Pathspecs: []string{"path1", "path2/**/*.yaml", "*.c"}}, }, wantRemovedWTs: []string{"link2"}, @@ -193,6 +200,10 @@ func Test_diffWorktrees(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + if err := tt.initialRepoConf.PopulateEmptyLinkPaths(); err != nil { + t.Fatalf("failed to create repo error = %v", err) + } + repo, err := mirror.NewRepository(*tt.initialRepoConf, nil, slog.Default()) if err != nil { t.Fatalf("failed to create repo error = %v", err) diff --git a/pkg/mirror/config.go b/pkg/mirror/config.go index ee74587..b8658eb 100644 --- a/pkg/mirror/config.go +++ b/pkg/mirror/config.go @@ -3,9 +3,16 @@ package mirror import ( "fmt" "path/filepath" + "regexp" + "strings" "time" + + "github.com/utilitywarehouse/git-mirror/pkg/giturl" ) +var matchSpecialCharReg = regexp.MustCompile(`[\\:\/*?"<>|\s]`) +var matchDupUnderscoreReg = regexp.MustCompile(`_+`) + // RepoPoolConfig is the configuration to create repoPool type RepoPoolConfig struct { // default config for all the repositories if not set @@ -103,8 +110,8 @@ type Auth struct { SSHKnownHostsPath string `yaml:"ssh_known_hosts_path"` } -// ValidateDefaults will verify default config -func (rpc *RepoPoolConfig) ValidateDefaults() error { +// validateDefaults will verify default config +func (rpc *RepoPoolConfig) validateDefaults() error { dc := rpc.Defaults var errs []error @@ -152,8 +159,8 @@ func DefaultRepoDir(root string) string { return filepath.Join(root, "repo-mirrors") } -// ApplyDefaults will add given default config to repository config if where needed -func (rpc *RepoPoolConfig) ApplyDefaults() { +// applyDefaults will add given default config to repository config if where needed +func (rpc *RepoPoolConfig) applyDefaults() { if rpc.Defaults.LinkRoot == "" { rpc.Defaults.LinkRoot = rpc.Defaults.Root } @@ -186,17 +193,62 @@ func (rpc *RepoPoolConfig) ApplyDefaults() { } } +func normaliseReference(ref string) string { + ref = strings.TrimSpace(ref) + // remove special char not allowed in file name + ref = matchSpecialCharReg.ReplaceAllString(ref, "_") + ref = matchDupUnderscoreReg.ReplaceAllString(ref, "_") + return ref +} + +func generateLink(remote, ref string) (string, error) { + gitURL, err := giturl.Parse(remote) + if err != nil { + return "", err + } + normalisedRef := normaliseReference(ref) + + // reject ref with all special char and . and .. has special meaning + if normalisedRef == "_" || + normalisedRef == "." || normalisedRef == ".." { + return "", fmt.Errorf("reference cant be normalised") + } + + // if reference is an hash then shorter version can be used as link path + if IsFullCommitHash(normalisedRef) { + normalisedRef = normalisedRef[:7] + } + + return filepath.Join(strings.TrimRight(gitURL.Repo, ".git"), normalisedRef), nil +} + +// PopulateEmptyLinkPaths will try and generate missing link paths +func (repo *RepositoryConfig) PopulateEmptyLinkPaths() error { + for i := range repo.Worktrees { + if repo.Worktrees[i].Link != "" { + continue + } + if repo.Worktrees[i].Ref == "" { + repo.Worktrees[i].Ref = "HEAD" + } + link, err := generateLink(repo.Remote, repo.Worktrees[i].Ref) + if err != nil { + return err + } + repo.Worktrees[i].Link = link + } + return nil +} + // It is possible that same root is used for multiple repositories // since Links are placed at the root, we need to make sure that all link's // name (path) are diff. -// ValidateLinkPaths makes sures all link's absolute paths are different. -func (rpc *RepoPoolConfig) ValidateLinkPaths() error { +// validateLinkPaths makes sures all link's absolute paths are different. +func (rpc *RepoPoolConfig) validateLinkPaths() error { var errs []error absLinks := make(map[string]bool) - rpc.ApplyDefaults() - // add defaults before checking abs link paths for _, repo := range rpc.Repositories { for _, l := range repo.Worktrees { @@ -220,15 +272,22 @@ func (rpc *RepoPoolConfig) ValidateLinkPaths() error { // ValidateAndApplyDefaults will validate link paths and default and apply defaults func (conf *RepoPoolConfig) ValidateAndApplyDefaults() error { - if err := conf.ValidateDefaults(); err != nil { + if err := conf.validateDefaults(); err != nil { return err } - if err := conf.ValidateLinkPaths(); err != nil { + conf.applyDefaults() + + for _, repo := range conf.Repositories { + if err := repo.PopulateEmptyLinkPaths(); err != nil { + return err + } + } + + if err := conf.validateLinkPaths(); err != nil { return err } - conf.ApplyDefaults() return nil } diff --git a/pkg/mirror/config_test.go b/pkg/mirror/config_test.go index 6ce7c6a..dda4488 100644 --- a/pkg/mirror/config_test.go +++ b/pkg/mirror/config_test.go @@ -7,7 +7,7 @@ import ( "github.com/google/go-cmp/cmp" ) -func TestRepoPoolConfig_ValidateDefaults(t *testing.T) { +func TestRepoPoolConfig_validateDefaults(t *testing.T) { type args struct { dc DefaultConfig } @@ -29,14 +29,14 @@ func TestRepoPoolConfig_ValidateDefaults(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := RepoPoolConfig{Defaults: tt.args.dc} - if err := config.ValidateDefaults(); (err != nil) != tt.wantErr { + if err := config.validateDefaults(); (err != nil) != tt.wantErr { t.Errorf("ValidateDefaults() error = %v, wantErr %v", err, tt.wantErr) } }) } } -func TestRepoPoolConfig_ApplyDefaults(t *testing.T) { +func TestRepoPoolConfig_applyDefaults(t *testing.T) { tests := []struct { name string config RepoPoolConfig @@ -129,7 +129,7 @@ func TestRepoPoolConfig_ApplyDefaults(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.config.ApplyDefaults() + tt.config.applyDefaults() if diff := cmp.Diff(tt.config, tt.want); diff != "" { t.Errorf("ApplyDefaults() mismatch (-want +got):\n%s", diff) @@ -138,7 +138,7 @@ func TestRepoPoolConfig_ApplyDefaults(t *testing.T) { } } -func TestRepoPoolConfig_ValidateLinkPaths(t *testing.T) { +func TestRepoPoolConfig_validateLinkPaths(t *testing.T) { tests := []struct { name string config RepoPoolConfig @@ -237,7 +237,7 @@ func TestRepoPoolConfig_ValidateLinkPaths(t *testing.T) { }, true, }, { - "same-link-with-abs", + "same-link-with-abs-2", RepoPoolConfig{ Repositories: []RepositoryConfig{ { @@ -253,13 +253,143 @@ func TestRepoPoolConfig_ValidateLinkPaths(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.config.ValidateLinkPaths(); (err != nil) != tt.wantErr { + + tt.config.applyDefaults() + + if err := tt.config.validateLinkPaths(); (err != nil) != tt.wantErr { t.Errorf("validateLinkPaths() error = %v, wantErr %v", err, tt.wantErr) } }) } } +func TestRepoPoolConfig_PopulateLinkPaths(t *testing.T) { + tests := []struct { + name string + config RepoPoolConfig + wantConfig RepoPoolConfig + wantValid bool + }{ + { + "valid", + RepoPoolConfig{ + Repositories: []RepositoryConfig{ + { + Remote: "https://github.com/org/repo1.git", + Worktrees: []WorktreeConfig{{Link: "link1", Ref: "main"}, {Link: "", Ref: "main"}, {Link: "", Ref: ""}}, + }, + { + Remote: "https://github.com/org/repo2.git", + Worktrees: []WorktreeConfig{{Link: "/diff-abs/link1"}, {Link: "link3"}}, + }, + { + Remote: "https://github.com/org/repo3.git", + Root: "/another-root", + LinkRoot: "/another-link-root", + Worktrees: []WorktreeConfig{{Link: "link1"}}, + }, + }, + }, + RepoPoolConfig{ + Repositories: []RepositoryConfig{ + { + Remote: "https://github.com/org/repo1.git", + Worktrees: []WorktreeConfig{{Link: "link1", Ref: "main"}, {Link: "repo1/main", Ref: "main"}, {Link: "repo1/HEAD", Ref: "HEAD"}}, + }, + { + Remote: "https://github.com/org/repo2.git", + Worktrees: []WorktreeConfig{{Link: "/diff-abs/link1"}, {Link: "link3"}}, + }, + { + Remote: "https://github.com/org/repo3.git", + Root: "/another-root", + LinkRoot: "/another-link-root", + Worktrees: []WorktreeConfig{{Link: "link1"}}, + }, + }, + }, + true, + }, { + "multiple-repo-empty-link", + RepoPoolConfig{ + Repositories: []RepositoryConfig{ + { + Remote: "https://github.com/org/repo1.git", + Worktrees: []WorktreeConfig{{Link: "link1", Ref: "main"}, {Link: "", Ref: "main"}, {Link: "", Ref: ""}}, + }, + { + Remote: "https://github.com/org/repo2.git", + Worktrees: []WorktreeConfig{{Link: "diff/link1", Ref: "main"}, {Link: "", Ref: "main"}, {Link: "", Ref: ""}}, + }, + { + Remote: "https://github.com/org/repo3.git", + Root: "/another-root", + LinkRoot: "/another-link-root", + Worktrees: []WorktreeConfig{{Link: "link1"}}, + }, + }, + }, + RepoPoolConfig{ + Repositories: []RepositoryConfig{ + { + Remote: "https://github.com/org/repo1.git", + Worktrees: []WorktreeConfig{{Link: "link1", Ref: "main"}, {Link: "repo1/main", Ref: "main"}, {Link: "repo1/HEAD", Ref: "HEAD"}}, + }, + { + Remote: "https://github.com/org/repo2.git", + Worktrees: []WorktreeConfig{{Link: "diff/link1", Ref: "main"}, {Link: "repo2/main", Ref: "main"}, {Link: "repo2/HEAD", Ref: "HEAD"}}, + }, + { + Remote: "https://github.com/org/repo3.git", + Root: "/another-root", + LinkRoot: "/another-link-root", + Worktrees: []WorktreeConfig{{Link: "link1"}}, + }, + }, + }, + true, + }, { + "one-repo-2-empty-link-same-ref", + RepoPoolConfig{ + Repositories: []RepositoryConfig{ + { + Remote: "https://github.com/org/repo1.git", + Worktrees: []WorktreeConfig{{Link: "", Ref: "main"}, {Link: "", Ref: "main"}, {Link: "", Ref: ""}}, + }, + }, + }, + RepoPoolConfig{ + Repositories: []RepositoryConfig{ + { + Remote: "https://github.com/org/repo1.git", + Worktrees: []WorktreeConfig{{Link: "repo1/main", Ref: "main"}, {Link: "repo1/main", Ref: "main"}, {Link: "repo1/HEAD", Ref: "HEAD"}}, + }, + }, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.config.applyDefaults() + + for _, repo := range tt.config.Repositories { + if err := repo.PopulateEmptyLinkPaths(); err != nil { + t.Errorf("populateEmptyLinkPaths() error = %v", err) + } + } + + if diff := cmp.Diff(tt.config, tt.wantConfig); diff != "" { + t.Errorf("PopulateEmptyLinkPaths() config mismatch (-want +got):\n%s", diff) + } + + if err := tt.config.validateLinkPaths(); (err == nil) != tt.wantValid { + t.Errorf("validateLinkPaths() error = %v, wantValid %v", err, tt.wantValid) + } + }) + } +} + func TestAuth_gitSSHCommand(t *testing.T) { type fields struct { SSHKeyPath string @@ -292,3 +422,54 @@ func TestAuth_gitSSHCommand(t *testing.T) { }) } } + +func Test_normaliseReference(t *testing.T) { + tests := []struct { + name string + ref string + want string + }{ + {"1", "// TODO: Add test cases.", "_TODO_Add_test_cases."}, + {"2", "name/ref", "name_ref"}, + {"3", `with lots of < > : " / \ | ? * char`, "with_lots_of_char"}, + {"4", `remotes/origin/MO-1001`, "remotes_origin_MO-1001"}, + {"5", `remotes/origin/revert-130445-uw-releaser-very-very-long-reference-service-64bbae965ce8d4a0eaf929f9455f40a72d3b3208`, + "remotes_origin_revert-130445-uw-releaser-very-very-long-reference-service-64bbae965ce8d4a0eaf929f9455f40a72d3b3208"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := normaliseReference(tt.ref); got != tt.want { + t.Errorf("normaliseReference() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_generateLink(t *testing.T) { + tests := []struct { + name string + remote string + ref string + want string + wantErr bool + }{ + {"1", "git@github.com:org/repo.git", "master", "repo/master", false}, + {"2", "ssh://git@github.com/org/repo.git", "21f541a953776c5d7c5c5c9d00cdfb26e6c9ecdb", "repo/21f541a", false}, + {"3", "https://github.com/org/repo.git", "remotes/origin/MO-1001", "repo/remotes_origin_MO-1001", false}, + {"4", "git@github.com:org/repo.git", "v2.16.1-3", "repo/v2.16.1-3", false}, + {"5", "ssh://git@github.com/org/repo.git", `< > : " / \ | ? *`, "", true}, + {"6", "https://github.com/org/repo.git", ".", "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := generateLink(tt.remote, tt.ref) + if (err != nil) != tt.wantErr { + t.Errorf("generateLink() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("generateLink() = %v, want %v", got, tt.want) + } + }) + } +} From c83a22289ac4a523b7c339a9d009611e724248cb Mon Sep 17 00:00:00 2001 From: Ashok Siyani Date: Fri, 28 Mar 2025 15:46:03 +0000 Subject: [PATCH 3/3] add link_root info --- readme.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 650d75c..1a14eb0 100644 --- a/readme.md +++ b/readme.md @@ -39,6 +39,13 @@ defaults: # specified in repo config (default: '/tmp/git-mirror') root: /tmp/git-mirror + # link_root is the absolute path to the dir which is the root for the worktree links + # if link is a relative path it will be relative to link_root dir + # if link is not specified it will be constructed from repo name and worktree ref + # and it will be placed in this dir + # if not specified it will be same as root + link_root: /app/links + # interval is time duration for how long to wait between mirrors. (default: '30s') interval: 30s @@ -63,6 +70,7 @@ repositories: # following fields are optional. # if these fields are not specified values from defaults section will be used root: /some/other/location + link_root: /some/path interval: 1m mirror_timeout: 5m git_gc: always @@ -70,9 +78,11 @@ repositories: ssh_key_path: /some/other/location ssh_known_hosts_path: /some/other/location worktrees: - # link is the path at which to create a symlink to the worktree dir - # if path is not absolute it will be created under repository root - - link: alerts # required + # link is the path at which to create a symlink to the worktree dir + # if path is not absolute it will be created under repository link_root + # if link is not specified it will be constructed from repo name and worktree ref + # and it will be placed in link_root dir + - link: alerts # ref represents the git reference of the worktree branch, tags or hash # are supported. default is HEAD