1package repository
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"io"
8	"os"
9	"path/filepath"
10
11	"gitlab.com/gitlab-org/gitaly/v14/internal/git"
12	"gitlab.com/gitlab-org/gitaly/v14/internal/git/catfile"
13	"gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/transaction"
14	"gitlab.com/gitlab-org/gitaly/v14/internal/helper"
15	"gitlab.com/gitlab-org/gitaly/v14/internal/safe"
16	"gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo"
17	"gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting"
18	"gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb"
19)
20
21const attributesFileMode os.FileMode = 0o644
22
23func (s *server) applyGitattributes(ctx context.Context, c catfile.Batch, repoPath string, revision []byte) error {
24	infoPath := filepath.Join(repoPath, "info")
25	attributesPath := filepath.Join(infoPath, "attributes")
26
27	_, err := c.Info(ctx, git.Revision(revision))
28	if err != nil {
29		if catfile.IsNotFound(err) {
30			return helper.ErrInvalidArgumentf("revision does not exist")
31		}
32
33		return err
34	}
35
36	blobInfo, err := c.Info(ctx, git.Revision(fmt.Sprintf("%s:.gitattributes", revision)))
37	if err != nil && !catfile.IsNotFound(err) {
38		return err
39	}
40
41	if catfile.IsNotFound(err) || blobInfo.Type != "blob" {
42		// If there is no gitattributes file, we simply use the ZeroOID
43		// as a placeholder to vote on the removal.
44		if err := s.vote(ctx, git.ZeroOID); err != nil {
45			return fmt.Errorf("could not remove gitattributes: %w", err)
46		}
47
48		// Remove info/attributes file if there's no .gitattributes file
49		if err := os.Remove(attributesPath); err != nil && !os.IsNotExist(err) {
50			return err
51		}
52
53		return nil
54	}
55
56	// Create  /info folder if it doesn't exist
57	if err := os.MkdirAll(infoPath, 0o755); err != nil {
58		return err
59	}
60
61	blobObj, err := c.Blob(ctx, git.Revision(blobInfo.Oid))
62	if err != nil {
63		return err
64	}
65
66	writer, err := safe.NewLockingFileWriter(attributesPath, safe.LockingFileWriterConfig{
67		FileWriterConfig: safe.FileWriterConfig{FileMode: attributesFileMode},
68	})
69	if err != nil {
70		return fmt.Errorf("creating gitattributes writer: %w", err)
71	}
72	defer writer.Close()
73
74	if _, err := io.CopyN(writer, blobObj.Reader, blobInfo.Size); err != nil {
75		return err
76	}
77
78	if err := transaction.CommitLockedFile(ctx, s.txManager, writer); err != nil {
79		return fmt.Errorf("committing gitattributes: %w", err)
80	}
81
82	return nil
83}
84
85func (s *server) vote(ctx context.Context, oid git.ObjectID) error {
86	tx, err := txinfo.TransactionFromContext(ctx)
87	if errors.Is(err, txinfo.ErrTransactionNotFound) {
88		return nil
89	}
90
91	hash, err := oid.Bytes()
92	if err != nil {
93		return fmt.Errorf("vote with invalid object ID: %w", err)
94	}
95
96	vote, err := voting.VoteFromHash(hash)
97	if err != nil {
98		return fmt.Errorf("cannot convert OID to vote: %w", err)
99	}
100
101	if err := s.txManager.Vote(ctx, tx, vote); err != nil {
102		return fmt.Errorf("vote failed: %w", err)
103	}
104
105	return nil
106}
107
108func (s *server) ApplyGitattributes(ctx context.Context, in *gitalypb.ApplyGitattributesRequest) (*gitalypb.ApplyGitattributesResponse, error) {
109	repo := s.localrepo(in.GetRepository())
110	repoPath, err := s.locator.GetRepoPath(repo)
111	if err != nil {
112		return nil, err
113	}
114
115	if err := git.ValidateRevision(in.GetRevision()); err != nil {
116		return nil, helper.ErrInvalidArgumentf("revision: %v", err)
117	}
118
119	c, err := s.catfileCache.BatchProcess(ctx, repo)
120	if err != nil {
121		return nil, err
122	}
123
124	if err := s.applyGitattributes(ctx, c, repoPath, in.GetRevision()); err != nil {
125		return nil, err
126	}
127
128	return &gitalypb.ApplyGitattributesResponse{}, nil
129}
130