github.com/shved/got@v0.0.0-20230322140632-a4bfa1e99685/object/object.go (about)

     1  // Package object includes all the functions and types related to the Got objects and operations on them.
     2  package object
     3  
     4  import (
     5  	"compress/gzip"
     6  	"crypto/sha1"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"log"
    10  	"os"
    11  	"path"
    12  	"sort"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/shved/got/got"
    17  )
    18  
    19  type ObjectType int
    20  
    21  const (
    22  	Commit ObjectType = iota + 1
    23  	Tree
    24  	Blob
    25  )
    26  
    27  // Object is a struct representation of a repo object.
    28  type Object struct {
    29  	ObjType          ObjectType
    30  	Parent           *Object
    31  	Children         []*Object
    32  	Name             string
    33  	ParentPath       string
    34  	Path             string
    35  	ParentCommitHash string
    36  	CommitMessage    string
    37  	HashString       string
    38  	Timestamp        time.Time
    39  
    40  	sha          []byte
    41  	contentLines []string
    42  	gzipContent  string
    43  }
    44  
    45  // Show returns a string with object content.
    46  func Show(shaString string) string {
    47  	if exists(path.Join(got.CommitDirAbsPath(), shaString)) {
    48  		return objContent(path.Join(got.CommitDirAbsPath(), shaString))
    49  	}
    50  
    51  	if exists(path.Join(got.TreeDirAbsPath(), shaString)) {
    52  		return objContent(path.Join(got.TreeDirAbsPath(), shaString))
    53  	}
    54  
    55  	if exists(path.Join(got.TreeDirAbsPath(), shaString)) {
    56  		return objContent(path.Join(got.TreeDirAbsPath(), shaString))
    57  	}
    58  
    59  	log.Fatal(got.ErrObjDoesNotExist)
    60  	panic("never reach")
    61  }
    62  
    63  // objContent reads object archive and returns only its contentw without gzip headers.
    64  func objContent(p string) string {
    65  	res, _ := readArchive(p)
    66  	return string(res)
    67  }
    68  
    69  // RecRestoreFromObject recursively writes objects into files/folders making an object graph
    70  // persisted in a worktree.
    71  func (o *Object) RecRestoreFromObject(p string) {
    72  	switch o.ObjType {
    73  	case Commit:
    74  		for _, ch := range o.Children {
    75  			ch.RecRestoreFromObject(p)
    76  		}
    77  	case Tree:
    78  		treePath := path.Join(p, o.Name)
    79  		err := os.Mkdir(treePath, 0755)
    80  		if err != nil {
    81  			log.Fatal(err)
    82  		}
    83  		for _, ch := range o.Children {
    84  			ch.RecRestoreFromObject(treePath)
    85  		}
    86  	case Blob:
    87  		blobPath := path.Join(p, o.Name)
    88  		if err := ioutil.WriteFile(blobPath, []byte(o.gzipContent), 0644); err != nil {
    89  			log.Fatal(err)
    90  		}
    91  	}
    92  }
    93  
    94  // RecReadObject recursively reads objects archives and links them into an object graph.
    95  func RecReadObject(t ObjectType, hashString string, parentObj *Object) *Object {
    96  	switch t {
    97  	case Commit:
    98  		oPath := path.Join(t.storePath(), hashString)
    99  		res, header := readArchive(oPath)
   100  		commit := &Object{
   101  			ObjType:       Commit,
   102  			Name:          header.Name,
   103  			sha:           []byte(hashString),
   104  			HashString:    hashString,
   105  			Timestamp:     header.ModTime,
   106  			CommitMessage: header.Comment,
   107  		}
   108  		children := parseObjContent(string(res))
   109  		for _, child := range children {
   110  			if child.t != Commit {
   111  				commit.Children = append(commit.Children, RecReadObject(child.t, child.hashString, commit))
   112  			} else {
   113  				continue // skip parent commit entry in commit content
   114  			}
   115  		}
   116  		return commit
   117  	case Tree:
   118  		oPath := path.Join(t.storePath(), hashString)
   119  		res, header := readArchive(oPath)
   120  		tree := &Object{
   121  			ObjType:    Tree,
   122  			Name:       header.Name,
   123  			sha:        []byte(hashString),
   124  			HashString: hashString,
   125  			Parent:     parentObj,
   126  			Timestamp:  header.ModTime,
   127  		}
   128  		children := parseObjContent(string(res))
   129  		for _, child := range children {
   130  			tree.Children = append(tree.Children, RecReadObject(child.t, child.hashString, tree))
   131  		}
   132  		return tree
   133  	case Blob:
   134  		oPath := path.Join(t.storePath(), hashString)
   135  		res, header := readArchive(oPath)
   136  		blob := &Object{
   137  			ObjType:     Blob,
   138  			Name:        header.Name,
   139  			sha:         []byte(hashString),
   140  			HashString:  hashString,
   141  			Parent:      parentObj,
   142  			gzipContent: string(res),
   143  			Timestamp:   header.ModTime,
   144  		}
   145  		return blob
   146  	default:
   147  		log.Fatalf("RecReadObject(): %v", got.ErrInvalidObjType)
   148  	}
   149  	panic("never reach")
   150  }
   151  
   152  // objRepr is a local type to proceed object string representation for the further transformation intro and object.
   153  type objRepr struct {
   154  	t          ObjectType
   155  	hashString string
   156  	name       string
   157  }
   158  
   159  // parseObjContent takes object (commit or tree) contents and returns a slice of containing objects
   160  // in a special representation form.
   161  func parseObjContent(s string) []objRepr {
   162  	var objects []objRepr
   163  	lines := strings.Split(s, "\n")
   164  	for _, line := range lines {
   165  		objects = append(objects, parseObjString(line))
   166  	}
   167  	return objects
   168  }
   169  
   170  // parseObjString parses object string representation.
   171  func parseObjString(s string) objRepr {
   172  	entries := strings.Split(s, "\t")
   173  	var name string
   174  	if len(entries) > 3 {
   175  		name = entries[2]
   176  	}
   177  	return objRepr{t: strToObjType(entries[0]), hashString: entries[1], name: name}
   178  }
   179  
   180  // storePath returns objects path to write into depending on its type.
   181  func (t ObjectType) storePath() string {
   182  	switch t {
   183  	case Commit:
   184  		return got.CommitDirAbsPath()
   185  	case Tree:
   186  		return got.TreeDirAbsPath()
   187  	case Blob:
   188  		return got.BlobDirAbsPath()
   189  	default:
   190  		log.Fatalf("storePath(): %v", got.ErrInvalidObjType)
   191  	}
   192  	panic("never reach")
   193  }
   194  
   195  // LogEntry function returns a string representation of a commit for repo commit log.
   196  func (o *Object) LogEntry() string {
   197  	if o.ObjType != Commit {
   198  		log.Fatal(got.ErrWrongLogEntryType)
   199  	}
   200  
   201  	logEntry := strings.Join(
   202  		[]string{
   203  			o.Timestamp.UTC().Format(time.RFC3339),
   204  			o.HashString,
   205  			o.ParentCommitHash,
   206  			o.CommitMessage,
   207  		},
   208  		"\t",
   209  	)
   210  	return logEntry + "\n"
   211  }
   212  
   213  // RecCalcHashSum recursively calculates all objects sha1 in an object graph started from very far children
   214  // and puts it into the object struct fields sha and HashString.
   215  func (o *Object) RecCalcHashSum() {
   216  	switch o.ObjType {
   217  	case Commit:
   218  		for _, ch := range o.Children {
   219  			ch.RecCalcHashSum()
   220  			o.contentLines = append(o.contentLines, ch.buildContentLineForParent())
   221  		}
   222  		if o.ParentCommitHash != string(got.EmptyCommitRef) {
   223  			parentCommitLine := parentCommitShaContentLine(o.ParentCommitHash)
   224  			o.contentLines = append(o.contentLines, parentCommitLine)
   225  		}
   226  		sort.Strings(o.contentLines)
   227  		o.gzipContent = strings.Join(o.contentLines, "\n")
   228  		data := []byte(o.gzipContent)
   229  		o.writeShaSum(data)
   230  	case Tree:
   231  		for _, ch := range o.Children {
   232  			ch.RecCalcHashSum()
   233  			o.contentLines = append(o.contentLines, ch.buildContentLineForParent())
   234  		}
   235  		sort.Strings(o.contentLines)
   236  		o.gzipContent = strings.Join(o.contentLines, "\n")
   237  		data := []byte(o.gzipContent)
   238  		o.writeShaSum(data)
   239  	case Blob:
   240  		data, err := ioutil.ReadFile(o.Path)
   241  		if err != nil {
   242  			log.Fatal(err)
   243  		}
   244  		o.writeShaSum(data)
   245  	default:
   246  		log.Fatalf("RecCalcHashSum(): %v", got.ErrInvalidObjType)
   247  	}
   248  }
   249  
   250  // writeShaSum takes bytes data, calculates sha sum for it and writes sum and hash string into the object struct.
   251  func (o *Object) writeShaSum(data []byte) {
   252  	h := sha1.New()
   253  	h.Write(data)
   254  	o.sha = h.Sum(nil)
   255  	o.HashString = hashString(o.sha)
   256  }
   257  
   258  // buildContentLineForParent builds a string to put into parents (commit or tree) content to be archived.
   259  func (o *Object) buildContentLineForParent() string {
   260  	entries := []string{o.ObjType.toString(), o.HashString, o.Name}
   261  	return strings.Join(entries, "\t")
   262  }
   263  
   264  // parentCommitShaContentLine reads commit archive and builds content line for commit
   265  // pointing to parent commit.
   266  func parentCommitShaContentLine(parentHash string) string {
   267  	parentCommitPath := path.Join(got.CommitDirAbsPath(), parentHash)
   268  	fd, err := os.Open(parentCommitPath)
   269  	if err != nil {
   270  		log.Fatal(err)
   271  	}
   272  	unarchiver, _ := gzip.NewReader(fd)
   273  	defer fd.Close()
   274  	defer unarchiver.Close()
   275  	entries := []string{Commit.toString(), parentHash, unarchiver.Comment}
   276  	return strings.Join(entries, "\t")
   277  }
   278  
   279  // RecWriteObjects recursively writes archive for objects in a graph.
   280  func (o *Object) RecWriteObjects() {
   281  	if o.ObjType == Commit || o.ObjType == Tree {
   282  		for _, ch := range o.Children {
   283  			ch.RecWriteObjects()
   284  		}
   285  	}
   286  
   287  	o.write()
   288  }
   289  
   290  // write function writes archives for objects.
   291  func (o *Object) write() {
   292  	switch o.ObjType {
   293  	case Commit:
   294  		path := path.Join(got.CommitDirAbsPath(), o.HashString)
   295  		writeArchive(path, o.Name, []byte(o.gzipContent), time.Now(), o.CommitMessage)
   296  		got.UpdateHead(o.HashString)
   297  	case Tree:
   298  		path := path.Join(got.TreeDirAbsPath(), o.HashString)
   299  		if exists(path) {
   300  			break
   301  		}
   302  		writeArchive(path, o.Name, []byte(o.gzipContent), time.Now(), "")
   303  	case Blob:
   304  		path := path.Join(got.BlobDirAbsPath(), o.HashString)
   305  		if exists(path) {
   306  			break
   307  		}
   308  		data, err := ioutil.ReadFile(o.Path)
   309  		if err != nil {
   310  			log.Fatal(err)
   311  		}
   312  		writeArchive(path, o.Name, data, time.Now(), "")
   313  	default:
   314  		log.Fatalf("write(): %v", got.ErrInvalidObjType)
   315  	}
   316  }
   317  
   318  // writeArchive implements archive writing for object data.
   319  func writeArchive(p string, name string, data []byte, t time.Time, commitMessage string) {
   320  	fd, _ := os.Create(p)
   321  	archiver := gzip.NewWriter(fd)
   322  	defer fd.Close()
   323  	defer archiver.Close()
   324  	archiver.Name = name
   325  	archiver.ModTime = t
   326  	if commitMessage != "" {
   327  		archiver.Comment = commitMessage
   328  	}
   329  	archiver.Write(data)
   330  }
   331  
   332  // readArchive reads a gzip archive and returns its content and header struct.
   333  func readArchive(p string) ([]byte, gzip.Header) {
   334  	fd, err := os.Open(p)
   335  	unarchiver, err := gzip.NewReader(fd)
   336  	if err != nil {
   337  		log.Fatalf("reading archive %s: %v", p, err)
   338  	}
   339  	defer fd.Close()
   340  	defer unarchiver.Close()
   341  	res, err := ioutil.ReadAll(unarchiver)
   342  	if err != nil {
   343  		log.Fatalf("reading archive %s: %v", p, err)
   344  	}
   345  	return res, unarchiver.Header
   346  }
   347  
   348  // hashString converts hashSum into string representation.
   349  func hashString(hashSum []byte) string {
   350  	return fmt.Sprintf("%x", hashSum)
   351  }
   352  
   353  // toString converts object type into its string representation.
   354  func (t ObjectType) toString() string {
   355  	switch t {
   356  	case Commit:
   357  		return "commit"
   358  	case Tree:
   359  		return "tree"
   360  	case Blob:
   361  		return "blob"
   362  	default:
   363  		log.Fatalf("toString(): %v", got.ErrInvalidObjType)
   364  		panic("never reach")
   365  	}
   366  }
   367  
   368  // strToObjType converts string into respective object type.
   369  func strToObjType(s string) ObjectType {
   370  	switch s {
   371  	case "commit":
   372  		return Commit
   373  	case "tree":
   374  		return Tree
   375  	case "blob":
   376  		return Blob
   377  	default:
   378  		log.Fatalf("strToObjType(): %v (%v)", got.ErrInvalidObjType, s)
   379  		panic("never reach")
   380  	}
   381  }
   382  
   383  // exists tests wheather a file exists.
   384  func exists(path string) bool {
   385  	if _, err := os.Stat(path); err != nil {
   386  		if os.IsNotExist(err) {
   387  			return false
   388  		}
   389  	}
   390  	return true
   391  }