diff --git a/cleanup.go b/cleanup.go new file mode 100644 index 0000000..35340f5 --- /dev/null +++ b/cleanup.go @@ -0,0 +1,95 @@ +package main + +import ( + "os" + "os/exec" + "path/filepath" + "slices" + "strconv" + "strings" + + "github.com/utilitywarehouse/git-mirror/pkg/mirror" +) + +var gitExecutablePath = exec.Command("git").String() + +// cleanupOrphanedRepos deletes directory of the repos from the default root +// which are no longer referenced in config and it was removed while app was down. +// Any removal while app is running is already handled by ensureConfig() hence +// this function should be called once +// this is best effort clean up as orphaned published link will not be clean up +// as its not known where it was published. +func cleanupOrphanedRepos(config *mirror.RepoPoolConfig, repoPool *mirror.RepoPool) { + // if default root is not set repos might not be located in same dir + if config.Defaults.Root == "" { + return + } + + repoDirs := repoPool.RepositoriesDirPath() + + entries, err := os.ReadDir(config.Defaults.Root) + if err != nil { + logger.Error("unable to read root dir for clean up", "err", err) + return + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + fullPath := filepath.Join(config.Defaults.Root, entry.Name()) + + if slices.Contains(repoDirs, fullPath) { + continue + } + + // since git-mirror creates bare repository for mirror + // non-repo dir or non-bare repo dir must be skipped + ok, err := isBareRepo(fullPath) + if err != nil { + logger.Error("unable to check if bare repo", "path", fullPath, "err", err) + continue + } + + if !ok { + continue + } + + logger.Info("removing orphaned repo dir...", "path", fullPath) + if err := os.RemoveAll(fullPath); err != nil { + logger.Error("unable orphaned repo dir", "path", fullPath, "err", err) + continue + } + } +} + +func isInsideGitDir(cwd string) bool { + // err is expected here + output, _ := runGitCommand(cwd, "rev-parse", "--is-inside-git-dir") + return output == "true" +} + +func isBareRepo(cwd string) (bool, error) { + // bare repository doesn't have worktrees + if !isInsideGitDir(cwd) { + return false, nil + } + + output, err := runGitCommand(cwd, "rev-parse", "--is-bare-repository") + if err != nil { + return false, err + } + + return strconv.ParseBool(output) +} + +// runGitCommand runs git command with given arguments on given CWD +func runGitCommand(cwd string, args ...string) (string, error) { + cmd := exec.Command(gitExecutablePath, args...) + if cwd != "" { + cmd.Dir = cwd + } + output, err := cmd.CombinedOutput() + return strings.TrimSpace(string(output)), err +} diff --git a/main.go b/main.go index 43dcd65..2082b4a 100644 --- a/main.go +++ b/main.go @@ -120,8 +120,16 @@ func main() { os.Exit(1) } + firstRun := true onConfigChange := func(config *mirror.RepoPoolConfig) bool { - return ensureConfig(repoPool, config) + ok := ensureConfig(repoPool, config) + + if firstRun { + cleanupOrphanedRepos(config, repoPool) + firstRun = false + } + + return ok } // Start watching the config file diff --git a/pkg/mirror/repo_pool.go b/pkg/mirror/repo_pool.go index 0d3412d..bb4f47a 100644 --- a/pkg/mirror/repo_pool.go +++ b/pkg/mirror/repo_pool.go @@ -179,6 +179,17 @@ func (rp *RepoPool) RepositoriesRemote() []string { return urls } +func (rp *RepoPool) RepositoriesDirPath() []string { + rp.lock.RLock() + defer rp.lock.RUnlock() + + var paths []string + for _, repo := range rp.repos { + paths = append(paths, repo.dir) + } + return paths +} + // AddWorktreeLink is wrapper around repositories AddWorktreeLink method func (rp *RepoPool) AddWorktreeLink(remote string, wt WorktreeConfig) error { rp.lock.RLock()