| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- package git
- /*
- manager.go
- Core Manager type plus repository discovery / creation.
- Everything in this package is built on go-git (pure Go, already a direct
- dependency of the project — see mod/modules/installer.go). No `git` binary is
- ever invoked, so the feature cross-compiles to every target in the Makefile
- and needs no host package installed.
- */
- import (
- "errors"
- "os"
- "path/filepath"
- "strings"
- gogit "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/config"
- "github.com/go-git/go-git/v5/plumbing"
- "imuslab.com/arozos/mod/info/logger"
- )
- // Options configures a Manager.
- type Options struct {
- //Database is the ArozOS system database used to persist credentials.
- //A nil Database disables the credential store (operations still work with
- //credentials passed per call).
- Database CredentialDatabase
- //KeyStorePath is the folder holding the AES key that encrypts stored
- //credentials, e.g. "./system/git". Created on demand.
- KeyStorePath string
- //Logger is the system-wide logger. Optional.
- Logger *logger.Logger
- }
- // Manager is the entry point for all git operations. It is stateless with
- // respect to repositories: every call resolves the repository from the real
- // path it is given, so concurrent operations on different repos never contend.
- type Manager struct {
- options *Options
- credentials *CredentialStore
- }
- // NewManager builds a Manager. It only fails when the credential store cannot
- // be initialised; git operations themselves have no host prerequisites.
- func NewManager(options Options) (*Manager, error) {
- manager := &Manager{options: &options}
- if options.Database != nil {
- store, err := newCredentialStore(options.Database, options.KeyStorePath)
- if err != nil {
- return nil, err
- }
- manager.credentials = store
- }
- return manager, nil
- }
- // Credentials exposes the credential store. Returns nil when no database was
- // wired in.
- func (m *Manager) Credentials() *CredentialStore {
- return m.credentials
- }
- // IsRepo reports whether realpath is inside a git working tree. A bare .git
- // folder counts, a plain folder does not.
- func (m *Manager) IsRepo(realpath string) bool {
- _, err := m.open(realpath)
- return err == nil
- }
- // RepoRoot returns the working tree root containing realpath. Used so the UI can
- // accept any folder inside a repository and still operate on the whole repo.
- func (m *Manager) RepoRoot(realpath string) (string, error) {
- current, err := filepath.Abs(realpath)
- if err != nil {
- return "", err
- }
- for {
- if isDir(filepath.Join(current, ".git")) || isFile(filepath.Join(current, ".git")) {
- return filepath.ToSlash(current), nil
- }
- parent := filepath.Dir(current)
- if parent == current {
- return "", ErrNotARepo
- }
- current = parent
- }
- }
- // Init creates an empty repository at realpath, creating the folder when it
- // does not exist yet.
- func (m *Manager) Init(realpath string) error {
- if err := os.MkdirAll(realpath, 0775); err != nil {
- return err
- }
- if m.IsRepo(realpath) {
- return errors.New("a git repository already exists at this location")
- }
- _, err := gogit.PlainInit(realpath, false)
- return err
- }
- // Clone clones req.URL into req.Dest. The destination must not exist or must be
- // an empty folder — go-git refuses to clone into a populated directory, and we
- // check up front so the caller gets a readable message instead of a low level
- // one.
- func (m *Manager) Clone(req *CloneRequest) error {
- if strings.TrimSpace(req.URL) == "" {
- return errors.New("remote URL cannot be empty")
- }
- if strings.TrimSpace(req.Dest) == "" {
- return errors.New("clone destination cannot be empty")
- }
- empty, err := dirIsEmpty(req.Dest)
- if err != nil {
- return err
- }
- if !empty {
- return errors.New("clone destination is not empty")
- }
- //Remember whether the folder was already there: a failed clone should clean
- //up after itself without deleting a folder the user created.
- destExisted := isDir(req.Dest)
- if err := os.MkdirAll(req.Dest, 0775); err != nil {
- return err
- }
- cloneOptions := &gogit.CloneOptions{
- URL: req.URL,
- Depth: req.Depth,
- Auth: buildAuth(req.Username, req.Token),
- }
- if req.Branch != "" {
- cloneOptions.ReferenceName = plumbing.NewBranchReferenceName(req.Branch)
- cloneOptions.SingleBranch = true
- }
- if _, err := gogit.PlainClone(req.Dest, false, cloneOptions); err != nil {
- //A failed clone leaves a half written folder behind. Clear it so the
- //user can retry into the same path (e.g. after fixing credentials).
- if destExisted {
- emptyDir(req.Dest)
- } else {
- os.RemoveAll(req.Dest)
- }
- return classifyError(err)
- }
- return nil
- }
- // Remotes lists the configured remotes of the repository at realpath.
- func (m *Manager) Remotes(realpath string) ([]RemoteInfo, error) {
- repo, err := m.open(realpath)
- if err != nil {
- return nil, err
- }
- remotes, err := repo.Remotes()
- if err != nil {
- return nil, err
- }
- results := []RemoteInfo{}
- for _, remote := range remotes {
- cfg := remote.Config()
- results = append(results, RemoteInfo{
- Name: cfg.Name,
- URLs: cfg.URLs,
- })
- }
- return results, nil
- }
- // AddRemote registers a new remote. Passing an existing name replaces its URLs.
- func (m *Manager) AddRemote(realpath string, name string, url string) error {
- if strings.TrimSpace(name) == "" || strings.TrimSpace(url) == "" {
- return errors.New("remote name and URL are both required")
- }
- repo, err := m.open(realpath)
- if err != nil {
- return err
- }
- if _, err := repo.Remote(name); err == nil {
- if err := repo.DeleteRemote(name); err != nil {
- return err
- }
- }
- _, err = repo.CreateRemote(&config.RemoteConfig{
- Name: name,
- URLs: []string{url},
- })
- return err
- }
- // RemoveRemote deletes a remote by name.
- func (m *Manager) RemoveRemote(realpath string, name string) error {
- repo, err := m.open(realpath)
- if err != nil {
- return err
- }
- return repo.DeleteRemote(name)
- }
- // open resolves realpath to a repository, walking up to the working tree root
- // so callers may pass any path inside the repo.
- func (m *Manager) open(realpath string) (*gogit.Repository, error) {
- repo, err := gogit.PlainOpenWithOptions(realpath, &gogit.PlainOpenOptions{
- DetectDotGit: true,
- })
- if err != nil {
- if errors.Is(err, gogit.ErrRepositoryNotExists) {
- return nil, ErrNotARepo
- }
- return nil, err
- }
- return repo, nil
- }
- // worktree resolves realpath to a repository and its working tree in one go.
- func (m *Manager) worktree(realpath string) (*gogit.Repository, *gogit.Worktree, error) {
- repo, err := m.open(realpath)
- if err != nil {
- return nil, nil, err
- }
- tree, err := repo.Worktree()
- if err != nil {
- return nil, nil, err
- }
- return repo, tree, nil
- }
- // dirIsEmpty reports whether path is absent or an empty directory.
- func dirIsEmpty(path string) (bool, error) {
- entries, err := os.ReadDir(path)
- if err != nil {
- if os.IsNotExist(err) {
- return true, nil
- }
- return false, err
- }
- return len(entries) == 0, nil
- }
- // emptyDir removes everything inside path but keeps the folder itself.
- func emptyDir(path string) {
- entries, err := os.ReadDir(path)
- if err != nil {
- return
- }
- for _, entry := range entries {
- os.RemoveAll(filepath.Join(path, entry.Name()))
- }
- }
- func isDir(path string) bool {
- info, err := os.Stat(path)
- return err == nil && info.IsDir()
- }
- func isFile(path string) bool {
- info, err := os.Stat(path)
- return err == nil && !info.IsDir()
- }
|