GitRoot

craft your forge, build your project, grow your community freely
  1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5package repository
  6
  7import (
  8	"errors"
  9
 10	git "github.com/go-git/go-git/v5"
 11	"github.com/go-git/go-git/v5/plumbing"
 12	"github.com/go-git/go-git/v5/plumbing/object"
 13	"github.com/go-git/go-git/v5/plumbing/storer"
 14	"github.com/samber/oops"
 15	"gitroot.dev/server/logger"
 16)
 17
 18func (repo *GitRootRepository) WalkCommit(from plumbing.Hash, to plumbing.Hash, callback func(*object.Commit) error) error {
 19	repoConfiguration, err := repo.Configuration()
 20	if err != nil {
 21		return repo.errorHandler.Wrapf(err, "configuration fail")
 22	}
 23	unlock := repo.manager.repoLocks.Read(repo.name)
 24	defer unlock()
 25	return walkCommit(repo.repo, repo.manager, repoConfiguration, from, to, callback)
 26}
 27
 28func (repo *GitRootRepositoryWrite) WalkCommit(from plumbing.Hash, to plumbing.Hash, callback func(*object.Commit) error) error {
 29	repoConfiguration, err := repo.repo.Configuration()
 30	if err != nil {
 31		return repo.errorHandler.Wrapf(err, "configuration fail")
 32	}
 33	unlock := repo.repo.manager.repoLocks.Read(repo.repo.name)
 34	defer unlock()
 35	return walkCommit(repo.repoWrite, repo.repo.manager, repoConfiguration, from, to, callback)
 36}
 37
 38func walkCommit(repo *git.Repository, manager *Manager, repoConf RepoConfiguration, from plumbing.Hash, to plumbing.Hash, callback func(*object.Commit) error) error {
 39	if to == plumbing.ZeroHash { // branch deletion
 40		return nil
 41	}
 42	if from == plumbing.ZeroHash { // branch creation
 43		com, err := repo.CommitObject(to)
 44		if err != nil {
 45			return oops.With("hash", to.String()).Wrapf(err, "can't get CommitObject")
 46		}
 47		com2, err := foundAncestor(repo, manager.logger, repoConf, com)
 48		if err != nil {
 49			return oops.With("hash", to.String()).Wrapf(err, "can't get FoundAncestor")
 50		}
 51		from = com2.Hash
 52	}
 53	// cIter, err := repo.Log(&git.LogOptions{From: to})
 54	// if err != nil {
 55	// 	return err
 56	// }
 57	// display := fmt.Sprintf("----- [%s] -> [%s]\n", from.String(), to.String())
 58	// cIter.ForEach(func(commit *object.Commit) error {
 59	// 	if commit.Hash == from {
 60	// 		return storer.ErrStop
 61	// 	}
 62	// 	display = fmt.Sprintf("%s [%s] %s : %s\n", display, commit.Hash.String(), commit.Message, commit.Author.Email)
 63	// 	return nil
 64	// })
 65	// display = display + "-----\n"
 66	//os.Stdout.Write([]byte(display)) // TODO make a special logger?
 67	// cIter.Close()
 68	cIter, err := repo.Log(&git.LogOptions{From: to})
 69	if err != nil {
 70		return err
 71	}
 72	defer cIter.Close()
 73	return cIter.ForEach(func(commit *object.Commit) error {
 74		if commit.Hash == from {
 75			return storer.ErrStop
 76		}
 77		return callback(commit)
 78	})
 79}
 80
 81func foundAncestor(repo *git.Repository, log *logger.Logger, repoConf RepoConfiguration, fromCommit *object.Commit) (*object.Commit, error) {
 82	var goodBranch *object.Commit
 83	var mainBranch *object.Commit
 84	bIter, err := repo.Branches()
 85	if err != nil {
 86		return nil, errors.New("branches fail")
 87	}
 88
 89	err = bIter.ForEach(func(ref *plumbing.Reference) error {
 90		log.Info("Branch",
 91			logger.NewLoggerPair("name", ref.Name().String()),
 92			logger.NewLoggerPair("hash", ref.Hash().String()),
 93			logger.NewLoggerPair("symbolic", ref.Type() == plumbing.SymbolicReference))
 94		branchCommit, err := repo.CommitObject(ref.Hash())
 95		if err != nil {
 96			log.Error("CommitObject fail", err, logger.NewLoggerPair("hash", ref.Hash().String()))
 97			return nil
 98		}
 99		if ref.Name() == repoConf.DefaultBranch {
100			mainBranch = branchCommit
101		}
102		if ref.Hash() == fromCommit.Hash { // eliminate current branch
103			return nil
104		}
105		if ok, err := branchCommit.IsAncestor(fromCommit); ok {
106			log.Info("Ref found", logger.NewLoggerPair("hash", ref.Hash().String()))
107			if goodBranch == nil {
108				goodBranch = branchCommit
109				return nil
110			}
111			if ok, err := goodBranch.IsAncestor(branchCommit); ok {
112				goodBranch = branchCommit
113			} else if err != nil {
114				log.Error("Ancestor between branches fail", err, logger.NewLoggerPair("goodBranch", goodBranch.Hash.String()), logger.NewLoggerPair("branchCommit", branchCommit.Hash.String()))
115			}
116		} else if err != nil {
117			log.Error("IsAncestor fail", err, logger.NewLoggerPair("hash", ref.Hash().String()))
118			return nil
119		} else {
120			log.Info("Not ancestor", logger.NewLoggerPair("hash", ref.Hash().String()))
121		}
122		return nil
123	})
124	if goodBranch != nil && err == nil {
125		log.Info("Selected Ref", logger.NewLoggerPair("for", fromCommit.Hash.String()), logger.NewLoggerPair("hash", goodBranch.Hash.String()))
126		return goodBranch, nil
127	} else if goodBranch == nil {
128		log.Info("Branch not found try with mergeBase", logger.NewLoggerPair("for", fromCommit.Hash.String()))
129		if mainBranch != nil {
130			try, err := mainBranch.MergeBase(fromCommit)
131			if err != nil {
132				return nil, oops.Wrapf(err, "can't mergeBase")
133			} else if len(try) > 0 {
134				log.Info("Selected Ref", logger.NewLoggerPair("for", fromCommit.Hash.String()), logger.NewLoggerPair("hash", try[0].Hash.String()))
135				return try[0], nil
136			}
137		}
138		return nil, oops.With("for", fromCommit.Hash.String()).Errorf("Ancestor not found")
139	}
140	return nil, err
141}