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