package conventionalcommit // Buffer represents a commit message in a more structured form than a simple // string or byte slice. This makes it easier to process a message for the // purposes of extracting detailed information, linting, and formatting. // // The commit message is conceptually broken down into two three separate // sections: // // - Head section holds the commit message subject/description, along with type // and scope for conventional commits. The head section should only ever be a // single line according to git convention, but Buffer supports multi-line // headers so they can be parsed and handled as needed. // // - Body section holds the rest of the message. Except if the last paragraph // starts with a footer token, then the last paragraph is omitted from the // body section. // // - Foot section holds conventional commit footers. It is always the last // paragraph of a commit message, and is only considered to be the foot // section if the first line of the paragraph beings with a footer token. // // Each section is returned as a Lines type, which provides per-line access to // the text within the section. type Buffer struct { // firstLine is the lines offset for the first line which contains any // non-whitespace character. firstLine int // lastLine is the lines offset for the last line which contains any // non-whitespace character. lastLine int // headLen is the number of lines that the headLen section (first paragraph) // spans. headLen int // footLen is the number of lines that the footLen section (last paragraph) // spans. footLen int // lines is a list of all individual lines of text in the commit message, // which also includes the original line number, making it easy to pass a // single Line around while still knowing where in the original commit // message it belongs. lines Lines } // NewBuffer returns a Buffer, with the given commit message broken down into // individual lines of text, with sequential non-empty lines grouped into // paragraphs. func NewBuffer(message []byte) *Buffer { buf := &Buffer{ lines: Lines{}, } if len(message) == 0 { return buf } buf.lines = NewLines(message) // Find fist non-whitespace line. if i := buf.lines.FirstTextIndex(); i > -1 { buf.firstLine = i } // Find last non-whitespace line. if i := buf.lines.LastTextIndex(); i > -1 { buf.lastLine = i } // Determine number of lines in first paragraph (head section). for i := buf.firstLine; i <= buf.lastLine; i++ { if buf.lines[i].Blank() { break } buf.headLen++ } // Determine number of lines in the last paragraph. lastLen := 0 for i := buf.lastLine; i > buf.firstLine+buf.headLen; i-- { if buf.lines[i].Blank() { break } lastLen++ } // If last paragraph starts with a Conventional Commit footer token, it is // the foot section, otherwise it is part of the body. if lastLen > 0 { line := buf.lines[buf.lastLine-lastLen+1] if FooterToken.Match(line.Content) { buf.footLen = lastLen } } return buf } // Head returns the first paragraph, defined as the first group of sequential // lines which contain any non-whitespace characters. func (s *Buffer) Head() Lines { return s.lines[s.firstLine : s.firstLine+s.headLen] } // Body returns all lines between the first and last paragraphs. If the body is // surrounded by multiple empty lines, they will be removed, ensuring first and // last line of body is not a blank whitespace line. func (s *Buffer) Body() Lines { if s.firstLine == s.lastLine { return Lines{} } first := s.firstLine + s.headLen + 1 last := s.lastLine + 1 if s.footLen > 0 { last -= s.footLen } return s.lines[first:last].Trim() } // Head returns the last paragraph, defined as the last group of sequential // lines which contain any non-whitespace characters. func (s *Buffer) Foot() Lines { if s.footLen == 0 { return Lines{} } return s.lines[s.lastLine-s.footLen+1 : s.lastLine+1] } // Lines returns all lines with any blank lines from the beginning and end of // the buffer removed. Effectively all lines from the first to the last line // which contain any non-whitespace characters. func (s *Buffer) Lines() Lines { if s.lastLine+1 > len(s.lines) || (s.lastLine == 0 && s.lines[0].Blank()) { return Lines{} } return s.lines[s.firstLine : s.lastLine+1] } // LinesRaw returns all lines of the buffer including any blank lines at the // beginning and end of the buffer. func (s *Buffer) LinesRaw() Lines { return s.lines } // LineCount returns number of lines in the buffer after discarding blank lines // from the beginning and end of the buffer. Effectively counting all lines from // the first to the last line which contain any non-whitespace characters. func (s *Buffer) LineCount() int { if s.headLen == 0 { return 0 } return (s.lastLine + 1) - s.firstLine } // LineCountRaw returns the number of lines in the buffer including any blank // lines at the beginning and end of the buffer. func (s *Buffer) LineCountRaw() int { return len(s.lines) } // Bytes renders the Buffer back into a byte slice, without any leading or // trailing whitespace lines. Leading whitespace on the first line which // contains non-whitespace characters is retained. It is only whole lines // consisting of only whitespace which are excluded. func (s *Buffer) Bytes() []byte { return s.Lines().Bytes() } // String renders the Buffer back into a string, without any leading or trailing // whitespace lines. Leading whitespace on the first line which contains // non-whitespace characters is retained. It is only whole lines consisting of // only whitespace which are excluded. func (s *Buffer) String() string { return s.Lines().String() } // BytesRaw renders the Buffer back into a byte slice which is identical to the // original input byte slice given to NewBuffer. This includes retaining the // original line break types for each line. func (s *Buffer) BytesRaw() []byte { return s.lines.Bytes() } // StringRaw renders the Buffer back into a string which is identical to the // original input byte slice given to NewBuffer. This includes retaining the // original line break types for each line. func (s *Buffer) StringRaw() string { return s.lines.String() }