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 "github.com/go-git/go-git/v5/plumbing"
11 "github.com/go-git/go-git/v5/plumbing/object"
12 "github.com/go-git/go-git/v5/plumbing/protocol/packp"
13 "github.com/go-git/go-git/v5/plumbing/storer"
14 "gitroot.dev/server/user"
15)
16
17var ErrCantWrite = errors.New("user can't write")
18
19func (repo *GitRootRepositoryWrite) Merge(fromBranch string, toBranch string, commiter *user.Commiter, pusher user.SimpleUser) error {
20 if ok, _, err := repo.repo.CanWrite(commiter.Signer.PublicKey(), fromBranch); err != nil || !ok {
21 if !ok {
22 return repo.errorHandler.With("commiter", commiter.Email).With("ssh", commiter.Signer.PublicKey()).Wrapf(ErrCantWrite, "commiter can't write")
23 }
24 return repo.errorHandler.Wrapf(err, "commiter can write err")
25 }
26
27 if ok, _, err := repo.repo.CanWrite(pusher.Ssh, fromBranch); err != nil || !ok {
28 if !ok {
29 return repo.errorHandler.With("sshUser", pusher.Ssh).Wrapf(ErrCantWrite, "sshUser can't write")
30 }
31 return repo.errorHandler.Wrapf(err, "sshUser can write err")
32 }
33
34 unlock := repo.lock.Write()
35
36 fromRef, err := repo.repoWrite.Storer.Reference(plumbing.NewBranchReferenceName(fromBranch))
37 if err != nil {
38 return repo.errorHandler.Wrapf(err, "fromRef error")
39 }
40
41 toRef, err := repo.repoWrite.Storer.Reference(plumbing.NewBranchReferenceName(toBranch))
42 if err != nil {
43 return repo.errorHandler.Wrapf(err, "toRef error")
44 }
45
46 if ok, err := isFastForward(repo.repoWrite.Storer, fromRef.Hash(), toRef.Hash()); !ok {
47 return repo.errorHandler.Errorf("fastForward not possible")
48 } else if err != nil {
49 return repo.errorHandler.Wrapf(err, "sshUser can write err")
50 }
51
52 repo.repo.logger.Debug("merge isFastForward")
53
54 unlock()
55 toRefNew, err := repo.DeleteBranch(toBranch)
56 if err != nil {
57 return repo.errorHandler.Wrapf(err, "can't delete branch")
58 }
59
60 unlock = repo.lock.Write()
61
62 if err := repo.repoWrite.Storer.SetReference(plumbing.NewHashReference(plumbing.NewBranchReferenceName(fromBranch), toRefNew)); err != nil {
63 return repo.errorHandler.Wrapf(err, "SetReference err")
64 }
65
66 if err := repo.storer.RemoveReference(plumbing.NewBranchReferenceName(toBranch)); err != nil {
67 return repo.errorHandler.Wrapf(err, "DeleteBranch err %s", toBranch)
68 }
69
70 commit, err := repo.repoWrite.CommitObject(toRefNew)
71 if err != nil {
72 return repo.errorHandler.With("hash", toRefNew).Wrapf(err, "can't get commit")
73 }
74 newCommands := make([]*packp.Command, 0)
75 commitIter := object.NewCommitPreorderIter(commit, make(map[plumbing.Hash]bool), make([]plumbing.Hash, 0))
76 commitIter.ForEach(func(commit *object.Commit) error {
77 if commit.Hash.String() == fromRef.Hash().String() {
78 return storer.ErrStop
79 }
80 newCommands = append(newCommands, &packp.Command{
81 Name: plumbing.NewBranchReferenceName(fromBranch),
82 Old: commit.ParentHashes[0],
83 New: commit.Hash,
84 })
85 return nil
86 })
87 repo.repo.manager.backgroundManager.PostPush(commiter.SimpleUser, repo.repo.Name(), newCommands)
88
89 unlock()
90 return nil
91}
92
93func isFastForward(s storer.EncodedObjectStorer, old, new plumbing.Hash) (bool, error) {
94 found := false
95
96 c, err := object.GetCommit(s, new)
97 if err != nil {
98 return false, err
99 }
100
101 iter := object.NewCommitPreorderIter(c, nil, nil)
102 err = iter.ForEach(func(c *object.Commit) error {
103 if c.Hash != old {
104 return nil
105 }
106
107 found = true
108 return storer.ErrStop
109 })
110 return found, err
111}