v0.1.0 5228465

commit.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6	"bytes"
7	"time"
8)
9
10// Signature is the author or committer line of a commit or the tagger of a
11// tag. Name and Email point into the buffer of the object they come from.
12type Signature struct {
13	Name   []byte
14	Email  []byte
15	Time   int64 // seconds since the Unix epoch
16	Offset int   // time zone, in seconds east of UTC
17}
18
19// Date returns the time of the signature, in its own time zone. It allocates.
20func (s Signature) Date() time.Time {
21	t := time.Unix(s.Time, 0)
22	if s.Offset == 0 {
23		return t.UTC()
24	}
25	return t.In(time.FixedZone("", s.Offset))
26}
27
28// Commit is a parsed commit. A Commit is meant to be reused: ReadCommit and
29// ParseCommit overwrite it and reuse its storage, so a hot loop allocates
30// nothing after the first few commits. Every []byte field is valid until the
31// next call that fills this Commit.
32type Commit struct {
33	ID        ObjectID // set by ReadCommit
34	Tree      ObjectID
35	Parents   []ObjectID
36	Author    Signature
37	Committer Signature
38	// Encoding is the value of the encoding header, nil when absent. The
39	// other headers (gpgsig, mergetag...) are skipped.
40	Encoding []byte
41	// Message is everything after the blank line that ends the headers,
42	// as raw bytes: it may not be valid UTF-8.
43	Message []byte
44
45	buf []byte
46}
47
48// ReadCommit reads and parses the commit id into c.
49func (r *Repo) ReadCommit(id ObjectID, c *Commit) error {
50	typ, data, err := r.ReadObject(id, c.buf)
51	if err != nil {
52		return err
53	}
54	c.buf = data
55	if typ != TypeCommit {
56		return wrongType(id, typ, TypeCommit)
57	}
58	if err := ParseCommit(data, c); err != nil {
59		return wrapObj(id, err)
60	}
61	c.ID = id
62	return nil
63}
64
65// ParseCommit parses the content of a commit object into c. The fields of c
66// point into data, which must stay unchanged while they are used.
67//
68// It reads headers the way Git does: the first line must be the tree, the
69// parent lines are the ones that immediately follow it, and any tree or
70// parent line further down is an ordinary, ignored header. It returns
71// ErrCorrupt if the first line is not a valid tree or a parent is malformed.
72func ParseCommit(data []byte, c *Commit) error {
73	c.ID = ObjectID{}
74	c.Parents = c.Parents[:0]
75	c.Author, c.Committer = Signature{}, Signature{}
76	c.Encoding, c.Message = nil, nil
77	const (
78		stTree = iota
79		stParents
80		stRest
81	)
82	state := stTree
83	for len(data) > 0 {
84		if data[0] == '\n' && state != stTree {
85			c.Message = data[1:]
86			break
87		}
88		var line []byte
89		if i := bytes.IndexByte(data, '\n'); i >= 0 {
90			line, data = data[:i], data[i+1:]
91		} else {
92			line, data = data, nil
93		}
94		key, val, _ := bytes.Cut(line, []byte{' '})
95		switch state {
96		case stTree:
97			id, ok := parseIDBytes(val)
98			if string(key) != "tree" || !ok {
99				return corrupt("commit: the first header is not a valid tree")
100			}
101			c.Tree = id
102			state = stParents
103			continue
104		case stParents:
105			if string(key) == "parent" {
106				id, ok := parseIDBytes(val)
107				if !ok {
108					return corrupt("commit: bad parent header")
109				}
110				c.Parents = append(c.Parents, id)
111				continue
112			}
113			state = stRest
114		}
115		if line[0] == ' ' {
116			continue // continuation of a multi-line header such as gpgsig
117		}
118		switch string(key) {
119		case "author":
120			c.Author = parseSignature(val)
121		case "committer":
122			c.Committer = parseSignature(val)
123		case "encoding":
124			c.Encoding = val
125		}
126	}
127	if state == stTree {
128		return corrupt("commit: no tree")
129	}
130	return nil
131}
132
133// parseSignature parses "Name <email> <seconds> <+hhmm>". It is as lenient
134// as Git: what cannot be parsed is left zero.
135func parseSignature(b []byte) Signature {
136	var s Signature
137	lt := bytes.IndexByte(b, '<')
138	gt := bytes.LastIndexByte(b, '>')
139	if lt < 0 || gt < lt {
140		s.Name = bytes.TrimSpace(b)
141		return s
142	}
143	s.Name = bytes.TrimRight(b[:lt], " \t\r\n")
144	s.Email = b[lt+1 : gt]
145	rest := bytes.TrimLeft(b[gt+1:], " ")
146	secs, rest, _ := bytes.Cut(rest, []byte{' '})
147	if t, ok := parseInt(secs); ok {
148		s.Time = t
149		if tz := bytes.TrimSpace(rest); len(tz) == 5 && (tz[0] == '+' || tz[0] == '-') {
150			hh, ok1 := parseInt(tz[1:3])
151			mm, ok2 := parseInt(tz[3:5])
152			if ok1 && ok2 {
153				off := int(hh*3600 + mm*60)
154				if tz[0] == '-' {
155					off = -off
156				}
157				s.Offset = off
158			}
159		}
160	}
161	return s
162}
163
164// parseInt parses a decimal integer of up to 18 digits.
165func parseInt(b []byte) (int64, bool) {
166	if len(b) == 0 || len(b) > 18 {
167		return 0, false
168	}
169	var v int64
170	for _, c := range b {
171		if c < '0' || c > '9' {
172			return 0, false
173		}
174		v = v*10 + int64(c-'0')
175	}
176	return v, true
177}
178
179// Tag is a parsed annotated tag. Name, Tagger and Message point into the
180// data that was parsed.
181type Tag struct {
182	Object  ObjectID
183	Type    Type // the type of Object
184	Name    []byte
185	Tagger  Signature
186	Message []byte
187}
188
189// ReadTag reads and parses the annotated tag id into t. The byte fields of t
190// are copies, owned by the caller.
191func (r *Repo) ReadTag(id ObjectID, t *Tag) error {
192	b, err := r.readTyped(id, TypeTag)
193	if err != nil {
194		return err
195	}
196	defer putBuf(b)
197	if err := ParseTag(b.b, t); err != nil {
198		return wrapObj(id, err)
199	}
200	// The pooled buffer goes back: detach the fields.
201	t.Name = bytes.Clone(t.Name)
202	t.Message = bytes.Clone(t.Message)
203	t.Tagger.Name = bytes.Clone(t.Tagger.Name)
204	t.Tagger.Email = bytes.Clone(t.Tagger.Email)
205	return nil
206}
207
208// ParseTag parses the content of a tag object into t, whose byte fields point
209// into data. It returns ErrCorrupt if the object or its type is missing.
210func ParseTag(data []byte, t *Tag) error {
211	*t = Tag{}
212	haveObj, haveType := false, false
213	for len(data) > 0 {
214		if data[0] == '\n' {
215			t.Message = data[1:]
216			break
217		}
218		var line []byte
219		if i := bytes.IndexByte(data, '\n'); i >= 0 {
220			line, data = data[:i], data[i+1:]
221		} else {
222			line, data = data, nil
223		}
224		key, val, _ := bytes.Cut(line, []byte{' '})
225		switch string(key) {
226		case "object":
227			id, ok := parseIDBytes(val)
228			if !ok {
229				return corrupt("tag: bad object header")
230			}
231			t.Object, haveObj = id, true
232		case "type":
233			typ, ok := parseTypeName(val)
234			if !ok {
235				return corrupt("tag: bad type header")
236			}
237			t.Type, haveType = typ, true
238		case "tag":
239			t.Name = val
240		case "tagger":
241			t.Tagger = parseSignature(val)
242		}
243	}
244	if !haveObj || !haveType {
245		return corrupt("tag: missing object or type")
246	}
247	return nil
248}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 2

↑↓ moveenter openesc close