Fast implementation of Git in pure Go
at master 161 lines 4.1 kB view raw
1package furgit 2 3import ( 4 "bytes" 5 "errors" 6 "fmt" 7) 8 9// Commit represents a Git commit object. 10type Commit struct { 11 // Tree represents the tree hash referenced by the commit. 12 Tree Hash 13 // Parents represents the parent commit hashes. 14 // Commits that have 0 parents are root commits. 15 // Commits that have >= 2 parents are merge commits. 16 Parents []Hash 17 // Author represents the author of the commit. 18 Author Ident 19 // Committer represents the committer of the commit. 20 Committer Ident 21 // Message represents the commit message. 22 Message []byte 23 // ChangeID represents the change-id header used by 24 // Gerrit and Jujutsu. 25 ChangeID string 26 // ExtraHeaders holds any extra headers present in the commit. 27 ExtraHeaders []ExtraHeader 28} 29 30// StoredCommit represents a commit stored in the object database. 31type StoredCommit struct { 32 Commit 33 hash Hash 34} 35 36// Hash returns the hash of the stored commit. 37func (sCommit *StoredCommit) Hash() Hash { 38 return sCommit.hash 39} 40 41// ObjectType returns the object type of the commit. 42// 43// It always returns ObjectTypeCommit. 44func (commit *Commit) ObjectType() ObjectType { 45 _ = commit 46 return ObjectTypeCommit 47} 48 49func parseCommit(id Hash, body []byte, repo *Repository) (*StoredCommit, error) { 50 c := new(StoredCommit) 51 c.hash = id 52 i := 0 53 for i < len(body) { 54 rel := bytes.IndexByte(body[i:], '\n') 55 if rel < 0 { 56 return nil, errors.New("furgit: commit: missing newline") 57 } 58 line := body[i : i+rel] 59 i += rel + 1 60 if len(line) == 0 { 61 break 62 } 63 64 switch { 65 case bytes.HasPrefix(line, []byte("tree ")): 66 treeID, err := repo.ParseHash(string(line[5:])) 67 if err != nil { 68 return nil, fmt.Errorf("furgit: commit: tree: %w", err) 69 } 70 c.Tree = treeID 71 case bytes.HasPrefix(line, []byte("parent ")): 72 parent, err := repo.ParseHash(string(line[7:])) 73 if err != nil { 74 return nil, fmt.Errorf("furgit: commit: parent: %w", err) 75 } 76 c.Parents = append(c.Parents, parent) 77 case bytes.HasPrefix(line, []byte("change-id ")): 78 c.ChangeID = string(line) 79 case bytes.HasPrefix(line, []byte("author ")): 80 idt, err := parseIdent(line[7:]) 81 if err != nil { 82 return nil, fmt.Errorf("furgit: commit: author: %w", err) 83 } 84 c.Author = *idt 85 case bytes.HasPrefix(line, []byte("committer ")): 86 idt, err := parseIdent(line[10:]) 87 if err != nil { 88 return nil, fmt.Errorf("furgit: commit: committer: %w", err) 89 } 90 c.Committer = *idt 91 case bytes.HasPrefix(line, []byte("gpgsig ")), bytes.HasPrefix(line, []byte("gpgsig-sha256 ")): 92 // TODO: handle this 93 for i < len(body) { 94 nextRel := bytes.IndexByte(body[i:], '\n') 95 if nextRel < 0 { 96 return nil, errors.New("furgit: commit: unterminated gpgsig") 97 } 98 if body[i] != ' ' { 99 break 100 } 101 i += nextRel + 1 102 } 103 default: 104 key, value, found := bytes.Cut(line, []byte{' '}) 105 if !found { 106 return nil, errors.New("furgit: commit: malformed header") 107 } 108 c.ExtraHeaders = append(c.ExtraHeaders, ExtraHeader{Key: string(key), Value: value}) 109 } 110 } 111 112 if i > len(body) { 113 return nil, ErrInvalidObject 114 } 115 116 c.Message = append([]byte(nil), body[i:]...) 117 return c, nil 118} 119 120func (commit *Commit) serialize() ([]byte, error) { 121 var buf bytes.Buffer 122 fmt.Fprintf(&buf, "tree %s\n", commit.Tree.String()) 123 for _, p := range commit.Parents { 124 fmt.Fprintf(&buf, "parent %s\n", p.String()) 125 } 126 buf.WriteString("author ") 127 ab, err := commit.Author.Serialize() 128 if err != nil { 129 return nil, err 130 } 131 buf.Write(ab) 132 buf.WriteByte('\n') 133 buf.WriteString("committer ") 134 cb, err := commit.Committer.Serialize() 135 if err != nil { 136 return nil, err 137 } 138 buf.Write(cb) 139 buf.WriteByte('\n') 140 buf.WriteByte('\n') 141 buf.Write(commit.Message) 142 143 return buf.Bytes(), nil 144} 145 146// Serialize renders the commit into its raw byte representation, 147// including the header (i.e., "type size\0"). 148func (commit *Commit) Serialize() ([]byte, error) { 149 body, err := commit.serialize() 150 if err != nil { 151 return nil, err 152 } 153 header, err := headerForType(ObjectTypeCommit, body) 154 if err != nil { 155 return nil, err 156 } 157 raw := make([]byte, len(header)+len(body)) 158 copy(raw, header) 159 copy(raw[len(header):], body) 160 return raw, nil 161}