git-module

git clone git://git.lin.moe/fork/git-module.git

  1package git
  2
  3import (
  4	"bufio"
  5	"bytes"
  6	"context"
  7	"fmt"
  8	"io"
  9	"os"
 10	"path"
 11	"path/filepath"
 12	"strconv"
 13	"strings"
 14)
 15
 16// ObjectFormat represent the hash algorithm of a Git repository
 17type ObjectFormat string
 18
 19const (
 20	ObjectFormatSHA1   ObjectFormat = "sha1"
 21	ObjectFormatSHA256 ObjectFormat = "sha256"
 22)
 23
 24func (obf *ObjectFormat) String() string {
 25	return string(*obf)
 26}
 27
 28// Repository contains information of a Git repository.
 29type Repository struct {
 30	path string
 31
 32	cachedCommits      *objectCache
 33	cachedTags         *objectCache
 34	cachedTrees        *objectCache
 35	cachedObjectFomart ObjectFormat
 36}
 37
 38// Path returns the path of the repository.
 39func (r *Repository) Path() string {
 40	return r.path
 41}
 42
 43const LogFormatHashOnly = `format:%H`
 44
 45// parsePrettyFormatLogToList returns a list of commits parsed from given logs
 46// that are formatted in LogFormatHashOnly.
 47func (r *Repository) parsePrettyFormatLogToList(ctx context.Context, logs []byte) ([]*Commit, error) {
 48	if len(logs) == 0 {
 49		return []*Commit{}, nil
 50	}
 51
 52	var err error
 53	ids := bytes.Split(logs, []byte{'\n'})
 54	commits := make([]*Commit, len(ids))
 55	for i, id := range ids {
 56		commits[i], err = r.CatFileCommit(ctx, string(id))
 57		if err != nil {
 58			return nil, err
 59		}
 60	}
 61	return commits, nil
 62}
 63
 64// InitOptions contains optional arguments for initializing a repository.
 65//
 66// Docs: https://git-scm.com/docs/git-init
 67type InitOptions struct {
 68	// Indicates whether the repository should be initialized in bare format.
 69	Bare         bool
 70	ObjectFormat ObjectFormat
 71
 72	CommandOptions
 73}
 74
 75// Init initializes a new Git repository.
 76func Init(ctx context.Context, path string, opts ...InitOptions) error {
 77	var opt InitOptions
 78	if len(opts) > 0 {
 79		opt = opts[0]
 80	}
 81
 82	err := os.MkdirAll(path, os.ModePerm)
 83	if err != nil {
 84		return err
 85	}
 86
 87	args := []string{"init"}
 88	if opt.Bare {
 89		args = append(args, "--bare")
 90	}
 91	if opt.ObjectFormat != "" {
 92		args = append(args, "--object-format", opt.ObjectFormat.String())
 93	}
 94	args = append(args, "--end-of-options")
 95	_, err = exec(ctx, path, args, opt.Envs)
 96	return err
 97}
 98
 99// Open opens the repository at the given path. It returns an os.ErrNotExist if
100// the path does not exist.
101func Open(repoPath string) (*Repository, error) {
102	repoPath, err := filepath.Abs(repoPath)
103	if err != nil {
104		return nil, err
105	} else if !isDir(repoPath) {
106		return nil, os.ErrNotExist
107	}
108
109	return &Repository{
110		path:          repoPath,
111		cachedCommits: newObjectCache(),
112		cachedTags:    newObjectCache(),
113		cachedTrees:   newObjectCache(),
114	}, nil
115}
116
117// CloneOptions contains optional arguments for cloning a repository.
118//
119// Docs: https://git-scm.com/docs/git-clone
120type CloneOptions struct {
121	// Indicates whether the repository should be cloned as a mirror.
122	Mirror bool
123	// Indicates whether the repository should be cloned in bare format.
124	Bare bool
125	// Indicates whether to suppress the log output.
126	Quiet bool
127	// The branch to checkout for the working tree when Bare=false.
128	Branch string
129	// The number of revisions to clone.
130	Depth uint64
131	CommandOptions
132}
133
134// Clone clones the repository from remote URL to the destination.
135func Clone(ctx context.Context, url, dst string, opts ...CloneOptions) error {
136	var opt CloneOptions
137	if len(opts) > 0 {
138		opt = opts[0]
139	}
140
141	err := os.MkdirAll(path.Dir(dst), os.ModePerm)
142	if err != nil {
143		return err
144	}
145
146	args := []string{"clone"}
147	if opt.Mirror {
148		args = append(args, "--mirror")
149	}
150	if opt.Bare {
151		args = append(args, "--bare")
152	}
153	if opt.Quiet {
154		args = append(args, "--quiet")
155	}
156	if !opt.Bare && opt.Branch != "" {
157		args = append(args, "-b", opt.Branch)
158	}
159	if opt.Depth > 0 {
160		args = append(args, "--depth", strconv.FormatUint(opt.Depth, 10))
161	}
162
163	args = append(args, "--end-of-options", url, dst)
164	_, err = exec(ctx, "", args, opt.Envs)
165	return err
166}
167
168// FetchOptions contains optional arguments for fetching repository updates.
169//
170// Docs: https://git-scm.com/docs/git-fetch
171type FetchOptions struct {
172	// Indicates whether to prune during fetching.
173	Prune bool
174	CommandOptions
175}
176
177// Fetch fetches updates for the repository.
178func (r *Repository) Fetch(ctx context.Context, opts ...FetchOptions) error {
179	var opt FetchOptions
180	if len(opts) > 0 {
181		opt = opts[0]
182	}
183
184	args := []string{"fetch"}
185	if opt.Prune {
186		args = append(args, "--prune")
187	}
188	args = append(args, "--end-of-options")
189
190	_, err := exec(ctx, r.path, args, opt.Envs)
191	return err
192}
193
194// PullOptions contains optional arguments for pulling repository updates.
195//
196// Docs: https://git-scm.com/docs/git-pull
197type PullOptions struct {
198	// Indicates whether to rebased during pulling.
199	Rebase bool
200	// Indicates whether to pull from all remotes.
201	All bool
202	// The remote to pull updates from when All=false.
203	Remote string
204	// The branch to pull updates from when All=false and Remote is supplied.
205	Branch string
206	CommandOptions
207}
208
209// Pull pulls updates for the repository.
210func (r *Repository) Pull(ctx context.Context, opts ...PullOptions) error {
211	var opt PullOptions
212	if len(opts) > 0 {
213		opt = opts[0]
214	}
215
216	args := []string{"pull"}
217	if opt.Rebase {
218		args = append(args, "--rebase")
219	}
220	if opt.All {
221		args = append(args, "--all")
222	}
223	args = append(args, "--end-of-options")
224	if !opt.All && opt.Remote != "" {
225		args = append(args, opt.Remote)
226		if opt.Branch != "" {
227			args = append(args, opt.Branch)
228		}
229	}
230
231	_, err := exec(ctx, r.path, args, opt.Envs)
232	return err
233}
234
235// PushOptions contains optional arguments for pushing repository changes.
236//
237// Docs: https://git-scm.com/docs/git-push
238type PushOptions struct {
239	// Indicates whether to set upstream tracking for the branch.
240	SetUpstream bool
241	CommandOptions
242}
243
244// Push pushes local changes to given remote and branch for the repository.
245func (r *Repository) Push(ctx context.Context, remote, branch string, opts ...PushOptions) error {
246	var opt PushOptions
247	if len(opts) > 0 {
248		opt = opts[0]
249	}
250
251	args := []string{"push"}
252	if opt.SetUpstream {
253		args = append(args, "-u")
254	}
255	args = append(args, "--end-of-options", remote, branch)
256	_, err := exec(ctx, r.path, args, opt.Envs)
257	return err
258}
259
260// CheckoutOptions contains optional arguments for checking out to a branch.
261//
262// Docs: https://git-scm.com/docs/git-checkout
263type CheckoutOptions struct {
264	// The base branch if checks out to a new branch.
265	BaseBranch string
266	CommandOptions
267}
268
269// Checkout checks out to given branch for the repository.
270func (r *Repository) Checkout(ctx context.Context, branch string, opts ...CheckoutOptions) error {
271	var opt CheckoutOptions
272	if len(opts) > 0 {
273		opt = opts[0]
274	}
275
276	args := []string{"checkout"}
277	if opt.BaseBranch != "" {
278		args = append(args, "-b", branch, "--end-of-options", opt.BaseBranch)
279	} else {
280		args = append(args, "--end-of-options", branch)
281	}
282
283	_, err := exec(ctx, r.path, args, opt.Envs)
284	return err
285}
286
287// ResetOptions contains optional arguments for resetting a branch.
288//
289// Docs: https://git-scm.com/docs/git-reset
290type ResetOptions struct {
291	// Indicates whether to perform a hard reset.
292	Hard bool
293	CommandOptions
294}
295
296// Reset resets working tree to given revision for the repository.
297func (r *Repository) Reset(ctx context.Context, rev string, opts ...ResetOptions) error {
298	var opt ResetOptions
299	if len(opts) > 0 {
300		opt = opts[0]
301	}
302
303	args := []string{"reset"}
304	if opt.Hard {
305		args = append(args, "--hard")
306	}
307	args = append(args, "--end-of-options", rev)
308
309	_, err := exec(ctx, r.path, args, opt.Envs)
310	return err
311}
312
313// MoveOptions contains optional arguments for moving a file, a directory, or a
314// symlink.
315//
316// Docs: https://git-scm.com/docs/git-mv
317type MoveOptions struct {
318	CommandOptions
319}
320
321// Move moves a file, a directory, or a symlink file or directory from source to
322// destination for the repository.
323func (r *Repository) Move(ctx context.Context, src, dst string, opts ...MoveOptions) error {
324	var opt MoveOptions
325	if len(opts) > 0 {
326		opt = opts[0]
327	}
328
329	args := []string{"mv", "--end-of-options", src, dst}
330	_, err := exec(ctx, r.path, args, opt.Envs)
331	return err
332}
333
334// AddOptions contains optional arguments for adding local changes.
335//
336// Docs: https://git-scm.com/docs/git-add
337type AddOptions struct {
338	// Indicates whether to add all changes to index.
339	All bool
340	// The specific pathspecs to be added to index.
341	Pathspecs []string
342	CommandOptions
343}
344
345// Add adds local changes to index for the repository.
346func (r *Repository) Add(ctx context.Context, opts ...AddOptions) error {
347	var opt AddOptions
348	if len(opts) > 0 {
349		opt = opts[0]
350	}
351
352	args := []string{"add"}
353	if opt.All {
354		args = append(args, "--all")
355	}
356	if len(opt.Pathspecs) > 0 {
357		args = append(args, "--")
358		args = append(args, opt.Pathspecs...)
359	}
360	_, err := exec(ctx, r.path, args, opt.Envs)
361	return err
362}
363
364// CommitOptions contains optional arguments to commit changes.
365//
366// Docs: https://git-scm.com/docs/git-commit
367type CommitOptions struct {
368	// Author is the author of the changes if that's not the same as committer.
369	Author *Signature
370	CommandOptions
371}
372
373// Commit commits local changes with given author, committer and message for the
374// repository.
375func (r *Repository) Commit(ctx context.Context, committer *Signature, message string, opts ...CommitOptions) error {
376	var opt CommitOptions
377	if len(opts) > 0 {
378		opt = opts[0]
379	}
380
381	envs := committerEnvs(committer)
382	envs = append(envs, opt.Envs...)
383
384	if opt.Author == nil {
385		opt.Author = committer
386	}
387
388	args := []string{"commit"}
389	args = append(args, fmt.Sprintf("--author=%s <%s>", opt.Author.Name, opt.Author.Email))
390	args = append(args, "-m", message)
391	args = append(args, "--end-of-options")
392
393	_, err := exec(ctx, r.path, args, envs)
394	// No stderr but exit status 1 means nothing to commit.
395	if isExitStatus(err, 1) {
396		return nil
397	}
398	return err
399}
400
401// NameStatus contains name status of a commit.
402type NameStatus struct {
403	Added    []string
404	Removed  []string
405	Modified []string
406}
407
408// ShowNameStatusOptions contains optional arguments for showing name status.
409//
410// Docs: https://git-scm.com/docs/git-show#Documentation/git-show.txt---name-status
411type ShowNameStatusOptions struct {
412	CommandOptions
413}
414
415// ShowNameStatus returns name status of given revision of the repository.
416func (r *Repository) ShowNameStatus(ctx context.Context, rev string, opts ...ShowNameStatusOptions) (*NameStatus, error) {
417	var opt ShowNameStatusOptions
418	if len(opts) > 0 {
419		opt = opts[0]
420	}
421
422	fileStatus := &NameStatus{}
423	stdout, w := io.Pipe()
424	done := make(chan struct{})
425	go func() {
426		scanner := bufio.NewScanner(stdout)
427		for scanner.Scan() {
428			fields := strings.Fields(scanner.Text())
429			if len(fields) < 2 {
430				continue
431			}
432
433			switch fields[0][0] {
434			case 'A':
435				fileStatus.Added = append(fileStatus.Added, fields[1])
436			case 'D':
437				fileStatus.Removed = append(fileStatus.Removed, fields[1])
438			case 'M':
439				fileStatus.Modified = append(fileStatus.Modified, fields[1])
440			}
441		}
442		done <- struct{}{}
443	}()
444
445	args := []string{"show", "--name-status", "--pretty=format:''", "--end-of-options", rev}
446
447	err := pipe(ctx, r.path, args, opt.Envs, w)
448	_ = w.Close() // Close writer to exit parsing goroutine
449	if err != nil {
450		return nil, err
451	}
452
453	<-done
454	return fileStatus, nil
455}
456
457// RevParseOptions contains optional arguments for parsing revision.
458//
459// Docs: https://git-scm.com/docs/git-rev-parse
460type RevParseOptions struct {
461	CommandOptions
462}
463
464// RevParse returns full length (40 or 64) commit ID by given revision in the
465// repository.
466func (r *Repository) RevParse(ctx context.Context, rev string, opts ...RevParseOptions) (string, error) {
467	var opt RevParseOptions
468	if len(opts) > 0 {
469		opt = opts[0]
470	}
471
472	args := []string{"rev-parse", rev}
473
474	commitID, err := exec(ctx, r.path, args, opt.Envs)
475	if err != nil {
476		if isExitStatus(err, 128) {
477			return "", ErrRevisionNotExist
478		}
479		return "", err
480	}
481	return strings.TrimSpace(string(commitID)), nil
482}
483
484// CountObject contains disk usage report of a repository.
485type CountObject struct {
486	Count         int64
487	Size          int64
488	InPack        int64
489	Packs         int64
490	SizePack      int64
491	PrunePackable int64
492	Garbage       int64
493	SizeGarbage   int64
494}
495
496// CountObjectsOptions contains optional arguments for counting objects.
497//
498// Docs: https://git-scm.com/docs/git-count-objects
499type CountObjectsOptions struct {
500	CommandOptions
501}
502
503// CountObjects returns disk usage report of the repository.
504func (r *Repository) CountObjects(ctx context.Context, opts ...CountObjectsOptions) (*CountObject, error) {
505	var opt CountObjectsOptions
506	if len(opts) > 0 {
507		opt = opts[0]
508	}
509
510	args := []string{"count-objects", "-v", "--end-of-options"}
511
512	stdout, err := exec(ctx, r.path, args, opt.Envs)
513	if err != nil {
514		return nil, err
515	}
516
517	toInt64 := func(b []byte) int64 {
518		i, _ := strconv.ParseInt(string(b), 10, 64)
519		return i
520	}
521
522	countObject := new(CountObject)
523	for _, line := range bytes.Split(stdout, []byte("\n")) {
524		switch {
525		case bytes.HasPrefix(line, []byte("count: ")):
526			countObject.Count = toInt64(line[7:])
527		case bytes.HasPrefix(line, []byte("size: ")):
528			countObject.Size = toInt64(line[6:]) * 1024
529		case bytes.HasPrefix(line, []byte("in-pack: ")):
530			countObject.InPack = toInt64(line[9:])
531		case bytes.HasPrefix(line, []byte("packs: ")):
532			countObject.Packs = toInt64(line[7:])
533		case bytes.HasPrefix(line, []byte("size-pack: ")):
534			countObject.SizePack = toInt64(line[11:]) * 1024
535		case bytes.HasPrefix(line, []byte("prune-packable: ")):
536			countObject.PrunePackable = toInt64(line[16:])
537		case bytes.HasPrefix(line, []byte("garbage: ")):
538			countObject.Garbage = toInt64(line[9:])
539		case bytes.HasPrefix(line, []byte("size-garbage: ")):
540			countObject.SizeGarbage = toInt64(line[14:]) * 1024
541		}
542	}
543
544	return countObject, nil
545}
546
547// FsckOptions contains optional arguments for verifying the objects.
548//
549// Docs: https://git-scm.com/docs/git-fsck
550type FsckOptions struct {
551	CommandOptions
552}
553
554// Fsck verifies the connectivity and validity of the objects in the database
555// for the repository.
556func (r *Repository) Fsck(ctx context.Context, opts ...FsckOptions) error {
557	var opt FsckOptions
558	if len(opts) > 0 {
559		opt = opts[0]
560	}
561
562	args := []string{"fsck", "--end-of-options"}
563	_, err := exec(ctx, r.path, args, opt.Envs)
564	return err
565
566}
567
568// ObjectFomat returns hash algorithm (sha1 or sha256) of the repository
569func (r *Repository) ObjectFormat(ctx context.Context) (ObjectFormat, error) {
570	if r.cachedObjectFomart != "" {
571		return r.cachedObjectFomart, nil
572	}
573
574	id, err := exec(ctx, r.path, []string{"hash-object", "--stdin"}, nil)
575	if err != nil {
576		return "", fmt.Errorf("hash object: %v", err)
577	}
578	oid, err := NewIDFromString(strings.TrimSpace(string(id)))
579	if err != nil {
580		return "", fmt.Errorf("parse oid: %v", err)
581	}
582	switch oid.(type) {
583	case *SHA1:
584		r.cachedObjectFomart = ObjectFormatSHA1
585		return ObjectFormatSHA1, nil
586	case *SHA256:
587		r.cachedObjectFomart = ObjectFormatSHA256
588		return ObjectFormatSHA256, nil
589	default:
590		return "", fmt.Errorf("unknown object format")
591	}
592}