1// Copyright 2015 The Gogs Authors. All rights reserved.
2// Copyright 2018 The Gitea Authors. All rights reserved.
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file.
5
6package git
7
8import (
9	"bufio"
10	"bytes"
11	"errors"
12	"fmt"
13	"io"
14	"os/exec"
15	"strconv"
16	"strings"
17
18	"code.gitea.io/gitea/modules/log"
19)
20
21// Commit represents a git commit.
22type Commit struct {
23	Branch string // Branch this commit belongs to
24	Tree
25	ID            SHA1 // The ID of this commit object
26	Author        *Signature
27	Committer     *Signature
28	CommitMessage string
29	Signature     *CommitGPGSignature
30
31	Parents        []SHA1 // SHA1 strings
32	submoduleCache *ObjectCache
33}
34
35// CommitGPGSignature represents a git commit signature part.
36type CommitGPGSignature struct {
37	Signature string
38	Payload   string //TODO check if can be reconstruct from the rest of commit information to not have duplicate data
39}
40
41// Message returns the commit message. Same as retrieving CommitMessage directly.
42func (c *Commit) Message() string {
43	return c.CommitMessage
44}
45
46// Summary returns first line of commit message.
47func (c *Commit) Summary() string {
48	return strings.Split(strings.TrimSpace(c.CommitMessage), "\n")[0]
49}
50
51// ParentID returns oid of n-th parent (0-based index).
52// It returns nil if no such parent exists.
53func (c *Commit) ParentID(n int) (SHA1, error) {
54	if n >= len(c.Parents) {
55		return SHA1{}, ErrNotExist{"", ""}
56	}
57	return c.Parents[n], nil
58}
59
60// Parent returns n-th parent (0-based index) of the commit.
61func (c *Commit) Parent(n int) (*Commit, error) {
62	id, err := c.ParentID(n)
63	if err != nil {
64		return nil, err
65	}
66	parent, err := c.repo.getCommit(id)
67	if err != nil {
68		return nil, err
69	}
70	return parent, nil
71}
72
73// ParentCount returns number of parents of the commit.
74// 0 if this is the root commit,  otherwise 1,2, etc.
75func (c *Commit) ParentCount() int {
76	return len(c.Parents)
77}
78
79// GetCommitByPath return the commit of relative path object.
80func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) {
81	return c.repo.getCommitByPathWithID(c.ID, relpath)
82}
83
84// AddChanges marks local changes to be ready for commit.
85func AddChanges(repoPath string, all bool, files ...string) error {
86	return AddChangesWithArgs(repoPath, GlobalCommandArgs, all, files...)
87}
88
89// AddChangesWithArgs marks local changes to be ready for commit.
90func AddChangesWithArgs(repoPath string, globalArgs []string, all bool, files ...string) error {
91	cmd := NewCommandNoGlobals(append(globalArgs, "add")...)
92	if all {
93		cmd.AddArguments("--all")
94	}
95	cmd.AddArguments("--")
96	_, err := cmd.AddArguments(files...).RunInDir(repoPath)
97	return err
98}
99
100// CommitChangesOptions the options when a commit created
101type CommitChangesOptions struct {
102	Committer *Signature
103	Author    *Signature
104	Message   string
105}
106
107// CommitChanges commits local changes with given committer, author and message.
108// If author is nil, it will be the same as committer.
109func CommitChanges(repoPath string, opts CommitChangesOptions) error {
110	cargs := make([]string, len(GlobalCommandArgs))
111	copy(cargs, GlobalCommandArgs)
112	return CommitChangesWithArgs(repoPath, cargs, opts)
113}
114
115// CommitChangesWithArgs commits local changes with given committer, author and message.
116// If author is nil, it will be the same as committer.
117func CommitChangesWithArgs(repoPath string, args []string, opts CommitChangesOptions) error {
118	cmd := NewCommandNoGlobals(args...)
119	if opts.Committer != nil {
120		cmd.AddArguments("-c", "user.name="+opts.Committer.Name, "-c", "user.email="+opts.Committer.Email)
121	}
122	cmd.AddArguments("commit")
123
124	if opts.Author == nil {
125		opts.Author = opts.Committer
126	}
127	if opts.Author != nil {
128		cmd.AddArguments(fmt.Sprintf("--author='%s <%s>'", opts.Author.Name, opts.Author.Email))
129	}
130	cmd.AddArguments("-m", opts.Message)
131
132	_, err := cmd.RunInDir(repoPath)
133	// No stderr but exit status 1 means nothing to commit.
134	if err != nil && err.Error() == "exit status 1" {
135		return nil
136	}
137	return err
138}
139
140// AllCommitsCount returns count of all commits in repository
141func AllCommitsCount(repoPath string, hidePRRefs bool, files ...string) (int64, error) {
142	args := []string{"--all", "--count"}
143	if hidePRRefs {
144		args = append([]string{"--exclude=" + PullPrefix + "*"}, args...)
145	}
146	cmd := NewCommand("rev-list")
147	cmd.AddArguments(args...)
148	if len(files) > 0 {
149		cmd.AddArguments("--")
150		cmd.AddArguments(files...)
151	}
152
153	stdout, err := cmd.RunInDir(repoPath)
154	if err != nil {
155		return 0, err
156	}
157
158	return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
159}
160
161// CommitsCountFiles returns number of total commits of until given revision.
162func CommitsCountFiles(repoPath string, revision, relpath []string) (int64, error) {
163	cmd := NewCommand("rev-list", "--count")
164	cmd.AddArguments(revision...)
165	if len(relpath) > 0 {
166		cmd.AddArguments("--")
167		cmd.AddArguments(relpath...)
168	}
169
170	stdout, err := cmd.RunInDir(repoPath)
171	if err != nil {
172		return 0, err
173	}
174
175	return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
176}
177
178// CommitsCount returns number of total commits of until given revision.
179func CommitsCount(repoPath string, revision ...string) (int64, error) {
180	return CommitsCountFiles(repoPath, revision, []string{})
181}
182
183// CommitsCount returns number of total commits of until current revision.
184func (c *Commit) CommitsCount() (int64, error) {
185	return CommitsCount(c.repo.Path, c.ID.String())
186}
187
188// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
189func (c *Commit) CommitsByRange(page, pageSize int) ([]*Commit, error) {
190	return c.repo.commitsByRange(c.ID, page, pageSize)
191}
192
193// CommitsBefore returns all the commits before current revision
194func (c *Commit) CommitsBefore() ([]*Commit, error) {
195	return c.repo.getCommitsBefore(c.ID)
196}
197
198// HasPreviousCommit returns true if a given commitHash is contained in commit's parents
199func (c *Commit) HasPreviousCommit(commitHash SHA1) (bool, error) {
200	this := c.ID.String()
201	that := commitHash.String()
202
203	if this == that {
204		return false, nil
205	}
206
207	if err := CheckGitVersionAtLeast("1.8"); err == nil {
208		_, err := NewCommand("merge-base", "--is-ancestor", that, this).RunInDir(c.repo.Path)
209		if err == nil {
210			return true, nil
211		}
212		var exitError *exec.ExitError
213		if errors.As(err, &exitError) {
214			if exitError.ProcessState.ExitCode() == 1 && len(exitError.Stderr) == 0 {
215				return false, nil
216			}
217		}
218		return false, err
219	}
220
221	result, err := NewCommand("rev-list", "--ancestry-path", "-n1", that+".."+this, "--").RunInDir(c.repo.Path)
222	if err != nil {
223		return false, err
224	}
225
226	return len(strings.TrimSpace(result)) > 0, nil
227}
228
229// CommitsBeforeLimit returns num commits before current revision
230func (c *Commit) CommitsBeforeLimit(num int) ([]*Commit, error) {
231	return c.repo.getCommitsBeforeLimit(c.ID, num)
232}
233
234// CommitsBeforeUntil returns the commits between commitID to current revision
235func (c *Commit) CommitsBeforeUntil(commitID string) ([]*Commit, error) {
236	endCommit, err := c.repo.GetCommit(commitID)
237	if err != nil {
238		return nil, err
239	}
240	return c.repo.CommitsBetween(c, endCommit)
241}
242
243// SearchCommitsOptions specify the parameters for SearchCommits
244type SearchCommitsOptions struct {
245	Keywords            []string
246	Authors, Committers []string
247	After, Before       string
248	All                 bool
249}
250
251// NewSearchCommitsOptions construct a SearchCommitsOption from a space-delimited search string
252func NewSearchCommitsOptions(searchString string, forAllRefs bool) SearchCommitsOptions {
253	var keywords, authors, committers []string
254	var after, before string
255
256	fields := strings.Fields(searchString)
257	for _, k := range fields {
258		switch {
259		case strings.HasPrefix(k, "author:"):
260			authors = append(authors, strings.TrimPrefix(k, "author:"))
261		case strings.HasPrefix(k, "committer:"):
262			committers = append(committers, strings.TrimPrefix(k, "committer:"))
263		case strings.HasPrefix(k, "after:"):
264			after = strings.TrimPrefix(k, "after:")
265		case strings.HasPrefix(k, "before:"):
266			before = strings.TrimPrefix(k, "before:")
267		default:
268			keywords = append(keywords, k)
269		}
270	}
271
272	return SearchCommitsOptions{
273		Keywords:   keywords,
274		Authors:    authors,
275		Committers: committers,
276		After:      after,
277		Before:     before,
278		All:        forAllRefs,
279	}
280}
281
282// SearchCommits returns the commits match the keyword before current revision
283func (c *Commit) SearchCommits(opts SearchCommitsOptions) ([]*Commit, error) {
284	return c.repo.searchCommits(c.ID, opts)
285}
286
287// GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision
288func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error) {
289	return c.repo.getFilesChanged(pastCommit, c.ID.String())
290}
291
292// FileChangedSinceCommit Returns true if the file given has changed since the the past commit
293// YOU MUST ENSURE THAT pastCommit is a valid commit ID.
294func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) {
295	return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String())
296}
297
298// HasFile returns true if the file given exists on this commit
299// This does only mean it's there - it does not mean the file was changed during the commit.
300func (c *Commit) HasFile(filename string) (bool, error) {
301	_, err := c.GetBlobByPath(filename)
302	if err != nil {
303		return false, err
304	}
305	return true, nil
306}
307
308// GetSubModules get all the sub modules of current revision git tree
309func (c *Commit) GetSubModules() (*ObjectCache, error) {
310	if c.submoduleCache != nil {
311		return c.submoduleCache, nil
312	}
313
314	entry, err := c.GetTreeEntryByPath(".gitmodules")
315	if err != nil {
316		if _, ok := err.(ErrNotExist); ok {
317			return nil, nil
318		}
319		return nil, err
320	}
321
322	rd, err := entry.Blob().DataAsync()
323	if err != nil {
324		return nil, err
325	}
326
327	defer rd.Close()
328	scanner := bufio.NewScanner(rd)
329	c.submoduleCache = newObjectCache()
330	var ismodule bool
331	var path string
332	for scanner.Scan() {
333		if strings.HasPrefix(scanner.Text(), "[submodule") {
334			ismodule = true
335			continue
336		}
337		if ismodule {
338			fields := strings.Split(scanner.Text(), "=")
339			k := strings.TrimSpace(fields[0])
340			if k == "path" {
341				path = strings.TrimSpace(fields[1])
342			} else if k == "url" {
343				c.submoduleCache.Set(path, &SubModule{path, strings.TrimSpace(fields[1])})
344				ismodule = false
345			}
346		}
347	}
348
349	return c.submoduleCache, nil
350}
351
352// GetSubModule get the sub module according entryname
353func (c *Commit) GetSubModule(entryname string) (*SubModule, error) {
354	modules, err := c.GetSubModules()
355	if err != nil {
356		return nil, err
357	}
358
359	if modules != nil {
360		module, has := modules.Get(entryname)
361		if has {
362			return module.(*SubModule), nil
363		}
364	}
365	return nil, nil
366}
367
368// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only')
369func (c *Commit) GetBranchName() (string, error) {
370	err := LoadGitVersion()
371	if err != nil {
372		return "", fmt.Errorf("Git version missing: %v", err)
373	}
374
375	args := []string{
376		"name-rev",
377	}
378	if CheckGitVersionAtLeast("2.13.0") == nil {
379		args = append(args, "--exclude", "refs/tags/*")
380	}
381	args = append(args, "--name-only", "--no-undefined", c.ID.String())
382
383	data, err := NewCommand(args...).RunInDir(c.repo.Path)
384	if err != nil {
385		// handle special case where git can not describe commit
386		if strings.Contains(err.Error(), "cannot describe") {
387			return "", nil
388		}
389
390		return "", err
391	}
392
393	// name-rev commitID output will be "master" or "master~12"
394	return strings.SplitN(strings.TrimSpace(data), "~", 2)[0], nil
395}
396
397// LoadBranchName load branch name for commit
398func (c *Commit) LoadBranchName() (err error) {
399	if len(c.Branch) != 0 {
400		return
401	}
402
403	c.Branch, err = c.GetBranchName()
404	return
405}
406
407// GetTagName gets the current tag name for given commit
408func (c *Commit) GetTagName() (string, error) {
409	data, err := NewCommand("describe", "--exact-match", "--tags", "--always", c.ID.String()).RunInDir(c.repo.Path)
410	if err != nil {
411		// handle special case where there is no tag for this commit
412		if strings.Contains(err.Error(), "no tag exactly matches") {
413			return "", nil
414		}
415
416		return "", err
417	}
418
419	return strings.TrimSpace(data), nil
420}
421
422// CommitFileStatus represents status of files in a commit.
423type CommitFileStatus struct {
424	Added    []string
425	Removed  []string
426	Modified []string
427}
428
429// NewCommitFileStatus creates a CommitFileStatus
430func NewCommitFileStatus() *CommitFileStatus {
431	return &CommitFileStatus{
432		[]string{}, []string{}, []string{},
433	}
434}
435
436func parseCommitFileStatus(fileStatus *CommitFileStatus, stdout io.Reader) {
437	rd := bufio.NewReader(stdout)
438	peek, err := rd.Peek(1)
439	if err != nil {
440		if err != io.EOF {
441			log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
442		}
443		return
444	}
445	if peek[0] == '\n' || peek[0] == '\x00' {
446		_, _ = rd.Discard(1)
447	}
448	for {
449		modifier, err := rd.ReadSlice('\x00')
450		if err != nil {
451			if err != io.EOF {
452				log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
453			}
454			return
455		}
456		file, err := rd.ReadString('\x00')
457		if err != nil {
458			if err != io.EOF {
459				log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
460			}
461			return
462		}
463		file = file[:len(file)-1]
464		switch modifier[0] {
465		case 'A':
466			fileStatus.Added = append(fileStatus.Added, file)
467		case 'D':
468			fileStatus.Removed = append(fileStatus.Removed, file)
469		case 'M':
470			fileStatus.Modified = append(fileStatus.Modified, file)
471		}
472	}
473}
474
475// GetCommitFileStatus returns file status of commit in given repository.
476func GetCommitFileStatus(repoPath, commitID string) (*CommitFileStatus, error) {
477	stdout, w := io.Pipe()
478	done := make(chan struct{})
479	fileStatus := NewCommitFileStatus()
480	go func() {
481		parseCommitFileStatus(fileStatus, stdout)
482		close(done)
483	}()
484
485	stderr := new(bytes.Buffer)
486	args := []string{"log", "--name-status", "-c", "--pretty=format:", "--parents", "--no-renames", "-z", "-1", commitID}
487
488	err := NewCommand(args...).RunInDirPipeline(repoPath, w, stderr)
489	w.Close() // Close writer to exit parsing goroutine
490	if err != nil {
491		return nil, ConcatenateError(err, stderr.String())
492	}
493
494	<-done
495	return fileStatus, nil
496}
497
498// GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository.
499func GetFullCommitID(repoPath, shortID string) (string, error) {
500	commitID, err := NewCommand("rev-parse", shortID).RunInDir(repoPath)
501	if err != nil {
502		if strings.Contains(err.Error(), "exit status 128") {
503			return "", ErrNotExist{shortID, ""}
504		}
505		return "", err
506	}
507	return strings.TrimSpace(commitID), nil
508}
509
510// GetRepositoryDefaultPublicGPGKey returns the default public key for this commit
511func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) {
512	if c.repo == nil {
513		return nil, nil
514	}
515	return c.repo.GetDefaultPublicGPGKey(forceUpdate)
516}
517