├── go.mod ├── assets ├── cover.png └── tech-talk.jpg ├── .gitignore ├── git ├── object.go ├── blob.go ├── tree.go ├── commit.go ├── repo_test.go └── repo.go ├── .github └── workflows │ ├── release.yml │ └── build.yml ├── main.go ├── LICENSE ├── util └── util.go ├── cmd ├── init.go ├── mktree.go ├── revparse.go ├── updateref.go ├── hashobject.go ├── command.go ├── committree.go ├── showref.go ├── lstree.go ├── checkout.go ├── log.go └── catfile.go └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ssrathi/gogit 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssrathi/gogit/HEAD/assets/cover.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac settings folder 2 | .DS_Store 3 | 4 | # Executables 5 | gogit 6 | 7 | -------------------------------------------------------------------------------- /assets/tech-talk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssrathi/gogit/HEAD/assets/tech-talk.jpg -------------------------------------------------------------------------------- /git/object.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | // ObjIntf is a common interface shared by all type of git objects. 4 | type ObjIntf interface { 5 | Print() string 6 | Type() string 7 | DataSize() int 8 | } 9 | 10 | // Object is a struct holding the raw data for any git object type. 11 | // ObjType can be one of "commit", "blob", "tree" or "tag". 12 | type Object struct { 13 | ObjType string 14 | ObjData []byte 15 | } 16 | 17 | // NewObject returns a new git object of given type and with given data bytes. 18 | func NewObject(objType string, data []byte) *Object { 19 | return &Object{ 20 | ObjType: objType, 21 | ObjData: data, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | create: 5 | tags: 6 | - v* 7 | 8 | workflow_dispatch: 9 | 10 | jobs: 11 | goreleaser: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | name: Checkout 16 | uses: actions/checkout@v2 17 | - 18 | name: Unshallow 19 | run: git fetch --prune --unshallow 20 | - 21 | name: Set up Go 22 | uses: actions/setup-go@v2 23 | with: 24 | go-version: 1.18 25 | - 26 | name: Run GoReleaser 27 | uses: goreleaser/goreleaser-action@v2 28 | with: 29 | version: latest 30 | args: release --rm-dist 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Implementation of git internal commands in Go language. 3 | 4 | This project is part of a learning exercise to implement a subset of "git" 5 | commands. It can be used to create and maintain git objects, such as blobs, 6 | trees, commits, branches and tags. 7 | 8 | Code Organization 9 | 10 | - "git": internal git objects and related APIs. 11 | - "cmd": Command line parsing and execution. 12 | - "util": Miscellaneous utility APIs. 13 | */ 14 | package main 15 | 16 | import ( 17 | "io/ioutil" 18 | "log" 19 | "os" 20 | 21 | "github.com/ssrathi/gogit/cmd" 22 | ) 23 | 24 | func init() { 25 | // Enable logging only if a specific ENV variable is set. 26 | if os.Getenv("GOGIT_DBG") != "1" { 27 | log.SetOutput(ioutil.Discard) 28 | log.SetFlags(0) 29 | } else { 30 | // Print file and line numbers in each log line. 31 | log.SetFlags(log.LstdFlags | log.Lshortfile) 32 | } 33 | } 34 | 35 | func main() { 36 | cmd.Execute() 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | if [ -f Gopkg.toml ]; then 29 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 30 | dep ensure 31 | fi 32 | 33 | - name: Build 34 | run: go build -v . 35 | 36 | - name: Test 37 | run: go test -v ./... 38 | 39 | - name: Lint 40 | run: | 41 | export PATH=$PATH:$(go env GOPATH)/bin 42 | go install golang.org/x/lint/golint@latest 43 | golint -set_exit_status ./... 44 | 45 | - name: Vet 46 | run: go vet ./... 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Shyamsunder Rathi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | // Package util implements miscellaneous utility APIs. 2 | package util 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | ) 12 | 13 | // Check is a helper function to exit on irrecoverable error. 14 | func Check(err error) { 15 | if err != nil { 16 | _, file, line, _ := runtime.Caller(1) 17 | log.Printf("%s:%d %s", filepath.Base(file), line, err) 18 | fmt.Println(err) 19 | os.Exit(1) 20 | } 21 | } 22 | 23 | // IsDirEmpty checks if given directory is empty or not. 24 | func IsDirEmpty(path string) (bool, error) { 25 | fd, err := os.Open(path) 26 | if err != nil { 27 | return false, err 28 | } 29 | defer fd.Close() 30 | 31 | // Read one item from the path. 32 | _, err = fd.Readdir(1) 33 | if err == io.EOF { 34 | return true, nil 35 | } 36 | 37 | return false, nil 38 | } 39 | 40 | // IsPathPresent checks if given path (dir or file) exists. 41 | func IsPathPresent(path string) bool { 42 | if _, err := os.Stat(path); os.IsNotExist(err) { 43 | return false 44 | } 45 | 46 | return true 47 | } 48 | 49 | // IsPathDir checks if given path is a directory (not a file). 50 | func IsPathDir(path string) (bool, error) { 51 | fInfo, err := os.Stat(path) 52 | if err != nil { 53 | return false, err 54 | } 55 | 56 | return fInfo.IsDir(), nil 57 | } 58 | -------------------------------------------------------------------------------- /git/blob.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | ) 7 | 8 | // Blob is a git object to represent the data of a single file. 9 | type Blob struct { 10 | Repository *Repo 11 | *Object 12 | } 13 | 14 | // NewBlob creates a new blob object by parsing a Object. 15 | func NewBlob(repo *Repo, obj *Object) (*Blob, error) { 16 | if obj.ObjType != "blob" { 17 | return nil, fmt.Errorf("Malformed object: bad type %s", obj.ObjType) 18 | } 19 | 20 | blob := Blob{ 21 | Repository: repo, 22 | Object: obj, 23 | } 24 | return &blob, nil 25 | } 26 | 27 | // NewBlobFromFile creates a new blob object by reading data from a file. 28 | func NewBlobFromFile(repo *Repo, file string) (*Blob, error) { 29 | data, err := ioutil.ReadFile(file) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | blob := Blob{ 35 | Repository: repo, 36 | Object: NewObject("blob", data), 37 | } 38 | return &blob, nil 39 | } 40 | 41 | // Print returns a string representation of a blob object. 42 | func (blob *Blob) Print() string { 43 | return string(blob.ObjData) 44 | } 45 | 46 | // Type returns the type string of a blob object. 47 | func (blob *Blob) Type() string { 48 | return "blob" 49 | } 50 | 51 | // DataSize returns the size of the data of a blob object. 52 | func (blob *Blob) DataSize() int { 53 | return len(blob.ObjData) 54 | } 55 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/ssrathi/gogit/git" 8 | "github.com/ssrathi/gogit/util" 9 | ) 10 | 11 | // InitCommand lists the components of "init" comamnd. 12 | type InitCommand struct { 13 | fs *flag.FlagSet 14 | path string 15 | } 16 | 17 | // NewInitCommand creates a new command object. 18 | func NewInitCommand() *InitCommand { 19 | cmd := &InitCommand{ 20 | fs: flag.NewFlagSet("init", flag.ExitOnError), 21 | } 22 | 23 | cmd.fs.StringVar(&cmd.path, "path", ".", "Path to create the repository") 24 | return cmd 25 | } 26 | 27 | // Name gives the name of the command. 28 | func (cmd *InitCommand) Name() string { 29 | return cmd.fs.Name() 30 | } 31 | 32 | // Description gives the description of the command. 33 | func (cmd *InitCommand) Description() string { 34 | return "Create an empty Git repository" 35 | } 36 | 37 | // Init initializes and validates the given command. 38 | func (cmd *InitCommand) Init(args []string) error { 39 | cmd.fs.Usage = cmd.Usage 40 | return cmd.fs.Parse(args) 41 | } 42 | 43 | // Usage prints the usage string for the end user. 44 | func (cmd *InitCommand) Usage() { 45 | fmt.Printf("%s - %s\n", cmd.Name(), cmd.Description()) 46 | fmt.Printf("usage: %s []\n", cmd.Name()) 47 | cmd.fs.PrintDefaults() 48 | } 49 | 50 | // Execute runs the given command till completion. 51 | func (cmd *InitCommand) Execute() { 52 | repo, err := git.NewRepo(cmd.path) 53 | util.Check(err) 54 | 55 | fmt.Printf("Initialized empty Git repository in %s/\n", repo.GitDir) 56 | } 57 | -------------------------------------------------------------------------------- /cmd/mktree.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/ssrathi/gogit/git" 10 | "github.com/ssrathi/gogit/util" 11 | ) 12 | 13 | // MkTreeCommand lists the components of "mktree" comamnd. 14 | type MkTreeCommand struct { 15 | fs *flag.FlagSet 16 | } 17 | 18 | // NewMkTreeCommand creates a new command object. 19 | func NewMkTreeCommand() *MkTreeCommand { 20 | cmd := &MkTreeCommand{ 21 | fs: flag.NewFlagSet("mktree", flag.ExitOnError), 22 | } 23 | return cmd 24 | } 25 | 26 | // Name gives the name of the command. 27 | func (cmd *MkTreeCommand) Name() string { 28 | return cmd.fs.Name() 29 | } 30 | 31 | // Description gives the description of the command. 32 | func (cmd *MkTreeCommand) Description() string { 33 | return "Build a tree-object from ls-tree formatted text" 34 | } 35 | 36 | // Init initializes and validates the given command. 37 | func (cmd *MkTreeCommand) Init(args []string) error { 38 | cmd.fs.Usage = cmd.Usage 39 | return cmd.fs.Parse(args) 40 | } 41 | 42 | // Usage prints the usage string for the end user. 43 | func (cmd *MkTreeCommand) Usage() { 44 | fmt.Printf("%s - %s\n", cmd.Name(), cmd.Description()) 45 | fmt.Printf("usage: %s\n", cmd.Name()) 46 | cmd.fs.PrintDefaults() 47 | } 48 | 49 | // Execute runs the given command till completion. 50 | func (cmd *MkTreeCommand) Execute() { 51 | repo, err := git.GetRepo(".") 52 | util.Check(err) 53 | 54 | input, err := ioutil.ReadAll(os.Stdin) 55 | util.Check(err) 56 | 57 | tree, err := git.NewTreeFromInput(repo, string(input)) 58 | util.Check(err) 59 | 60 | // Write the tree now. 61 | hash, err := repo.ObjectWrite(tree.Object, true) 62 | util.Check(err) 63 | 64 | fmt.Println(hash) 65 | } 66 | -------------------------------------------------------------------------------- /cmd/revparse.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/ssrathi/gogit/git" 9 | "github.com/ssrathi/gogit/util" 10 | ) 11 | 12 | // RevParseCommand lists the components of "rev-parse" comamnd. 13 | type RevParseCommand struct { 14 | fs *flag.FlagSet 15 | revision string 16 | } 17 | 18 | // NewRevParseCommand creates a new command object. 19 | func NewRevParseCommand() *RevParseCommand { 20 | cmd := &RevParseCommand{ 21 | fs: flag.NewFlagSet("rev-parse", flag.ExitOnError), 22 | } 23 | return cmd 24 | } 25 | 26 | // Name gives the name of the command. 27 | func (cmd *RevParseCommand) Name() string { 28 | return cmd.fs.Name() 29 | } 30 | 31 | // Init initializes and validates the given command. 32 | func (cmd *RevParseCommand) Init(args []string) error { 33 | cmd.fs.Usage = cmd.Usage 34 | if err := cmd.fs.Parse(args); err != nil { 35 | return err 36 | } 37 | 38 | if cmd.fs.NArg() < 1 { 39 | return errors.New("error: Missing argument") 40 | } 41 | 42 | cmd.revision = cmd.fs.Arg(0) 43 | return nil 44 | } 45 | 46 | // Description gives the description of the command. 47 | func (cmd *RevParseCommand) Description() string { 48 | return "Parse a given git identifier" 49 | } 50 | 51 | // Usage prints the usage string for the end user. 52 | func (cmd *RevParseCommand) Usage() { 53 | fmt.Printf("%s - %s\n", cmd.Name(), cmd.Description()) 54 | fmt.Printf("usage: %s \n", cmd.Name()) 55 | cmd.fs.PrintDefaults() 56 | } 57 | 58 | // Execute runs the given command till completion. 59 | func (cmd *RevParseCommand) Execute() { 60 | repo, err := git.GetRepo(".") 61 | util.Check(err) 62 | 63 | // Resolve the given name to a full hash. 64 | objHash, err := repo.UniqueNameResolve(cmd.revision) 65 | util.Check(err) 66 | 67 | fmt.Println(objHash) 68 | } 69 | -------------------------------------------------------------------------------- /cmd/updateref.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/ssrathi/gogit/git" 9 | "github.com/ssrathi/gogit/util" 10 | ) 11 | 12 | // UpdateRefCommand lists the components of "update-ref" comamnd. 13 | type UpdateRefCommand struct { 14 | fs *flag.FlagSet 15 | reference string 16 | newValue string 17 | } 18 | 19 | // NewUpdateRefCommand creates a new command object. 20 | func NewUpdateRefCommand() *UpdateRefCommand { 21 | cmd := &UpdateRefCommand{ 22 | fs: flag.NewFlagSet("update-ref", flag.ExitOnError), 23 | } 24 | 25 | return cmd 26 | } 27 | 28 | // Name gives the name of the command. 29 | func (cmd *UpdateRefCommand) Name() string { 30 | return cmd.fs.Name() 31 | } 32 | 33 | // Init initializes and validates the given command. 34 | func (cmd *UpdateRefCommand) Init(args []string) error { 35 | cmd.fs.Usage = cmd.Usage 36 | if err := cmd.fs.Parse(args); err != nil { 37 | return err 38 | } 39 | 40 | if cmd.fs.NArg() < 2 { 41 | return errors.New("error: and/or not provided") 42 | } 43 | 44 | cmd.reference = cmd.fs.Arg(0) 45 | cmd.newValue = cmd.fs.Arg(1) 46 | return nil 47 | } 48 | 49 | // Description gives the description of the command. 50 | func (cmd *UpdateRefCommand) Description() string { 51 | return "Update the object name stored in a ref safely" 52 | } 53 | 54 | // Usage prints the usage string for the end user. 55 | func (cmd *UpdateRefCommand) Usage() { 56 | fmt.Printf("%s - %s\n", cmd.Name(), cmd.Description()) 57 | fmt.Printf("usage: %s [] \n", cmd.Name()) 58 | cmd.fs.PrintDefaults() 59 | } 60 | 61 | // Execute runs the given command till completion. 62 | func (cmd *UpdateRefCommand) Execute() { 63 | repo, err := git.GetRepo(".") 64 | util.Check(err) 65 | 66 | err = repo.UpdateRef(cmd.reference, cmd.newValue) 67 | util.Check(err) 68 | } 69 | -------------------------------------------------------------------------------- /cmd/hashobject.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/ssrathi/gogit/git" 9 | "github.com/ssrathi/gogit/util" 10 | ) 11 | 12 | // HashObjectCommand lists the components of "cat-file" comamnd. 13 | type HashObjectCommand struct { 14 | fs *flag.FlagSet 15 | write bool 16 | file string 17 | } 18 | 19 | // NewHashObjectCommand creates a new command object. 20 | func NewHashObjectCommand() *HashObjectCommand { 21 | cmd := &HashObjectCommand{ 22 | fs: flag.NewFlagSet("hash-object", flag.ExitOnError), 23 | } 24 | 25 | cmd.fs.BoolVar(&cmd.write, "w", false, 26 | "Actually write the object into the object database.") 27 | 28 | return cmd 29 | } 30 | 31 | // Name gives the name of the command. 32 | func (cmd *HashObjectCommand) Name() string { 33 | return cmd.fs.Name() 34 | } 35 | 36 | // Description gives the description of the command. 37 | func (cmd *HashObjectCommand) Description() string { 38 | return "Compute object ID and optionally creates a blob from a file" 39 | } 40 | 41 | // Init initializes and validates the given command. 42 | func (cmd *HashObjectCommand) Init(args []string) error { 43 | cmd.fs.Usage = cmd.Usage 44 | if err := cmd.fs.Parse(args); err != nil { 45 | return err 46 | } 47 | 48 | if cmd.fs.NArg() < 1 { 49 | return errors.New("Error: Missing argument") 50 | } 51 | 52 | cmd.file = cmd.fs.Arg(0) 53 | return nil 54 | } 55 | 56 | // Usage prints the usage string for the end user. 57 | func (cmd *HashObjectCommand) Usage() { 58 | fmt.Printf("%s - %s\n", cmd.Name(), cmd.Description()) 59 | fmt.Printf("usage: %s [] \n", cmd.Name()) 60 | cmd.fs.PrintDefaults() 61 | } 62 | 63 | // Execute runs the given command till completion. 64 | func (cmd *HashObjectCommand) Execute() { 65 | repo, err := git.GetRepo(".") 66 | util.Check(err) 67 | 68 | blob, err := git.NewBlobFromFile(repo, cmd.file) 69 | util.Check(err) 70 | 71 | sha1, err := repo.ObjectWrite(blob.Object, cmd.write) 72 | util.Check(err) 73 | 74 | fmt.Println(sha1) 75 | } 76 | -------------------------------------------------------------------------------- /cmd/command.go: -------------------------------------------------------------------------------- 1 | // Package cmd is the entry point for gogit command line parsing. 2 | package cmd 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | ) 10 | 11 | // Subcommand is an interface that all subcommands must implement. 12 | type Subcommand interface { 13 | Init([]string) error 14 | Name() string 15 | Description() string 16 | Usage() 17 | Execute() 18 | } 19 | 20 | // Execute parses CLI arguments and executes the given subcommand. 21 | func Execute() { 22 | progName := os.Args[0] 23 | args := os.Args[1:] 24 | 25 | // Create an object for each subcommand. 26 | cmds := []Subcommand{ 27 | NewInitCommand(), 28 | NewHashObjectCommand(), 29 | NewCatFileCommand(), 30 | NewLsTreeCommand(), 31 | NewMkTreeCommand(), 32 | NewCheckoutCommand(), 33 | NewCommitTreeCommand(), 34 | NewLogCommand(), 35 | NewShowRefCommand(), 36 | NewUpdateRefCommand(), 37 | NewRevParseCommand(), 38 | } 39 | 40 | // Prepare the global usage message. 41 | flag.Usage = func() { 42 | fmt.Printf("gogit - the stupid content tracker\n\n") 43 | fmt.Printf("usage: %s []\n", progName) 44 | fmt.Println("Valid commands:") 45 | 46 | for _, cmd := range cmds { 47 | fmt.Printf(" %-14s %s\n", cmd.Name(), cmd.Description()) 48 | } 49 | flag.PrintDefaults() 50 | fmt.Printf("\nUse \"%s --help\" for help on a specific "+ 51 | "command\n", progName) 52 | } 53 | 54 | flag.Parse() 55 | if len(args) < 1 { 56 | flag.Usage() 57 | return 58 | } 59 | 60 | subcommand := args[0] 61 | for _, cmd := range cmds { 62 | if cmd.Name() != subcommand { 63 | continue 64 | } 65 | 66 | // Parse and validate the command specific arguments. 67 | if err := cmd.Init(args[1:]); err != nil { 68 | fmt.Println(err) 69 | fmt.Println() 70 | fmt.Printf("See \"%s %s --help\".\n", progName, subcommand) 71 | os.Exit(1) 72 | } 73 | 74 | // Execute this command. 75 | log.Println("Executing command:", cmd.Name()) 76 | cmd.Execute() 77 | return 78 | } 79 | 80 | fmt.Printf("%[1]s: '%s' is not a valid command. See '%[1]s --help'.\n", 81 | progName, subcommand) 82 | } 83 | -------------------------------------------------------------------------------- /cmd/committree.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/ssrathi/gogit/git" 9 | "github.com/ssrathi/gogit/util" 10 | ) 11 | 12 | // CommitTreeCommand lists the components of "commit-tree" comamnd. 13 | type CommitTreeCommand struct { 14 | fs *flag.FlagSet 15 | treeHash string 16 | parentHash string 17 | msg string 18 | } 19 | 20 | // NewCommitTreeCommand creates a new command object. 21 | func NewCommitTreeCommand() *CommitTreeCommand { 22 | cmd := &CommitTreeCommand{ 23 | fs: flag.NewFlagSet("commit-tree", flag.ExitOnError), 24 | } 25 | 26 | cmd.fs.StringVar(&cmd.parentHash, "p", "", "id of a parent commit object") 27 | cmd.fs.StringVar(&cmd.msg, "m", "", "A paragraph in the commit log message") 28 | return cmd 29 | } 30 | 31 | // Name gives the name of the command. 32 | func (cmd *CommitTreeCommand) Name() string { 33 | return cmd.fs.Name() 34 | } 35 | 36 | // Init initializes and validates the given command. 37 | func (cmd *CommitTreeCommand) Init(args []string) error { 38 | cmd.fs.Usage = cmd.Usage 39 | if err := cmd.fs.Parse(args); err != nil { 40 | return err 41 | } 42 | 43 | if cmd.fs.NArg() < 1 { 44 | return errors.New("error: Missing argument") 45 | } 46 | 47 | // Message is currently mandatory till getting it from an editor is implemented. 48 | if cmd.msg == "" { 49 | return errors.New("error: Missing [-m message] argument") 50 | } 51 | 52 | cmd.treeHash = cmd.fs.Arg(0) 53 | return nil 54 | } 55 | 56 | // Description gives the description of the command. 57 | func (cmd *CommitTreeCommand) Description() string { 58 | return "Create a new commit object" 59 | } 60 | 61 | // Usage prints the usage string for the end user. 62 | func (cmd *CommitTreeCommand) Usage() { 63 | fmt.Printf("%s - %s\n", cmd.Name(), cmd.Description()) 64 | fmt.Printf("usage: %s [] \n", cmd.Name()) 65 | cmd.fs.PrintDefaults() 66 | } 67 | 68 | // Execute runs the given command till completion. 69 | func (cmd *CommitTreeCommand) Execute() { 70 | repo, err := git.GetRepo(".") 71 | util.Check(err) 72 | 73 | // Add a new line to the msg. 74 | msg := cmd.msg + "\n" 75 | commit, err := git.NewCommitFromParams( 76 | repo, cmd.treeHash, cmd.parentHash, msg) 77 | util.Check(err) 78 | 79 | // Write the commit now. 80 | hash, err := repo.ObjectWrite(commit.Object, true) 81 | util.Check(err) 82 | 83 | fmt.Println(hash) 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Builds](https://github.com/ssrathi/gogit/workflows/Build/badge.svg?branch=master)](https://github.com/ssrathi/gogit/actions?query=branch%3Amaster+workflow%3ABuild) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/ssrathi/gogit)](https://goreportcard.com/report/github.com/ssrathi/gogit) 3 | [![GoDoc](https://godoc.org/github.com/ssrathi/gogit?status.svg)](https://godoc.org/github.com/ssrathi/gogit) 4 | 5 | 6 | 7 | # gogit 8 | 9 | Implementation of git internal commands in Go language. 10 | 11 | This project is part of a learning exercise to implement a subset of "git" 12 | commands. It can be used to create and maintain git objects, such as blobs, 13 | trees, commits, references and tags. 14 | 15 | [demo](https://asciinema.org/a/331278?speed=2&autoplay=1&t=8) 16 | 17 | # Video with explanation 18 | 19 | This video is a supplemental tech-talk recording explaining the internals of "git" (Delievered at Nutanix, Inc, 2020) 20 | 21 | [tech-talk](https://players.brightcove.net/5850956868001/default_default/index.html?videoId=6304540004001) 22 | 23 | ## Supported commands 24 | ``` 25 | gogit - the stupid content tracker 26 | 27 | usage: gogit [] 28 | Valid commands: 29 | init Create an empty Git repository 30 | hash-object Compute object ID and optionally creates a blob from a file 31 | cat-file Provide content or type and size information for repository objects 32 | ls-tree List the contents of a tree object 33 | mktree Build a tree-object from ls-tree formatted text 34 | checkout restore working tree files 35 | commit-tree Create a new commit object 36 | log Shows the commit logs 37 | show-ref List references in a local repository 38 | update-ref Update the object name stored in a ref safely 39 | rev-parse Parse a given git identifier 40 | 41 | Use "gogit --help" for help on a specific command 42 | ``` 43 | 44 | ## Installation 45 | ``` 46 | go get github.com/ssrathi/gogit 47 | ``` 48 | 49 | ## Contributing 50 | 51 | Contributions are most welcome! Please create a new issue and link your PR to it. 52 | 53 | -------------------------------------------------------------------------------- /cmd/showref.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/ssrathi/gogit/git" 9 | "github.com/ssrathi/gogit/util" 10 | ) 11 | 12 | // ShowRefCommand lists the components of "show-ref" comamnd. 13 | type ShowRefCommand struct { 14 | fs *flag.FlagSet 15 | showHead bool 16 | verify bool 17 | pattern string 18 | } 19 | 20 | // NewShowRefCommand creates a new command object. 21 | func NewShowRefCommand() *ShowRefCommand { 22 | cmd := &ShowRefCommand{ 23 | fs: flag.NewFlagSet("show-ref", flag.ExitOnError), 24 | } 25 | 26 | cmd.fs.BoolVar(&cmd.showHead, "head", false, 27 | "Show the HEAD reference, even if it would normally be filtered out.") 28 | cmd.fs.BoolVar(&cmd.verify, "verify", false, 29 | "Enable stricter reference checking by requiring an exact ref path.") 30 | 31 | return cmd 32 | } 33 | 34 | // Name gives the name of the command. 35 | func (cmd *ShowRefCommand) Name() string { 36 | return cmd.fs.Name() 37 | } 38 | 39 | // Init initializes and validates the given command. 40 | func (cmd *ShowRefCommand) Init(args []string) error { 41 | cmd.fs.Usage = cmd.Usage 42 | if err := cmd.fs.Parse(args); err != nil { 43 | return err 44 | } 45 | 46 | if cmd.fs.NArg() == 0 && cmd.verify { 47 | return errors.New("fatal: --verify requires a reference") 48 | } 49 | 50 | if cmd.fs.NArg() >= 1 { 51 | cmd.pattern = cmd.fs.Arg(0) 52 | } 53 | 54 | return nil 55 | } 56 | 57 | // Description gives the description of the command. 58 | func (cmd *ShowRefCommand) Description() string { 59 | return "List references in a local repository" 60 | } 61 | 62 | // Usage prints the usage string for the end user. 63 | func (cmd *ShowRefCommand) Usage() { 64 | fmt.Printf("%s - %s\n", cmd.Name(), cmd.Description()) 65 | fmt.Printf("usage: %s [] \n", cmd.Name()) 66 | cmd.fs.PrintDefaults() 67 | } 68 | 69 | // Execute runs the given command till completion. 70 | func (cmd *ShowRefCommand) Execute() { 71 | repo, err := git.GetRepo(".") 72 | util.Check(err) 73 | 74 | if cmd.verify { 75 | refHash, err := repo.ValidateRef(cmd.pattern) 76 | util.Check(err) 77 | 78 | fmt.Printf("%s %s\n", refHash, cmd.pattern) 79 | } else { 80 | // Get a list of all the references. 81 | refs, err := repo.GetRefs(cmd.pattern, cmd.showHead) 82 | util.Check(err) 83 | 84 | for _, ref := range refs { 85 | fmt.Printf("%s %s\n", ref.RefHash, ref.Name) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /cmd/lstree.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/ssrathi/gogit/git" 10 | "github.com/ssrathi/gogit/util" 11 | ) 12 | 13 | // LsTreeCommand lists the components of "lstree" comamnd. 14 | type LsTreeCommand struct { 15 | fs *flag.FlagSet 16 | revision string 17 | } 18 | 19 | // NewLsTreeCommand creates a new command object. 20 | func NewLsTreeCommand() *LsTreeCommand { 21 | cmd := &LsTreeCommand{ 22 | fs: flag.NewFlagSet("ls-tree", flag.ExitOnError), 23 | } 24 | return cmd 25 | } 26 | 27 | // Name gives the name of the command. 28 | func (cmd *LsTreeCommand) Name() string { 29 | return cmd.fs.Name() 30 | } 31 | 32 | // Init initializes and validates the given command. 33 | func (cmd *LsTreeCommand) Init(args []string) error { 34 | cmd.fs.Usage = cmd.Usage 35 | if err := cmd.fs.Parse(args); err != nil { 36 | return err 37 | } 38 | 39 | if cmd.fs.NArg() < 1 { 40 | return errors.New("error: Missing argument") 41 | } 42 | 43 | cmd.revision = cmd.fs.Arg(0) 44 | return nil 45 | } 46 | 47 | // Description gives the description of the command. 48 | func (cmd *LsTreeCommand) Description() string { 49 | return "List the contents of a tree object" 50 | } 51 | 52 | // Usage prints the usage string for the end user. 53 | func (cmd *LsTreeCommand) Usage() { 54 | fmt.Printf("%s - %s\n", cmd.Name(), cmd.Description()) 55 | fmt.Printf("usage: %s \n", cmd.Name()) 56 | cmd.fs.PrintDefaults() 57 | } 58 | 59 | // Execute runs the given command till completion. 60 | func (cmd *LsTreeCommand) Execute() { 61 | repo, err := git.GetRepo(".") 62 | util.Check(err) 63 | 64 | // Resolve the given name to a full hash. 65 | objHash, err := repo.UniqueNameResolve(cmd.revision) 66 | util.Check(err) 67 | 68 | obj, err := repo.ObjectParse(objHash) 69 | if err != nil { 70 | fmt.Println("fatal: not a tree object.", err) 71 | os.Exit(1) 72 | } 73 | if obj.ObjType != "tree" && obj.ObjType != "commit" { 74 | fmt.Println("fatal: not a tree object") 75 | os.Exit(1) 76 | } 77 | 78 | // If it is a "commit" object, then get its "tree" component first. 79 | if obj.ObjType == "commit" { 80 | commit, err := git.NewCommit(repo, obj) 81 | util.Check(err) 82 | obj, err = repo.ObjectParse(commit.TreeHash()) 83 | util.Check(err) 84 | } 85 | 86 | // "obj" is now a valid tree object. 87 | tree, err := git.NewTree(repo, obj) 88 | util.Check(err) 89 | 90 | fmt.Print(tree.Print()) 91 | } 92 | -------------------------------------------------------------------------------- /cmd/checkout.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/ssrathi/gogit/git" 10 | "github.com/ssrathi/gogit/util" 11 | ) 12 | 13 | // CheckoutCommand lists the components of "checkout" comamnd. 14 | type CheckoutCommand struct { 15 | fs *flag.FlagSet 16 | path string 17 | revision string 18 | } 19 | 20 | // NewCheckoutCommand creates a new command object. 21 | func NewCheckoutCommand() *CheckoutCommand { 22 | cmd := &CheckoutCommand{ 23 | fs: flag.NewFlagSet("checkout", flag.ExitOnError), 24 | } 25 | 26 | cmd.fs.StringVar(&cmd.path, "path", ".", "Path to create the files") 27 | return cmd 28 | } 29 | 30 | // Name gives the name of the command. 31 | func (cmd *CheckoutCommand) Name() string { 32 | return cmd.fs.Name() 33 | } 34 | 35 | // Description gives the description of the command. 36 | func (cmd *CheckoutCommand) Description() string { 37 | return "Restore working tree files" 38 | } 39 | 40 | // Init initializes and validates the given command. 41 | func (cmd *CheckoutCommand) Init(args []string) error { 42 | cmd.fs.Usage = cmd.Usage 43 | if err := cmd.fs.Parse(args); err != nil { 44 | return err 45 | } 46 | 47 | if cmd.fs.NArg() < 1 { 48 | return errors.New("error: Missing argument") 49 | } 50 | 51 | cmd.revision = cmd.fs.Arg(0) 52 | return nil 53 | } 54 | 55 | // Usage prints the usage string for the end user. 56 | func (cmd *CheckoutCommand) Usage() { 57 | fmt.Printf("%s - %s\n", cmd.Name(), cmd.Description()) 58 | fmt.Printf("usage: %s [] \n", cmd.Name()) 59 | cmd.fs.PrintDefaults() 60 | } 61 | 62 | // Execute runs the given command till completion. 63 | func (cmd *CheckoutCommand) Execute() { 64 | repo, err := git.GetRepo(".") 65 | util.Check(err) 66 | 67 | // Resolve the given revision to a full hash. 68 | objHash, err := repo.UniqueNameResolve(cmd.revision) 69 | util.Check(err) 70 | 71 | obj, err := repo.ObjectParse(objHash) 72 | if err != nil { 73 | fmt.Println("fatal: not a tree object.", err) 74 | os.Exit(1) 75 | } 76 | if obj.ObjType != "tree" && obj.ObjType != "commit" { 77 | fmt.Println("fatal: not a tree object") 78 | os.Exit(1) 79 | } 80 | 81 | // If it is a "commit" object, then get its "tree" component first. 82 | if obj.ObjType == "commit" { 83 | commit, err := git.NewCommit(repo, obj) 84 | util.Check(err) 85 | obj, err = repo.ObjectParse(commit.TreeHash()) 86 | util.Check(err) 87 | } 88 | 89 | // "obj" is now a valid tree object. 90 | tree, err := git.NewTree(repo, obj) 91 | util.Check(err) 92 | 93 | err = tree.Checkout(cmd.path) 94 | util.Check(err) 95 | } 96 | -------------------------------------------------------------------------------- /cmd/log.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/ssrathi/gogit/git" 9 | "github.com/ssrathi/gogit/util" 10 | ) 11 | 12 | // LogCommand lists the components of "log" comamnd. 13 | type LogCommand struct { 14 | fs *flag.FlagSet 15 | limit uint 16 | revision string 17 | } 18 | 19 | // NewLogCommand creates a new command object. 20 | func NewLogCommand() *LogCommand { 21 | cmd := &LogCommand{ 22 | fs: flag.NewFlagSet("log", flag.ExitOnError), 23 | } 24 | 25 | cmd.fs.UintVar(&cmd.limit, "n", 0, "Limit the number of commits to output") 26 | return cmd 27 | } 28 | 29 | // Name gives the name of the command. 30 | func (cmd *LogCommand) Name() string { 31 | return cmd.fs.Name() 32 | } 33 | 34 | // Init initializes and validates the given command. 35 | func (cmd *LogCommand) Init(args []string) error { 36 | cmd.fs.Usage = cmd.Usage 37 | if err := cmd.fs.Parse(args); err != nil { 38 | return err 39 | } 40 | 41 | if cmd.fs.NArg() < 1 { 42 | cmd.revision = "HEAD" 43 | } else { 44 | cmd.revision = cmd.fs.Arg(0) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | // Description gives the description of the command. 51 | func (cmd *LogCommand) Description() string { 52 | return "Shows the commit logs" 53 | } 54 | 55 | // Usage prints the usage string for the end user. 56 | func (cmd *LogCommand) Usage() { 57 | fmt.Printf("%s - %s\n", cmd.Name(), cmd.Description()) 58 | fmt.Printf("usage: %s [] []\n", cmd.Name()) 59 | cmd.fs.PrintDefaults() 60 | } 61 | 62 | // Execute runs the given command till completion. 63 | func (cmd *LogCommand) Execute() { 64 | repo, err := git.GetRepo(".") 65 | util.Check(err) 66 | 67 | // Resolve the given revision to a full hash. 68 | commitHash, err := repo.UniqueNameResolve(cmd.revision) 69 | util.Check(err) 70 | 71 | var printed uint 72 | for { 73 | obj, err := repo.ObjectParse(commitHash) 74 | if err != nil || obj.ObjType != "commit" { 75 | fmt.Printf("fatal: not a commit object (%s)\n", commitHash) 76 | os.Exit(1) 77 | } 78 | 79 | // Print this commit now. 80 | commit, err := git.NewCommit(repo, obj) 81 | util.Check(err) 82 | commitStr, err := commit.PrettyPrint() 83 | util.Check(err) 84 | 85 | // Print the commit msg now. If it doesn't end with a newline, then 86 | // add one manually. 87 | fmt.Printf(commitStr) 88 | if commitStr[len(commitStr)-1] != byte('\n') { 89 | fmt.Println() 90 | } 91 | 92 | // See if the user specified limit is reached. 93 | printed++ 94 | if cmd.limit > 0 && printed == cmd.limit { 95 | break 96 | } 97 | 98 | // Find the parent list of this commit. 99 | parents := commit.Parents() 100 | // If there are no more parents (base commit), then stop. 101 | if len(parents) == 0 { 102 | break 103 | } 104 | 105 | // Currently, "git log" only supports a single parent. In real "git", 106 | // there can be more than one parent in "merge" scenarios. 107 | commitHash = parents[0] 108 | 109 | // Put a new line between two successive commits. 110 | fmt.Println() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /cmd/catfile.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/ssrathi/gogit/git" 9 | "github.com/ssrathi/gogit/util" 10 | ) 11 | 12 | // CatFileCommand lists the components of "cat-file" comamnd. 13 | type CatFileCommand struct { 14 | fs *flag.FlagSet 15 | revision string 16 | getType bool 17 | getSize bool 18 | printObj bool 19 | } 20 | 21 | // NewCatFileCommand creates a new command object. 22 | func NewCatFileCommand() *CatFileCommand { 23 | cmd := &CatFileCommand{ 24 | fs: flag.NewFlagSet("cat-file", flag.ExitOnError), 25 | } 26 | 27 | cmd.fs.BoolVar(&cmd.getType, "t", false, 28 | "Instead of the content, show the object type identified by ") 29 | cmd.fs.BoolVar(&cmd.getSize, "s", false, 30 | "Instead of the content, show the object size identified by ") 31 | cmd.fs.BoolVar(&cmd.printObj, "p", false, 32 | "Pretty-print the contents of based on its type.") 33 | return cmd 34 | } 35 | 36 | // Name gives the name of the command. 37 | func (cmd *CatFileCommand) Name() string { 38 | return cmd.fs.Name() 39 | } 40 | 41 | // Description gives the description of the command. 42 | func (cmd *CatFileCommand) Description() string { 43 | return "Provide content or type and size information for repository objects" 44 | } 45 | 46 | // Init initializes and validates the given command. 47 | func (cmd *CatFileCommand) Init(args []string) error { 48 | cmd.fs.Usage = cmd.Usage 49 | if err := cmd.fs.Parse(args); err != nil { 50 | return err 51 | } 52 | 53 | if cmd.fs.NArg() < 1 { 54 | return errors.New("error: Missing argument") 55 | } 56 | 57 | // All optional boolean args are mutually exclusive 58 | if !(cmd.getType || cmd.getSize || cmd.printObj) { 59 | return errors.New("error: one of '-t', '-s' or '-p' must be provided") 60 | } 61 | 62 | if cmd.getType && cmd.getSize { 63 | return errors.New("error: switch 't' and 's' are incompatible") 64 | } 65 | if cmd.getSize && cmd.printObj { 66 | return errors.New("error: switch 's' and 'p' are incompatible") 67 | } 68 | if cmd.printObj && cmd.getType { 69 | return errors.New("error: switch 'p' and 't' are incompatible") 70 | } 71 | 72 | cmd.revision = cmd.fs.Arg(0) 73 | return nil 74 | } 75 | 76 | // Usage prints the usage string for the end user. 77 | func (cmd *CatFileCommand) Usage() { 78 | fmt.Printf("%s - %s\n", cmd.Name(), cmd.Description()) 79 | fmt.Printf("usage: %s [] \n", cmd.Name()) 80 | cmd.fs.PrintDefaults() 81 | } 82 | 83 | // Execute runs the given command till completion. 84 | func (cmd *CatFileCommand) Execute() { 85 | repo, err := git.GetRepo(".") 86 | util.Check(err) 87 | 88 | // Resolve the given name to a full hash. 89 | objHash, err := repo.UniqueNameResolve(cmd.revision) 90 | util.Check(err) 91 | 92 | obj, err := repo.ObjectParse(objHash) 93 | util.Check(err) 94 | 95 | var objIntf git.ObjIntf 96 | switch obj.ObjType { 97 | case "blob": 98 | objIntf, err = git.NewBlob(repo, obj) 99 | util.Check(err) 100 | case "tree": 101 | objIntf, err = git.NewTree(repo, obj) 102 | util.Check(err) 103 | case "commit": 104 | objIntf, err = git.NewCommit(repo, obj) 105 | util.Check(err) 106 | } 107 | 108 | // Only one of 'printObj', 'getType' and 'getSize' is provided. 109 | if cmd.printObj { 110 | fmt.Print(objIntf.Print()) 111 | } else if cmd.getType { 112 | fmt.Println(objIntf.Type()) 113 | } else if cmd.getSize { 114 | fmt.Println(objIntf.DataSize()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /git/tree.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | // TreeEntry is a struct holding components of a tree object. 16 | type TreeEntry struct { 17 | mode string 18 | hash string 19 | objType string 20 | name string 21 | } 22 | 23 | // Tree is a object with a list of "tree" entries and a git object. 24 | type Tree struct { 25 | Repository *Repo 26 | *Object 27 | Entries []TreeEntry 28 | } 29 | 30 | // NewTree creates a new tree object by parsing a Object. 31 | func NewTree(repo *Repo, obj *Object) (*Tree, error) { 32 | if obj.ObjType != "tree" { 33 | return nil, fmt.Errorf("Malformed object: bad type %s", obj.ObjType) 34 | } 35 | 36 | tree := Tree{ 37 | Repository: repo, 38 | Object: obj, 39 | Entries: []TreeEntry{}, 40 | } 41 | 42 | // Parse the tree data. 43 | if err := tree.ParseData(); err != nil { 44 | return nil, err 45 | } 46 | 47 | return &tree, nil 48 | } 49 | 50 | // NewTreeFromInput parses the given string and create a tree from it. 51 | func NewTreeFromInput(repo *Repo, input string) (*Tree, error) { 52 | data := []byte{} 53 | entries := strings.Split(input, "\n") 54 | for _, entry := range entries { 55 | if len(entry) == 0 { 56 | continue 57 | } 58 | 59 | // Each entry is in the following format. 60 | // 100644 blob a1e5680b811ded8762390b94a40643293ee6c1b0README.md 61 | props := strings.Fields(entry) 62 | 63 | // Strip initial 0s from the mode if any. 64 | mode := strings.TrimLeft(props[0], "0") 65 | data = append(data, []byte(mode)...) 66 | data = append(data, byte(' ')) 67 | data = append(data, []byte(props[3])...) 68 | data = append(data, byte('\x00')) 69 | 70 | // Append blob/tree hash as bytes. 71 | byteHash, err := hex.DecodeString(props[2]) 72 | if err != nil { 73 | return nil, err 74 | } 75 | data = append(data, byteHash...) 76 | } 77 | 78 | obj := NewObject("tree", data) 79 | return NewTree(repo, obj) 80 | } 81 | 82 | // Type returns the type string of a tree object. 83 | func (tree *Tree) Type() string { 84 | return "tree" 85 | } 86 | 87 | // DataSize returns the size of the data of a tree object. 88 | func (tree *Tree) DataSize() int { 89 | return len(tree.ObjData) 90 | } 91 | 92 | // Print returns a string representation of a tree object. 93 | func (tree *Tree) Print() string { 94 | var b strings.Builder 95 | for _, entry := range tree.Entries { 96 | // Prepend 0 in front of mode to make it 6 char long. 97 | entryMode := strings.Repeat("0", 6-len(entry.mode)) + entry.mode 98 | 99 | fmt.Fprintf(&b, "%s %s %s\t%s\n", 100 | entryMode, entry.objType, entry.hash, entry.name) 101 | } 102 | 103 | return b.String() 104 | } 105 | 106 | // ParseData parses a tree object's bytes and prepares a list of its components. 107 | func (tree *Tree) ParseData() error { 108 | datalen := len(tree.ObjData) 109 | for start := 0; start < datalen; { 110 | // First get the mode which has a space after that. 111 | data := tree.ObjData[start:] 112 | spaceInd := bytes.IndexByte(data, byte(' ')) 113 | entryMode := string(data[0:spaceInd]) 114 | 115 | // Mode must be 40000 for directories and 100xxx for files. 116 | if len(entryMode) != 5 && len(entryMode) != 6 { 117 | return fmt.Errorf("Malformed object: bad mode %s", entryMode) 118 | } 119 | 120 | // Next get the name/path which has a null char after that. 121 | nameInd := bytes.IndexByte(data, byte('\x00')) 122 | entryName := string(data[spaceInd+1 : nameInd]) 123 | 124 | // Next 20 bytes form the entry sha1 hash. It is in binary. 125 | entryHash := hex.EncodeToString(data[nameInd+1 : nameInd+21]) 126 | 127 | // Get the type of each hash for printing. 128 | obj, err := tree.Repository.ObjectParse(entryHash) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | // Prepare a new TreeEntry object and push it to the list. 134 | entry := TreeEntry{ 135 | mode: entryMode, 136 | hash: entryHash, 137 | objType: obj.ObjType, 138 | name: entryName, 139 | } 140 | tree.Entries = append(tree.Entries, entry) 141 | 142 | // Update the next starting point. 143 | start += (nameInd + 21) 144 | } 145 | 146 | // Sort the entries (git keeps them sorted for display) 147 | sort.Slice(tree.Entries, func(i, j int) bool { 148 | return tree.Entries[i].name < tree.Entries[j].name 149 | }) 150 | 151 | return nil 152 | } 153 | 154 | // Checkout recreates an entire worktree in a given path by recursively reading 155 | // the blobs and trees inside this tree object. 156 | func (tree *Tree) Checkout(path string) error { 157 | for _, entry := range tree.Entries { 158 | createPath := filepath.Join(path, entry.name) 159 | obj, err := tree.Repository.ObjectParse(entry.hash) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | if entry.objType == "tree" { 165 | // handle a tree object 166 | if err := os.Mkdir(createPath, os.ModePerm); err != nil { 167 | return err 168 | } 169 | 170 | // Recurse on the tree object. 171 | tree, err := NewTree(tree.Repository, obj) 172 | if err != nil { 173 | return err 174 | } 175 | 176 | err = tree.Checkout(createPath) 177 | if err != nil { 178 | return err 179 | } 180 | } else { 181 | // handle a blob object 182 | blob, err := NewBlob(tree.Repository, obj) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | // Mode is in the format 100xxx. Strip the 100 from it. 188 | mode, _ := strconv.ParseInt(entry.mode[3:], 8, 32) 189 | err = ioutil.WriteFile(createPath, blob.ObjData, os.FileMode(mode)) 190 | if err != nil { 191 | return err 192 | } 193 | } 194 | } 195 | 196 | return nil 197 | } 198 | -------------------------------------------------------------------------------- /git/commit.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "runtime" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | const ( 13 | // AuthorName is tbe name of the author used as part of metadata of a commit. 14 | AuthorName string = "Shyamsunder Rathi" 15 | // AuthorEmail is the email of the author used as part of metadata of a commit. 16 | AuthorEmail string = "sxxxxxx@gmail.com" 17 | ) 18 | 19 | type entryMap map[string][]string 20 | 21 | // Commit is a object with a map of "commit" entries, commit msg and a git object. 22 | type Commit struct { 23 | Repository *Repo 24 | *Object 25 | Entries entryMap 26 | // Keep the keys to maintain the insertion order. 27 | Keys []string 28 | Msg string 29 | } 30 | 31 | // NewCommit creates a new commit object by parsing a Object. 32 | func NewCommit(repo *Repo, obj *Object) (*Commit, error) { 33 | if obj.ObjType != "commit" { 34 | return nil, fmt.Errorf("Malformed object: bad type %s", obj.ObjType) 35 | } 36 | 37 | commit := Commit{ 38 | Repository: repo, 39 | Object: obj, 40 | Entries: entryMap{}, 41 | Keys: []string{}, 42 | } 43 | 44 | // Parse the tree data. 45 | if err := commit.ParseData(); err != nil { 46 | return nil, err 47 | } 48 | 49 | return &commit, nil 50 | } 51 | 52 | // NewCommitFromParams builds a commit object using a 'tree' and optionall a 53 | // 'parent' hash, and a given commit message. 54 | // This can be used by CLI commands such as "gogit commit-tree". 55 | func NewCommitFromParams(repo *Repo, treeHash, parentHash, msg string) (*Commit, error) { 56 | data := []byte{} 57 | data = append(data, []byte("tree "+treeHash+"\n")...) 58 | if parentHash != "" { 59 | data = append(data, []byte("parent "+parentHash+"\n")...) 60 | } 61 | 62 | // Get the current time in format 63 | // Example: 1589530357 -0700 64 | cTime := time.Now() 65 | timeStamp := strconv.FormatInt(cTime.Unix(), 10) + " " + cTime.Format("-0700") 66 | 67 | // Build author and commiter values 68 | authorValue := fmt.Sprintf("%s <%s> %s", AuthorName, AuthorEmail, timeStamp) 69 | 70 | data = append(data, []byte("author "+authorValue+"\n")...) 71 | data = append(data, []byte("committer "+authorValue+"\n")...) 72 | data = append(data, byte('\n')) 73 | data = append(data, []byte(msg)...) 74 | 75 | obj := NewObject("commit", data) 76 | return NewCommit(repo, obj) 77 | } 78 | 79 | // Type returns the type string of a commit object. 80 | func (commit *Commit) Type() string { 81 | return "commit" 82 | } 83 | 84 | // DataSize returns the size of the data of a commit object. 85 | func (commit *Commit) DataSize() int { 86 | return len(commit.ObjData) 87 | } 88 | 89 | // TreeHash returns the "tree" object hash inside the given commit object. 90 | // Each commit object has only one "tree" object inside it. 91 | func (commit *Commit) TreeHash() string { 92 | return commit.Entries["tree"][0] 93 | } 94 | 95 | // Parents returns a list of parents of the given commit. If there are no 96 | // parent (base commit), then it returns an empty list. 97 | func (commit *Commit) Parents() []string { 98 | return commit.Entries["parent"] 99 | } 100 | 101 | // Author returns the name and email of the author of the given commit. 102 | func (commit *Commit) Author() (name, email string) { 103 | items := strings.Fields(commit.Entries["author"][0]) 104 | email = items[len(items)-3] 105 | name = strings.Join(items[:len(items)-3], " ") 106 | 107 | // Remove <> around email (format: ) 108 | email = strings.TrimLeft(email, "<") 109 | email = strings.TrimRight(email, ">") 110 | return 111 | } 112 | 113 | // Print returns a string representation of a commit object. 114 | func (commit *Commit) Print() string { 115 | var b strings.Builder 116 | 117 | // Print the key-values in insertion order first. 118 | for _, key := range commit.Keys { 119 | for _, val := range commit.Entries[key] { 120 | fmt.Fprintf(&b, "%s %s\n", key, val) 121 | } 122 | } 123 | 124 | // Print a blank line followed by the commit message. 125 | fmt.Fprintln(&b) 126 | fmt.Fprintf(&b, commit.Msg) 127 | return b.String() 128 | } 129 | 130 | // PrettyPrint prints a commit object in a human readable format, similar to 131 | // what is shown by "git log" output. 132 | func (commit *Commit) PrettyPrint() (string, error) { 133 | var b strings.Builder 134 | 135 | // Find the commit hash of this commit object first. 136 | commitHash, _ := commit.Repository.ObjectWrite(commit.Object, false) 137 | 138 | // Print the needed key-values in "git log" format. 139 | // On Bash and similar, git prints the commit hash in Yellow color. 140 | // TODO: Move color coding to a util API. 141 | if runtime.GOOS != "windows" { 142 | fmt.Fprintf(&b, "\033[33m"+"commit %s\n"+"\033[0m", commitHash) 143 | } else { 144 | fmt.Fprintf(&b, "commit %s\n", commitHash) 145 | } 146 | authorEntry := commit.Entries["author"][0] 147 | // Author line is in the following format: 148 | // " " 149 | // Example: "Shyamsunder Rathi 1589619289 -0700" 150 | items := strings.Fields(authorEntry) 151 | timezone := items[len(items)-1] 152 | epoch, _ := strconv.ParseInt(items[len(items)-2], 10, 64) 153 | epochTime := time.Unix(epoch, 0) 154 | // "git" time format in logs: "Sat May 16 19:26:38 2020 -0700" 155 | timeStr := epochTime.Format("Mon Jan 02 15:04:05 2006") 156 | 157 | email := items[len(items)-3] 158 | author := strings.Join(items[:len(items)-3], " ") 159 | fmt.Fprintf(&b, "Author: %s %s\n", author, email) 160 | fmt.Fprintf(&b, "Date: %s %s\n", timeStr, timezone) 161 | 162 | // Print a blank line followed by the commit message. 163 | fmt.Fprintln(&b) 164 | 165 | // Message is printed with 4 lines indentation in each line. 166 | msgParts := strings.Split(commit.Msg, "\n") 167 | for i, msg := range msgParts { 168 | if len(msg) != 0 { 169 | fmt.Fprintf(&b, " %s", msg) 170 | } 171 | if i != len(msgParts)-1 { 172 | fmt.Fprintln(&b) 173 | } 174 | } 175 | 176 | return b.String(), nil 177 | } 178 | 179 | // ParseData parses a commit object's bytes and prepares a dictionary of its 180 | // components. 181 | func (commit *Commit) ParseData() error { 182 | /* Commit object has the following format: 183 | \n 184 | \n 185 | \n 186 | \n 187 | \n 188 | ... 189 | 190 | 394 | // refs/tags/ 395 | // refs/heads/ 396 | // refs/remotes/ 397 | // refs/remotes//HEAD 398 | // 399 | // Just pick the ref with shortest name as per the rules above. 400 | // TODO: enhance this algorithm with actual matches. Shortest way just 401 | // happens to work right now. 402 | if len(refs) > 0 { 403 | sort.Slice(refs, func(i, j int) bool { 404 | return len(refs[i].Name) < len(refs[j].Name) 405 | }) 406 | log.Printf("Found %d references for %s", len(refs), name) 407 | matches = append(matches, refs[0].RefHash) 408 | } 409 | 410 | // The given name may even be a short or full hash. 411 | // Check if the given ref is a valid hexadecimal hash. 412 | re := regexp.MustCompile(`^[a-fA-F0-9]*$`) 413 | if !re.MatchString(name) { 414 | return matches, nil 415 | } 416 | 417 | // If the hash is smaller than 4, then return. "git" doesn't resolve 418 | // a hash smaller than 4 characters. 419 | // Also, git hashes are limited to 40 char (SHA1) 420 | if len(name) < 4 || len(name) > 40 { 421 | return matches, nil 422 | } 423 | 424 | // If the hash is given in full size, then use it as is. 425 | if len(name) == 40 { 426 | matches = append(matches, name) 427 | return matches, nil 428 | } 429 | 430 | // If reached here, then 'name' may be a valid short hash matching one or 431 | // more full hashes. Collect them all by looking at all files inside '.git/objects'. 432 | objectsPath, err := r.DirPath(true, "objects", name[0:2]) 433 | if err != nil { 434 | log.Printf("Objects path not found for name %s (%v)", name, err) 435 | return matches, nil 436 | } 437 | 438 | // Read all files under this directory and collect all files matching the 439 | // remaining hash (after first 2 char). 440 | files, err := ioutil.ReadDir(objectsPath) 441 | if err != nil { 442 | log.Printf("Objects path dir %s access error (%v)", objectsPath, err) 443 | return matches, nil 444 | } 445 | 446 | for _, file := range files { 447 | if !file.IsDir() && strings.HasPrefix(file.Name(), name[2:]) { 448 | matches = append(matches, name[0:2]+file.Name()) 449 | } 450 | } 451 | 452 | return matches, nil 453 | } 454 | 455 | // UniqueNameResolve converts a given name to a unique valid full object hash. 456 | // It returns an error if there are less or more than 1 matching objects to the 457 | // given name. 458 | // This can be used by many commands to act on a single unique hash after taking 459 | // a possible ambiguous name from the user. 460 | func (r *Repo) UniqueNameResolve(name string) (string, error) { 461 | errmsg := fmt.Sprintf("fatal: ambiguous argument '%s': unknown revision or "+ 462 | "path not in the working tree", name) 463 | 464 | matches, err := r.NameResolve(name) 465 | if err != nil || len(matches) == 0 { 466 | log.Printf("Failed to convert name %s to object hash or no matches "+ 467 | "found: %v", name, err) 468 | return "", fmt.Errorf(errmsg) 469 | } 470 | 471 | // If there are more than one matches, then prepare an error with all the 472 | // matches. 473 | if len(matches) > 1 { 474 | msg := fmt.Sprintf("short SHA1 %s is ambiguous\n"+ 475 | "Matching SHA1 list:\n"+ 476 | "%s", name, strings.Join(matches, "\n")) 477 | return "", fmt.Errorf(msg) 478 | } 479 | 480 | return matches[0], nil 481 | } 482 | 483 | // ValidateRef strictly validates if a given reference is a valid reference 484 | // in the local repository (or is HEAD). Returns the resolved object hash. 485 | // This can be used by commands such as "gogit show-ref -verify". 486 | func (r *Repo) ValidateRef(ref string) (string, error) { 487 | msg := "fatal: '{%s}' - not a valid ref" 488 | if ref != "HEAD" && !strings.HasPrefix(ref, "refs") { 489 | return "", fmt.Errorf(msg, ref) 490 | } 491 | 492 | refHash, err := r.UniqueNameResolve(ref) 493 | if err != nil { 494 | log.Println(err) 495 | return "", fmt.Errorf(msg, ref) 496 | } 497 | 498 | return refHash, nil 499 | } 500 | 501 | // UpdateRef updates the given strict reference to the full hash by resolving 502 | // the value given by newValue. 503 | // If 'ref' is a symbolic reference (such as HEAD), then the target reference 504 | // is updated instaed (if HEAD is pointing to master, then master is updated). 505 | func (r *Repo) UpdateRef(ref string, newValue string) error { 506 | if ref != "HEAD" && !strings.HasPrefix(ref, "refs/heads") && 507 | !strings.HasPrefix(ref, "refs/tags") { 508 | return fmt.Errorf("fatal: '{%s}' - not a valid ref", ref) 509 | } 510 | 511 | // Get the full hash from given newValue. 512 | newValueHash, err := r.UniqueNameResolve(newValue) 513 | if err != nil { 514 | return fmt.Errorf("fatal: '{%s}' - not a valid SHA1", newValue) 515 | } 516 | 517 | // If the 'ref' file is not present, then create it first. 518 | refFile, _ := r.FilePath(true, ref) 519 | if !util.IsPathPresent(refFile) { 520 | fd, err := os.Create(refFile) 521 | if err != nil { 522 | return err 523 | } 524 | fd.Close() 525 | } 526 | 527 | // Resolve the given reference to its hash and path. 528 | refHash, refPath, err := r.RefResolve(ref) 529 | if err != nil { 530 | log.Printf("RefResolve failed with err: %v", err) 531 | return fmt.Errorf("fatal: '{%s}' - not a valid ref", ref) 532 | } 533 | 534 | log.Printf("UpdateRef - refHash: %q refPath: %q ref: %q newValueHash: %q\n", 535 | refHash, refPath, ref, newValueHash) 536 | 537 | refFile, _ = r.FilePath(false, refPath) 538 | err = ioutil.WriteFile(refFile, []byte(newValueHash+"\n"), 0644) 539 | if err != nil { 540 | return err 541 | } 542 | 543 | return nil 544 | } 545 | --------------------------------------------------------------------------------