github.com/driusan/dgit@v0.0.0-20221118233547-f39f0c15edbb/git/commit.go (about)

     1  package git
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"os"
     7  	"regexp"
     8  	"strings"
     9  	"time"
    10  )
    11  
    12  type CommitOptions struct {
    13  	All   bool
    14  	Patch bool
    15  
    16  	ResetAuthor bool
    17  	Amend       bool
    18  
    19  	Date   time.Time
    20  	Author Person
    21  
    22  	Signoff           bool
    23  	NoVerify          bool
    24  	AllowEmpty        bool
    25  	AllowEmptyMessage bool
    26  
    27  	NoPostRewrite bool
    28  	Include       bool
    29  	Only          bool
    30  	Quiet         bool
    31  
    32  	CleanupMode string
    33  	NoEdit      bool
    34  
    35  	// Should be passed to CommitTree, which needs support first:
    36  	// GPGSign GPGKeyID
    37  	// NoGpgSign bool
    38  
    39  	// Things that are used to create the commit message and need to be
    40  	// parsed by package cmd/, but not included here.
    41  	//	ReuseMessage, ReeditMessage, Fixup, Squash Commitish
    42  	// File string
    43  	// Message string (-m)
    44  	// Template File (COMMIT_EDITMSG)
    45  	// Status, NoStatus bool
    46  	// Verbose bool
    47  	//
    48  
    49  	// Things that only affect the output with --dry-run.
    50  	// Note: Printing the status after --dry-run isn't implemented,
    51  	// all it does is prevent the call to UpdateRef after CommitTree.
    52  	// Most of these are a no-op.
    53  	DryRun        bool
    54  	Short         bool
    55  	Branch        bool
    56  	Porcelain     bool
    57  	Long          bool
    58  	NullTerminate bool
    59  	UntrackedMode StatusUntrackedMode
    60  
    61  	// FIXME: Add all the missing options here.
    62  }
    63  
    64  // Commit implements the command "git commit" in the repository pointed
    65  // to by c.
    66  func Commit(c *Client, opts CommitOptions, message CommitMessage, files []File) (CommitID, error) {
    67  	if !opts.AllowEmptyMessage && message == "" {
    68  		return CommitID{}, fmt.Errorf("Aborting commit due to empty commit message.")
    69  	}
    70  	if opts.Patch {
    71  		return CommitID{}, fmt.Errorf("Commit --patch not implemented")
    72  	}
    73  
    74  	var idx *Index
    75  
    76  	if opts.All {
    77  		var tostage []File
    78  		if opts.Include {
    79  			tostage = files
    80  		}
    81  
    82  		if _, err := Add(c, AddOptions{Update: true, DryRun: opts.DryRun}, tostage); err != nil {
    83  			log.Println("Commit adding files:", err)
    84  			return CommitID{}, err
    85  		}
    86  	}
    87  
    88  	if !opts.All && len(files) != 0 {
    89  		var idx1 *Index
    90  		head, err := c.GetHeadCommit()
    91  		if err != nil {
    92  			idx1 = NewIndex()
    93  		} else {
    94  			idx1, err = ReadTree(c, ReadTreeOptions{DryRun: true}, head)
    95  			if err != nil {
    96  				return CommitID{}, err
    97  			}
    98  		}
    99  		idx2, err := UpdateIndex(c, idx1, UpdateIndexOptions{Add: true, Remove: true}, files)
   100  		if err != nil {
   101  			return CommitID{}, err
   102  		}
   103  		idx = idx2
   104  	} else {
   105  		idx1, err := c.GitDir.ReadIndex()
   106  		if err != nil {
   107  			return CommitID{}, err
   108  		}
   109  		idx = idx1
   110  	}
   111  	// Happy path: write the tree
   112  	treeid, err := WriteTreeFromIndex(c, idx, WriteTreeOptions{})
   113  	if err != nil {
   114  		return CommitID{}, err
   115  	}
   116  	// Write the commit object
   117  	var parents []CommitID
   118  	oldHead, err := c.GetHeadCommit()
   119  	if opts.Amend {
   120  		parents, err = oldHead.Parents(c)
   121  		if err != nil {
   122  			return CommitID{}, err
   123  		}
   124  		author, err := oldHead.GetAuthor(c)
   125  		if err != nil {
   126  			return CommitID{}, err
   127  		}
   128  		if !opts.ResetAuthor {
   129  			// Back up the environment variables that commit uses to
   130  			// communicate with commit-tree, so that nothing external
   131  			// changes to the caller of this script.
   132  			defer func(oldauthorname, oldauthoremail, oldauthordate string) {
   133  				os.Setenv("GIT_AUTHOR_NAME", oldauthorname)
   134  				os.Setenv("GIT_AUTHOR_EMAIL", oldauthoremail)
   135  				os.Setenv("GIT_AUTHOR_DATE", oldauthordate)
   136  
   137  			}(
   138  				os.Getenv("GIT_AUTHOR_NAME"),
   139  				os.Getenv("GIT_AUTHOR_EMAIL"),
   140  				os.Getenv("GIT_AUTHOR_DATE"),
   141  			)
   142  			os.Setenv("GIT_AUTHOR_NAME", author.Name)
   143  			os.Setenv("GIT_AUTHOR_EMAIL", author.Email)
   144  			date, err := oldHead.GetDate(c)
   145  			if err != nil {
   146  				return CommitID{}, err
   147  			}
   148  			os.Setenv("GIT_AUTHOR_DATE", date.Format("Mon, 02 Jan 2006 15:04:05 -0700"))
   149  		}
   150  		goto skipemptycheck
   151  	} else if err == nil || err == DetachedHead {
   152  		parents = append(parents, oldHead)
   153  	}
   154  
   155  	if !opts.AllowEmpty {
   156  		if oldtree, err := oldHead.TreeID(c); err == nil {
   157  			if oldtree == treeid {
   158  				return CommitID{}, fmt.Errorf("No changes staged for commit.")
   159  			}
   160  		}
   161  	}
   162  skipemptycheck:
   163  	cleanMessage, err := message.Cleanup(opts.CleanupMode, !opts.NoEdit)
   164  	if err != nil {
   165  		return CommitID{}, err
   166  	}
   167  	var noConfig error
   168  	cid, err := CommitTree(c, CommitTreeOptions{}, TreeID(treeid), parents, cleanMessage)
   169  	switch err {
   170  	case nil:
   171  		// Nothing
   172  	case NoGlobalConfig:
   173  		noConfig = err
   174  	default:
   175  		return CommitID{}, err
   176  	}
   177  
   178  	// Update the reference
   179  	var refmsg string
   180  	if len(cleanMessage) < 50 {
   181  		refmsg = cleanMessage
   182  	} else {
   183  		refmsg = cleanMessage[:50]
   184  	}
   185  	refmsg = fmt.Sprintf("commit: %s (dgit)", refmsg)
   186  
   187  	if err := UpdateRef(c, UpdateRefOptions{OldValue: oldHead, CreateReflog: true}, "HEAD", cid, refmsg); err != nil {
   188  		return CommitID{}, err
   189  	}
   190  	return cid, noConfig
   191  }
   192  
   193  type CommitMessage string
   194  
   195  func (cm CommitMessage) String() string {
   196  	return string(cm)
   197  }
   198  func (cm CommitMessage) Cleanup(mode string, edit bool) (string, error) {
   199  	switch mode {
   200  	case "strip":
   201  		return cm.strip(), nil
   202  	case "whitespace":
   203  		return cm.whitespace(), nil
   204  	case "", "default":
   205  		if edit {
   206  			return cm.strip(), nil
   207  		}
   208  		return cm.whitespace(), nil
   209  	case "scissors":
   210  		return string(cm), fmt.Errorf("Unsupported cleanup mode")
   211  	default:
   212  		return string(cm), fmt.Errorf("Invalid cleanup mode")
   213  	}
   214  }
   215  
   216  func (cm CommitMessage) whitespace() string {
   217  	nonewlineRE, err := regexp.Compile("([\n]+)\n")
   218  	if err != nil {
   219  		panic(err)
   220  	}
   221  	replaced := nonewlineRE.ReplaceAllString(string(cm), "\n\n")
   222  	return strings.TrimSpace(replaced) + "\n"
   223  }
   224  
   225  func (cm CommitMessage) strip() string {
   226  	lines := strings.Split(cm.whitespace(), "\n")
   227  	filtered := make([]string, 0, len(lines))
   228  	for _, line := range lines {
   229  		if len(line) >= 1 && line[0] == '#' {
   230  			continue
   231  		}
   232  		filtered = append(filtered, line)
   233  	}
   234  	return strings.Join(filtered, "\n")
   235  }
   236  
   237  func (cm CommitMessage) Subject() string {
   238  	lines := strings.SplitN(cm.whitespace(), "\n", 1)
   239  	if len(lines) > 0 {
   240  		return strings.TrimSpace(lines[0])
   241  	}
   242  	return ""
   243  }