├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commands ├── add.go ├── cat_file.go ├── check_ignore.go ├── checkout.go ├── commit.go ├── hash_object.go ├── init.go ├── log.go ├── ls_files.go ├── ls_trees.go ├── reverse_parse.go ├── rm.go ├── show_ref.go ├── status.go └── tags.go ├── index └── index.go ├── main.go ├── objects ├── blob.go ├── commit.go ├── tag.go └── tree.go ├── refs └── refs.go └── repo └── repo.go /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to OpenGit 2 | 3 | We love your input! We want to make contributing to OpenGit as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## We Develop with Github 12 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 13 | 14 | ## Pull Requests 15 | 1. Fork the repo and create your branch from `main`. 16 | 2. If you've added code that should be tested, add tests. 17 | 3. If you've changed APIs, update the documentation. 18 | 4. Ensure the test suite passes. 19 | 5. Make sure your code lints. 20 | 6. Issue that pull request! 21 | 22 | ## Any contributions you make will be under the MIT Software License 23 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. 24 | 25 | ## Report bugs using -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 OpenGit 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenGit 🚀 2 | 3 | [![Go Version](https://img.shields.io/badge/Go-1.20+-00ADD8?style=flat&logo=go)](https://golang.org/doc/go1.20) 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 5 | 6 | A lightweight, educational Git implementation written in Go. OpenGit helps you understand Git's internal workings by implementing core Git functionality from scratch. 7 | 8 | ## 🌟 Features 9 | 10 | - **Core Git Operations** 11 | - Repository initialization 12 | - Staging files 13 | - Committing changes 14 | - Branching and tagging 15 | - Basic version control operations 16 | 17 | - **Git Plumbing Commands** 18 | - `hash-object`: Compute object ID 19 | - `cat-file`: Display object contents 20 | - `ls-tree`: List tree contents 21 | - `rev-parse`: Parse revisions 22 | 23 | - **Git Porcelain Commands** 24 | - `init`: Create new repository 25 | - `add`: Stage files 26 | - `commit`: Record changes 27 | - `status`: Show working tree status 28 | - `log`: Display commit history 29 | - `checkout`: Switch branches/restore files 30 | - And more! 31 | 32 | ## 🚀 Quick Start 33 | 34 | ```bash 35 | # Clone the repository 36 | git clone https://github.com/yourusername/opengit.git 37 | cd opengit 38 | 39 | # Build the project 40 | go build 41 | 42 | # Initialize a new repository 43 | ./opengit init 44 | 45 | # Add some files 46 | ./opengit add file1.txt 47 | 48 | # Create a commit 49 | ./opengit commit -m "Initial commit" 50 | 51 | # Check status 52 | ./opengit status 53 | ``` 54 | 55 | ## 📚 Command Reference 56 | 57 | ### Basic Commands 58 | 59 | | Command | Description | Usage | 60 | |---------|-------------|-------| 61 | | `init` | Create a new repository | `opengit init [path]` | 62 | | `add` | Add files to staging | `opengit add ` | 63 | | `commit` | Record changes | `opengit commit -m "message"` | 64 | | `status` | Show working tree status | `opengit status` | 65 | | `log` | Show commit history | `opengit log` | 66 | 67 | ### Advanced Commands 68 | 69 | | Command | Description | Usage | 70 | |---------|-------------|-------| 71 | | `checkout` | Switch branches/restore | `opengit checkout ` | 72 | | `tag` | Create a new tag | `opengit tag [commit]` | 73 | | `cat-file` | Show object contents | `opengit cat-file -p ` | 74 | | `ls-tree` | List tree contents | `opengit ls-tree ` | 75 | 76 | ## 🛠️ Project Structure 77 | 78 | ``` 79 | opengit/ 80 | ├── commands/ # Command implementations 81 | ├── objects/ # Git object handling 82 | ├── refs/ # Reference management 83 | ├── repo/ # Repository operations 84 | ├── index/ # Index (staging) management 85 | └── main.go # Entry point 86 | ``` 87 | 88 | ## 🤝 Contributing 89 | 90 | Contributions are welcome! Here's how you can help: 91 | 92 | 1. Fork the repository 93 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 94 | 3. Commit your changes (`git commit -m 'Add amazing feature'`) 95 | 4. Push to the branch (`git push origin feature/amazing-feature`) 96 | 5. Open a Pull Request 97 | 98 | ## 📝 License 99 | 100 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 101 | 102 | ## 🎯 Educational Purpose 103 | 104 | OpenGit is designed as an educational tool to help developers understand Git's internal mechanisms. While it implements many core Git features, it's not intended for production use. For real version control needs, please use the official Git implementation. 105 | 106 | ## ⭐ Show Your Support 107 | 108 | If you find this project helpful, please consider giving it a star! It helps others discover this educational resource. 109 | 110 | ## 📬 Contact 111 | 112 | Your Name - [@venkat1017](https://twitter.com/yourtwitterhandle) 113 | 114 | Project Link: [https://github.com/yourusername/opengit](https://github.com/yourusername/opengit) 115 | -------------------------------------------------------------------------------- /commands/add.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/index" 5 | "Opengit/objects" 6 | "Opengit/repo" 7 | "fmt" 8 | "os" 9 | ) 10 | 11 | func Add(args []string) { 12 | if len(args) < 1 { 13 | fmt.Println("Usage: Opengit add ...") 14 | os.Exit(1) 15 | } 16 | 17 | r, err := repo.NewRepository(".") 18 | if err != nil { 19 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 20 | os.Exit(1) 21 | } 22 | 23 | idx, err := index.ReadIndex(r) 24 | if err != nil { 25 | idx = &index.Index{Entries: make(map[string]*index.Entry)} 26 | } 27 | 28 | for _, file := range args { 29 | data, err := os.ReadFile(file) 30 | if err != nil { 31 | fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", file, err) 32 | continue 33 | } 34 | blob := objects.NewBlob(data) 35 | hash := blob.Hash() 36 | idx.Entries[file] = &index.Entry{Hash: hash} 37 | } 38 | 39 | err = index.WriteIndex(r, idx) 40 | if err != nil { 41 | fmt.Fprintf(os.Stderr, "Error writing index: %v\n", err) 42 | os.Exit(1) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /commands/cat_file.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/objects" 5 | "Opengit/refs" 6 | "Opengit/repo" 7 | "bytes" 8 | "fmt" 9 | "os" 10 | ) 11 | 12 | func CatFile(args []string) { 13 | if len(args) < 2 || args[0] != "-p" { 14 | fmt.Println("Usage: Opengit cat-file -p ") 15 | os.Exit(1) 16 | } 17 | 18 | r, err := repo.NewRepository(".") 19 | if err != nil { 20 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 21 | os.Exit(1) 22 | } 23 | 24 | refs := refs.NewRefs(r) 25 | name := args[1] 26 | hash := name 27 | 28 | // Handle HEAD and refs 29 | if name == "HEAD" { 30 | var err error 31 | hash, err = refs.ReadHEAD() 32 | if err != nil { 33 | fmt.Fprintf(os.Stderr, "Error reading HEAD: %v\n", err) 34 | os.Exit(1) 35 | } 36 | } else if h, err := refs.ReadRef("refs/heads/" + name); err == nil && h != "" { 37 | hash = h 38 | } else if h, err := refs.ReadRef("refs/tags/" + name); err == nil && h != "" { 39 | hash = h 40 | } 41 | 42 | data, err := r.ReadObject(hash) 43 | if err != nil { 44 | fmt.Fprintf(os.Stderr, "Error reading object %s: %v\n", hash, err) 45 | os.Exit(1) 46 | } 47 | 48 | // Parse object type and content 49 | objType := string(data[:bytes.IndexByte(data, ' ')]) 50 | content := data[bytes.IndexByte(data, '\x00')+1:] 51 | 52 | switch objType { 53 | case "blob": 54 | fmt.Print(string(content)) 55 | case "tree": 56 | tree := objects.ParseTree(content) 57 | for _, entry := range tree.Entries { 58 | fmt.Printf("%s %s %s\n", entry.Mode, entry.Hash, entry.Name) 59 | } 60 | case "commit": 61 | commit := objects.ParseCommit(content) 62 | fmt.Printf("tree %s\n", commit.Tree) 63 | for _, parent := range commit.Parents { 64 | fmt.Printf("parent %s\n", parent) 65 | } 66 | fmt.Printf("author %s\n", commit.Author) 67 | fmt.Printf("committer %s\n", commit.Committer) 68 | fmt.Printf("\n%s\n", commit.Message) 69 | default: 70 | fmt.Fprintf(os.Stderr, "Unknown object type: %s\n", objType) 71 | os.Exit(1) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /commands/check_ignore.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/repo" 5 | "bufio" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | func CheckIgnore(args []string) { 13 | if len(args) < 1 { 14 | fmt.Println("Usage: Opengit check-ignore ...") 15 | os.Exit(1) 16 | } 17 | 18 | r, err := repo.NewRepository(".") 19 | if err != nil { 20 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 21 | os.Exit(1) 22 | } 23 | 24 | ignorePatterns, err := loadGitignore(r) 25 | if err != nil { 26 | fmt.Fprintf(os.Stderr, "Error loading .gitignore: %v\n", err) 27 | os.Exit(1) 28 | } 29 | 30 | for _, path := range args { 31 | relPath, _ := filepath.Rel(r.Worktree, path) 32 | if isIgnored(relPath, ignorePatterns) { 33 | fmt.Println(path) 34 | } 35 | } 36 | } 37 | 38 | func loadGitignore(r *repo.Repository) ([]string, error) { 39 | file, err := os.Open(r.GitFile("../.gitignore")) 40 | if os.IsNotExist(err) { 41 | return nil, nil 42 | } else if err != nil { 43 | return nil, err 44 | } 45 | defer file.Close() 46 | 47 | var patterns []string 48 | scanner := bufio.NewScanner(file) 49 | for scanner.Scan() { 50 | line := strings.TrimSpace(scanner.Text()) 51 | if line != "" && !strings.HasPrefix(line, "#") { 52 | patterns = append(patterns, line) 53 | } 54 | } 55 | return patterns, scanner.Err() 56 | } 57 | 58 | func isIgnored(path string, patterns []string) bool { 59 | for _, pattern := range patterns { 60 | if matched, _ := filepath.Match(pattern, path); matched { 61 | return true 62 | } 63 | } 64 | return false 65 | } 66 | -------------------------------------------------------------------------------- /commands/checkout.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/objects" 5 | "Opengit/refs" 6 | "Opengit/repo" 7 | "bytes" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | func Checkout(args []string) { 14 | if len(args) < 1 { 15 | fmt.Println("Usage: Opengit checkout ") 16 | os.Exit(1) 17 | } 18 | 19 | r, err := repo.NewRepository(".") 20 | if err != nil { 21 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 22 | os.Exit(1) 23 | } 24 | 25 | refs := refs.NewRefs(r) 26 | target := args[0] 27 | hash := target // Assume it’s a hash; resolve if it’s a ref 28 | if refHash, err := refs.ReadRef("refs/heads/" + target); err == nil && refHash != "" { 29 | hash = refHash 30 | } 31 | 32 | // Load commit and its tree 33 | commitData, err := r.ReadObject(hash) 34 | if err != nil { 35 | fmt.Fprintf(os.Stderr, "Error reading commit %s: %v\n", hash, err) 36 | os.Exit(1) 37 | } 38 | commit := objects.ParseCommit(commitData[bytes.IndexByte(commitData, '\x00')+1:]) 39 | treeData, err := r.ReadObject(commit.Tree) 40 | if err != nil { 41 | fmt.Fprintf(os.Stderr, "Error reading tree %s: %v\n", commit.Tree, err) 42 | os.Exit(1) 43 | } 44 | tree := objects.ParseTree(treeData[bytes.IndexByte(treeData, '\x00')+1:]) 45 | 46 | // Update working tree 47 | for _, entry := range tree.Entries { 48 | blobData, err := r.ReadObject(entry.Hash) 49 | if err != nil { 50 | fmt.Fprintf(os.Stderr, "Error reading blob %s: %v\n", entry.Hash, err) 51 | continue 52 | } 53 | content := blobData[bytes.IndexByte(blobData, '\x00')+1:] 54 | path := filepath.Join(r.Worktree, entry.Name) 55 | os.MkdirAll(filepath.Dir(path), 0755) 56 | os.WriteFile(path, content, 0644) 57 | } 58 | 59 | // Update HEAD 60 | refs.WriteHEAD(hash) 61 | fmt.Printf("Checked out %s\n", hash) 62 | } 63 | -------------------------------------------------------------------------------- /commands/commit.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/index" 5 | "Opengit/objects" 6 | "Opengit/refs" 7 | "Opengit/repo" 8 | "fmt" 9 | "os" 10 | "time" 11 | ) 12 | 13 | func Commit(args []string) { 14 | if len(args) < 1 { 15 | fmt.Println("Usage: Opengit commit -m ") 16 | os.Exit(1) 17 | } 18 | 19 | if args[0] != "-m" { 20 | fmt.Println("Error: missing commit message") 21 | os.Exit(1) 22 | } 23 | 24 | message := args[1] 25 | r, err := repo.NewRepository(".") 26 | if err != nil { 27 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 28 | os.Exit(1) 29 | } 30 | 31 | // Create a tree from the current index 32 | tree := &objects.Tree{Entries: make([]objects.TreeEntry, 0)} 33 | idx, err := index.ReadIndex(r) 34 | if err != nil { 35 | fmt.Fprintf(os.Stderr, "Error reading index: %v\n", err) 36 | os.Exit(1) 37 | } 38 | 39 | for path, entry := range idx.Entries { 40 | tree.Entries = append(tree.Entries, objects.TreeEntry{ 41 | Mode: entry.Mode, 42 | Name: path, 43 | Hash: entry.Hash, 44 | }) 45 | } 46 | 47 | // Write tree object 48 | treeData := tree.Serialize() 49 | treeHash := tree.Hash() 50 | err = r.WriteObject(treeData, treeHash) 51 | if err != nil { 52 | fmt.Fprintf(os.Stderr, "Error writing tree: %v\n", err) 53 | os.Exit(1) 54 | } 55 | 56 | // Create commit object 57 | refs := refs.NewRefs(r) 58 | parentHash, _ := refs.ReadHEAD() 59 | 60 | commit := &objects.Commit{ 61 | Tree: treeHash, 62 | Parents: []string{}, 63 | Author: "User ", 64 | Committer: "User ", 65 | Message: message, 66 | Timestamp: time.Now(), 67 | } 68 | 69 | if parentHash != "" { 70 | commit.Parents = append(commit.Parents, parentHash) 71 | } 72 | 73 | // Write commit object 74 | commitData := commit.Serialize() 75 | commitHash := commit.Hash() 76 | err = r.WriteObject(commitData, commitHash) 77 | if err != nil { 78 | fmt.Fprintf(os.Stderr, "Error writing commit: %v\n", err) 79 | os.Exit(1) 80 | } 81 | 82 | // Update HEAD 83 | err = refs.WriteRef("refs/heads/main", commitHash) 84 | if err != nil { 85 | fmt.Fprintf(os.Stderr, "Error updating HEAD: %v\n", err) 86 | os.Exit(1) 87 | } 88 | 89 | fmt.Printf("Created commit %s\n", commitHash) 90 | } 91 | -------------------------------------------------------------------------------- /commands/hash_object.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/objects" 5 | "Opengit/repo" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func HashObject(args []string) { 12 | if len(args) < 1 { 13 | fmt.Println("Usage: Opengit hash-object [-w] ") 14 | os.Exit(1) 15 | } 16 | 17 | write := false 18 | file := args[0] 19 | if args[0] == "-w" { 20 | if len(args) < 2 { 21 | fmt.Println("Usage: Opengit hash-object -w ") 22 | os.Exit(1) 23 | } 24 | write = true 25 | file = args[1] 26 | } 27 | 28 | data, err := os.ReadFile(file) 29 | if err != nil { 30 | fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err) 31 | os.Exit(1) 32 | } 33 | 34 | blob := objects.NewBlob(data) 35 | hash := blob.Hash() 36 | fmt.Println(hash) 37 | 38 | if write { 39 | r, err := repo.NewRepository(".") 40 | if err != nil { 41 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 42 | os.Exit(1) 43 | } 44 | objPath := r.GitFile("objects", hash[:2], hash[2:]) 45 | err = os.MkdirAll(filepath.Dir(objPath), 0755) 46 | if err != nil { 47 | fmt.Fprintf(os.Stderr, "Error creating directory: %v\n", err) 48 | os.Exit(1) 49 | } 50 | err = os.WriteFile(objPath, blob.Serialize(), 0644) 51 | if err != nil { 52 | fmt.Fprintf(os.Stderr, "Error writing object: %v\n", err) 53 | os.Exit(1) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /commands/init.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func Init(args []string) { 10 | path := "." 11 | if len(args) > 0 { 12 | path = args[0] 13 | // Create the directory if it doesn't exist 14 | err := os.MkdirAll(path, 0755) 15 | if err != nil { 16 | fmt.Fprintf(os.Stderr, "Error creating directory %s: %v\n", path, err) 17 | os.Exit(1) 18 | } 19 | } 20 | 21 | // Create the .opengit directory instead of .git 22 | gitdir := filepath.Join(path, ".opengit") 23 | err := os.MkdirAll(gitdir, 0755) 24 | if err != nil { 25 | fmt.Fprintf(os.Stderr, "Error creating .opengit directory: %v\n", err) 26 | os.Exit(1) 27 | } 28 | 29 | // Create the required subdirectories 30 | dirs := []string{"objects", "refs/heads", "refs/tags"} 31 | for _, dir := range dirs { 32 | err := os.MkdirAll(filepath.Join(gitdir, dir), 0755) 33 | if err != nil { 34 | fmt.Fprintf(os.Stderr, "Error creating %s: %v\n", dir, err) 35 | os.Exit(1) 36 | } 37 | } 38 | 39 | // Write the initial HEAD file 40 | err = os.WriteFile(filepath.Join(gitdir, "HEAD"), []byte("ref: refs/heads/main\n"), 0644) 41 | if err != nil { 42 | fmt.Fprintf(os.Stderr, "Error writing HEAD: %v\n", err) 43 | os.Exit(1) 44 | } 45 | 46 | fmt.Printf("Initialized empty Opengit repository in %s\n", gitdir) 47 | } 48 | -------------------------------------------------------------------------------- /commands/log.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/objects" 5 | "Opengit/refs" 6 | "Opengit/repo" 7 | "bytes" 8 | "fmt" 9 | "os" 10 | ) 11 | 12 | func Log(args []string) { 13 | r, err := repo.NewRepository(".") 14 | if err != nil { 15 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 16 | os.Exit(1) 17 | } 18 | 19 | refs := refs.NewRefs(r) 20 | headHash, err := refs.ReadHEAD() 21 | if err != nil || headHash == "" { 22 | fmt.Fprintf(os.Stderr, "No commits yet\n") 23 | os.Exit(1) 24 | } 25 | 26 | for hash := headHash; hash != ""; { 27 | objPath := r.GitFile("objects", hash[:2], hash[2:]) 28 | data, err := os.ReadFile(objPath) 29 | if err != nil { 30 | fmt.Fprintf(os.Stderr, "Error reading commit %s: %v\n", hash, err) 31 | os.Exit(1) 32 | } 33 | 34 | content := data[bytes.IndexByte(data, '\x00')+1:] 35 | commit := objects.ParseCommit(content) 36 | fmt.Printf("commit %s\n", hash) 37 | fmt.Printf("Author: %s\n", commit.Author) 38 | fmt.Printf("Date: %s\n", commit.Timestamp) 39 | fmt.Printf("\n %s\n\n", commit.Message) 40 | 41 | if len(commit.Parents) > 0 { 42 | hash = commit.Parents[0] // Follow first parent 43 | } else { 44 | break 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /commands/ls_files.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/index" 5 | "Opengit/repo" 6 | "fmt" 7 | "os" 8 | ) 9 | 10 | func LsFiles(args []string) { 11 | r, err := repo.NewRepository(".") 12 | if err != nil { 13 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 14 | os.Exit(1) 15 | } 16 | 17 | idx, err := index.ReadIndex(r) 18 | if err != nil { 19 | fmt.Fprintf(os.Stderr, "Error reading index: %v\n", err) 20 | os.Exit(1) 21 | } 22 | 23 | for path := range idx.Entries { 24 | fmt.Println(path) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /commands/ls_trees.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/objects" 5 | "Opengit/repo" 6 | "bytes" 7 | "fmt" 8 | "os" 9 | ) 10 | 11 | func LsTree(args []string) { 12 | if len(args) < 1 { 13 | fmt.Println("Usage: mygit ls-tree ") 14 | os.Exit(1) 15 | } 16 | 17 | r, err := repo.NewRepository(".") 18 | if err != nil { 19 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 20 | os.Exit(1) 21 | } 22 | 23 | hash := args[0] 24 | objPath := r.GitFile("objects", hash[:2], hash[2:]) 25 | data, err := os.ReadFile(objPath) 26 | if err != nil { 27 | fmt.Fprintf(os.Stderr, "Error reading tree %s: %v\n", hash, err) 28 | os.Exit(1) 29 | } 30 | 31 | if !bytes.HasPrefix(data, []byte("tree")) { 32 | fmt.Fprintf(os.Stderr, "Not a tree object: %s\n", hash) 33 | os.Exit(1) 34 | } 35 | 36 | content := data[bytes.IndexByte(data, '\x00')+1:] 37 | tree := objects.ParseTree(content) 38 | for _, entry := range tree.Entries { 39 | fmt.Printf("%s %s %s\t%s\n", entry.Mode, "blob", entry.Hash, entry.Name) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /commands/reverse_parse.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/refs" 5 | "Opengit/repo" 6 | "fmt" 7 | "os" 8 | ) 9 | 10 | func RevParse(args []string) { 11 | if len(args) < 1 { 12 | fmt.Println("Usage: mygit rev-parse ") 13 | os.Exit(1) 14 | } 15 | 16 | r, err := repo.NewRepository(".") 17 | if err != nil { 18 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 19 | os.Exit(1) 20 | } 21 | 22 | refs := refs.NewRefs(r) 23 | name := args[0] 24 | 25 | // Try as a ref first 26 | if hash, err := refs.ReadRef("refs/heads/" + name); err == nil && hash != "" { 27 | fmt.Println(hash) 28 | return 29 | } 30 | if hash, err := refs.ReadRef("refs/tags/" + name); err == nil && hash != "" { 31 | fmt.Println(hash) 32 | return 33 | } 34 | if hash, err := refs.ReadHEAD(); err == nil && name == "HEAD" && hash != "" { 35 | fmt.Println(hash) 36 | return 37 | } 38 | 39 | // Assume it’s a hash 40 | if len(name) == 40 && objectExists(r, name) { 41 | fmt.Println(name) 42 | return 43 | } 44 | 45 | fmt.Fprintf(os.Stderr, "Unknown reference or object: %s\n", name) 46 | os.Exit(1) 47 | } 48 | 49 | func objectExists(r *repo.Repository, hash string) bool { 50 | _, err := r.ReadObject(hash) 51 | return err == nil 52 | } 53 | -------------------------------------------------------------------------------- /commands/rm.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/index" 5 | "Opengit/repo" 6 | "fmt" 7 | "os" 8 | ) 9 | 10 | func Rm(args []string) { 11 | if len(args) < 1 { 12 | fmt.Println("Usage: mygit rm ...") 13 | os.Exit(1) 14 | } 15 | 16 | r, err := repo.NewRepository(".") 17 | if err != nil { 18 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 19 | os.Exit(1) 20 | } 21 | 22 | idx, err := index.ReadIndex(r) 23 | if err != nil { 24 | fmt.Fprintf(os.Stderr, "Error reading index: %v\n", err) 25 | os.Exit(1) 26 | } 27 | 28 | for _, file := range args { 29 | if _, exists := idx.Entries[file]; !exists { 30 | fmt.Fprintf(os.Stderr, "Error: %s not in index\n", file) 31 | continue 32 | } 33 | delete(idx.Entries, file) 34 | } 35 | 36 | err = index.WriteIndex(r, idx) 37 | if err != nil { 38 | fmt.Fprintf(os.Stderr, "Error writing index: %v\n", err) 39 | os.Exit(1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /commands/show_ref.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/refs" 5 | "Opengit/repo" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func ShowRef(args []string) { 12 | r, err := repo.NewRepository(".") 13 | if err != nil { 14 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 15 | os.Exit(1) 16 | } 17 | 18 | refs := refs.NewRefs(r) 19 | for _, dir := range []string{"refs/heads", "refs/tags"} { 20 | filepath.Walk(r.GitFile(dir), func(path string, info os.FileInfo, err error) error { 21 | if err != nil || info.IsDir() { 22 | return nil 23 | } 24 | relPath, _ := filepath.Rel(r.Gitdir, path) 25 | hash, err := refs.ReadRef(relPath) 26 | if err == nil && hash != "" { 27 | fmt.Printf("%s %s\n", hash, relPath) 28 | } 29 | return nil 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /commands/status.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/index" 5 | "Opengit/objects" 6 | "Opengit/refs" 7 | "Opengit/repo" 8 | "bytes" 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | "sync" 13 | ) 14 | 15 | // Helper function to check if a tree contains a file with given path and hash 16 | func treeContains(r *repo.Repository, tree *objects.Tree, path string, hash string) bool { 17 | parts := filepath.SplitList(path) 18 | current := tree 19 | 20 | // Navigate through directories in the path 21 | for i := 0; i < len(parts)-1; i++ { 22 | found := false 23 | for _, entry := range current.Entries { 24 | if entry.Name == parts[i] { 25 | // Read the tree object and continue searching 26 | treeData, err := r.ReadObject(entry.Hash) 27 | if err != nil { 28 | return false 29 | } 30 | current = objects.ParseTree(treeData[bytes.IndexByte(treeData, '\x00')+1:]) 31 | found = true 32 | break 33 | } 34 | } 35 | if !found { 36 | return false 37 | } 38 | } 39 | 40 | // Check the file itself 41 | fileName := parts[len(parts)-1] 42 | for _, entry := range current.Entries { 43 | if entry.Name == fileName && entry.Hash == hash { 44 | return true 45 | } 46 | } 47 | return false 48 | } 49 | 50 | func Status(args []string) { 51 | r, err := repo.NewRepository(".") 52 | if err != nil { 53 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 54 | os.Exit(1) 55 | } 56 | 57 | refs := refs.NewRefs(r) 58 | headHash, err := refs.ReadHEAD() 59 | if err != nil { 60 | fmt.Fprintf(os.Stderr, "Error reading HEAD: %v\n", err) 61 | os.Exit(1) 62 | } 63 | 64 | idx, err := index.ReadIndex(r) 65 | if err != nil { 66 | fmt.Fprintf(os.Stderr, "Error reading index: %v\n", err) 67 | os.Exit(1) 68 | } 69 | 70 | // Load HEAD commit’s tree 71 | var headTree *objects.Tree 72 | if headHash != "" { 73 | commitData, _ := r.ReadObject(headHash) 74 | commit := objects.ParseCommit(commitData[bytes.IndexByte(commitData, '\x00')+1:]) 75 | treeData, _ := r.ReadObject(commit.Tree) 76 | headTree = objects.ParseTree(treeData[bytes.IndexByte(treeData, '\x00')+1:]) 77 | } 78 | 79 | // Compare HEAD tree and index 80 | fmt.Println("Changes to be committed:") 81 | for path, entry := range idx.Entries { 82 | if headTree == nil || !treeContains(r, headTree, path, entry.Hash) { 83 | fmt.Printf(" modified: %s\n", path) 84 | } 85 | } 86 | 87 | // Compare index and working tree with concurrency 88 | fmt.Println("\nChanges not staged for commit:") 89 | var wg sync.WaitGroup 90 | changes := make(chan string, 10) 91 | errChan := make(chan error, 1) 92 | 93 | filepath.Walk(r.Worktree, func(path string, info os.FileInfo, err error) error { 94 | if err != nil || info.IsDir() || path == r.Gitdir { 95 | return nil 96 | } 97 | wg.Add(1) 98 | go func() { 99 | defer wg.Done() 100 | relPath, _ := filepath.Rel(r.Worktree, path) 101 | data, err := os.ReadFile(path) 102 | if err != nil { 103 | errChan <- err 104 | return 105 | } 106 | blob := objects.NewBlob(data) 107 | hash := blob.Hash() 108 | if idxEntry, exists := idx.Entries[relPath]; !exists || idxEntry.Hash != hash { 109 | changes <- fmt.Sprintf(" modified: %s", relPath) 110 | } 111 | }() 112 | return nil 113 | }) 114 | 115 | go func() { 116 | wg.Wait() 117 | close(changes) 118 | close(errChan) 119 | }() 120 | 121 | for change := range changes { 122 | fmt.Println(change) 123 | } 124 | if err := <-errChan; err != nil { 125 | fmt.Fprintf(os.Stderr, "Error scanning working tree: %v\n", err) 126 | os.Exit(1) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /commands/tags.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "Opengit/refs" 5 | "Opengit/repo" 6 | "fmt" 7 | "os" 8 | ) 9 | 10 | func Tag(args []string) { 11 | if len(args) < 1 { 12 | fmt.Println("Usage: mygit tag []") 13 | os.Exit(1) 14 | } 15 | 16 | r, err := repo.NewRepository(".") 17 | if err != nil { 18 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 19 | os.Exit(1) 20 | } 21 | 22 | refs := refs.NewRefs(r) 23 | tagName := args[0] 24 | commitHash := "" 25 | if len(args) > 1 { 26 | commitHash = args[1] 27 | } else { 28 | commitHash, err = refs.ReadHEAD() 29 | if err != nil || commitHash == "" { 30 | fmt.Fprintf(os.Stderr, "Error: no commit to tag\n") 31 | os.Exit(1) 32 | } 33 | } 34 | 35 | if !objectExists(r, commitHash) { 36 | fmt.Fprintf(os.Stderr, "Error: invalid commit %s\n", commitHash) 37 | os.Exit(1) 38 | } 39 | 40 | err = refs.WriteRef("refs/tags/"+tagName, commitHash) 41 | if err != nil { 42 | fmt.Fprintf(os.Stderr, "Error writing tag: %v\n", err) 43 | os.Exit(1) 44 | } 45 | fmt.Printf("Created tag %s pointing to %s\n", tagName, commitHash) 46 | } 47 | -------------------------------------------------------------------------------- /index/index.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "Opengit/repo" 5 | "bytes" 6 | "encoding/binary" 7 | "fmt" 8 | "os" 9 | "time" 10 | ) 11 | 12 | type Entry struct { 13 | Mode string // e.g., "100644" for regular file 14 | Hash string // SHA-1 hash of the blob 15 | ModTime time.Time // Last modification time 16 | FileSize int64 // File size in bytes 17 | } 18 | 19 | type Index struct { 20 | Entries map[string]*Entry 21 | } 22 | 23 | func ReadIndex(r *repo.Repository) (*Index, error) { 24 | indexPath := r.GitFile("index") 25 | data, err := os.ReadFile(indexPath) 26 | if err != nil { 27 | if os.IsNotExist(err) { 28 | return &Index{Entries: make(map[string]*Entry)}, nil 29 | } 30 | return nil, fmt.Errorf("reading index: %v", err) 31 | } 32 | 33 | buf := bytes.NewReader(data) 34 | idx := &Index{Entries: make(map[string]*Entry)} 35 | for buf.Len() > 0 { 36 | var pathLen uint32 37 | binary.Read(buf, binary.BigEndian, &pathLen) 38 | pathBytes := make([]byte, pathLen) 39 | buf.Read(pathBytes) 40 | path := string(pathBytes) 41 | 42 | var mode [6]byte 43 | buf.Read(mode[:]) 44 | hashBytes := make([]byte, 40) 45 | buf.Read(hashBytes) 46 | var mtimeSec, mtimeNsec int64 47 | binary.Read(buf, binary.BigEndian, &mtimeSec) 48 | binary.Read(buf, binary.BigEndian, &mtimeNsec) 49 | var fileSize int64 50 | binary.Read(buf, binary.BigEndian, &fileSize) 51 | 52 | idx.Entries[path] = &Entry{ 53 | Mode: string(mode[:]), 54 | Hash: string(hashBytes), 55 | ModTime: time.Unix(mtimeSec, mtimeNsec), 56 | FileSize: fileSize, 57 | } 58 | } 59 | return idx, nil 60 | } 61 | 62 | func WriteIndex(r *repo.Repository, idx *Index) error { 63 | indexPath := r.GitFile("index") 64 | var buf bytes.Buffer 65 | for path, entry := range idx.Entries { 66 | pathLen := uint32(len(path)) 67 | binary.Write(&buf, binary.BigEndian, pathLen) 68 | buf.WriteString(path) 69 | buf.WriteString(entry.Mode) 70 | buf.WriteString(entry.Hash) 71 | binary.Write(&buf, binary.BigEndian, entry.ModTime.Unix()) 72 | binary.Write(&buf, binary.BigEndian, int64(entry.ModTime.Nanosecond())) 73 | binary.Write(&buf, binary.BigEndian, entry.FileSize) 74 | } 75 | return os.WriteFile(indexPath, buf.Bytes(), 0644) 76 | } 77 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "Opengit/commands" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | if len(os.Args) < 2 { 11 | fmt.Println("Usage: Opengit []") 12 | fmt.Println("\nAvailable commands:") 13 | fmt.Println(" init Create an empty Opengit repository") 14 | fmt.Println(" add Add file contents to the index") 15 | fmt.Println(" rm Remove files from the working tree and index") 16 | fmt.Println(" commit Record changes to the repository") 17 | fmt.Println(" checkout Switch branches or restore working tree files") 18 | fmt.Println(" hash-object Compute object ID and optionally creates a blob from a file") 19 | fmt.Println(" cat-file Provide content of repository objects") 20 | fmt.Println(" ls-tree List the contents of a tree object") 21 | fmt.Println(" ls-files List files in the index") 22 | fmt.Println(" status Show the working tree status") 23 | fmt.Println(" log Show commit logs") 24 | fmt.Println(" tag Create a tag object") 25 | fmt.Println(" show-ref List references") 26 | fmt.Println(" rev-parse Parse revisions") 27 | fmt.Println(" check-ignore Check if files are ignored by .gitignore") 28 | os.Exit(1) 29 | } 30 | 31 | cmd := os.Args[1] 32 | args := os.Args[2:] 33 | 34 | switch cmd { 35 | case "init": 36 | commands.Init(args) 37 | case "add": 38 | commands.Add(args) 39 | case "rm": 40 | commands.Rm(args) 41 | case "commit": 42 | commands.Commit(args) 43 | case "checkout": 44 | commands.Checkout(args) 45 | case "hash-object": 46 | commands.HashObject(args) 47 | case "cat-file": 48 | commands.CatFile(args) 49 | case "ls-tree": 50 | commands.LsTree(args) 51 | case "ls-files": 52 | commands.LsFiles(args) 53 | case "status": 54 | commands.Status(args) 55 | case "log": 56 | commands.Log(args) 57 | case "tag": 58 | commands.Tag(args) 59 | case "show-ref": 60 | commands.ShowRef(args) 61 | case "rev-parse": 62 | commands.RevParse(args) 63 | case "check-ignore": 64 | commands.CheckIgnore(args) 65 | default: 66 | fmt.Printf("Unknown command: %s\n", cmd) 67 | os.Exit(1) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /objects/blob.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | ) 7 | 8 | type Blob struct { 9 | Data []byte 10 | } 11 | 12 | func NewBlob(data []byte) *Blob { 13 | return &Blob{Data: data} 14 | } 15 | 16 | func (b *Blob) Serialize() []byte { 17 | header := fmt.Sprintf("blob %d\x00", len(b.Data)) 18 | return append([]byte(header), b.Data...) 19 | } 20 | 21 | func (b *Blob) Hash() string { 22 | data := b.Serialize() 23 | hash := sha1.Sum(data) 24 | return fmt.Sprintf("%x", hash) 25 | } 26 | -------------------------------------------------------------------------------- /objects/commit.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Commit struct { 11 | Tree string 12 | Parents []string 13 | Author string 14 | Committer string 15 | Message string 16 | Timestamp time.Time 17 | } 18 | 19 | func ParseCommit(data []byte) *Commit { 20 | commit := &Commit{ 21 | Parents: make([]string, 0), 22 | } 23 | 24 | // Split into headers and message 25 | parts := strings.SplitN(string(data), "\n\n", 2) 26 | if len(parts) != 2 { 27 | return commit 28 | } 29 | 30 | headers := strings.Split(parts[0], "\n") 31 | commit.Message = parts[1] 32 | 33 | // Parse headers 34 | for _, header := range headers { 35 | if header == "" { 36 | continue 37 | } 38 | 39 | key := header[:strings.Index(header, " ")] 40 | value := header[strings.Index(header, " ")+1:] 41 | 42 | switch key { 43 | case "tree": 44 | commit.Tree = value 45 | case "parent": 46 | commit.Parents = append(commit.Parents, value) 47 | case "author": 48 | commit.Author = value 49 | case "committer": 50 | commit.Committer = value 51 | } 52 | } 53 | 54 | return commit 55 | } 56 | 57 | func (c *Commit) Serialize() []byte { 58 | // Format: tree \n 59 | // parent \n (optional, multiple) 60 | // author \n 61 | // committer \n 62 | // \n 63 | // 64 | content := fmt.Sprintf("tree %s\n", c.Tree) 65 | 66 | for _, parent := range c.Parents { 67 | content += fmt.Sprintf("parent %s\n", parent) 68 | } 69 | 70 | timestamp := c.Timestamp.Unix() 71 | content += fmt.Sprintf("author %s %d +0000\n", c.Author, timestamp) 72 | content += fmt.Sprintf("committer %s %d +0000\n", c.Committer, timestamp) 73 | content += "\n" + c.Message 74 | 75 | header := fmt.Sprintf("commit %d\x00", len(content)) 76 | return append([]byte(header), []byte(content)...) 77 | } 78 | 79 | func (c *Commit) Hash() string { 80 | data := c.Serialize() 81 | hash := sha1.Sum(data) 82 | return fmt.Sprintf("%x", hash) 83 | } 84 | -------------------------------------------------------------------------------- /objects/tag.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | type Tag struct { 4 | Object string 5 | Type string 6 | Tag string 7 | Tagger string 8 | Message string 9 | } 10 | -------------------------------------------------------------------------------- /objects/tree.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "fmt" 7 | "sort" 8 | ) 9 | 10 | type TreeEntry struct { 11 | Mode string 12 | Name string 13 | Hash string 14 | } 15 | 16 | type Tree struct { 17 | Entries []TreeEntry 18 | } 19 | 20 | func ParseTree(data []byte) *Tree { 21 | tree := &Tree{Entries: make([]TreeEntry, 0)} 22 | pos := 0 23 | 24 | for pos < len(data) { 25 | // Find space after mode 26 | spacePos := pos 27 | for spacePos < len(data) && data[spacePos] != ' ' { 28 | spacePos++ 29 | } 30 | mode := string(data[pos:spacePos]) 31 | 32 | // Find null byte after name 33 | pos = spacePos + 1 34 | nullPos := pos 35 | for nullPos < len(data) && data[nullPos] != 0 { 36 | nullPos++ 37 | } 38 | name := string(data[pos:nullPos]) 39 | 40 | // Read 20 bytes for hash 41 | pos = nullPos + 1 42 | if pos+20 > len(data) { 43 | break 44 | } 45 | hash := hex.EncodeToString(data[pos : pos+20]) 46 | pos += 20 47 | 48 | tree.Entries = append(tree.Entries, TreeEntry{ 49 | Mode: mode, 50 | Name: name, 51 | Hash: hash, 52 | }) 53 | } 54 | 55 | return tree 56 | } 57 | 58 | func (t *Tree) Serialize() []byte { 59 | // Sort entries by name for consistent hashing 60 | sort.Slice(t.Entries, func(i, j int) bool { 61 | return t.Entries[i].Name < t.Entries[j].Name 62 | }) 63 | 64 | // Calculate total size for the header 65 | size := 0 66 | for _, entry := range t.Entries { 67 | // Format: modenamehash(20 bytes) 68 | size += len(entry.Mode) + 1 + len(entry.Name) + 1 + 20 69 | } 70 | 71 | // Create header 72 | header := fmt.Sprintf("tree %d\x00", size) 73 | result := []byte(header) 74 | 75 | // Add entries 76 | for _, entry := range t.Entries { 77 | // Convert hash from hex to bytes 78 | hashBytes := make([]byte, 20) 79 | fmt.Sscanf(entry.Hash, "%x", &hashBytes) 80 | 81 | // Append entry data 82 | result = append(result, []byte(entry.Mode)...) 83 | result = append(result, ' ') 84 | result = append(result, []byte(entry.Name)...) 85 | result = append(result, 0) 86 | result = append(result, hashBytes...) 87 | } 88 | 89 | return result 90 | } 91 | 92 | func (t *Tree) Hash() string { 93 | data := t.Serialize() 94 | hash := sha1.Sum(data) 95 | return fmt.Sprintf("%x", hash) 96 | } 97 | -------------------------------------------------------------------------------- /refs/refs.go: -------------------------------------------------------------------------------- 1 | package refs 2 | 3 | import ( 4 | "Opengit/repo" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | type Refs struct { 12 | r *repo.Repository 13 | } 14 | 15 | func NewRefs(r *repo.Repository) *Refs { 16 | return &Refs{r: r} 17 | } 18 | 19 | func (refs *Refs) ReadRef(ref string) (string, error) { 20 | path := refs.r.GitFile(ref) 21 | data, err := os.ReadFile(path) 22 | if err != nil { 23 | if os.IsNotExist(err) { 24 | return "", nil // Ref doesn’t exist 25 | } 26 | return "", fmt.Errorf("reading ref %s: %v", ref, err) 27 | } 28 | return strings.TrimSpace(string(data)), nil 29 | } 30 | 31 | func (refs *Refs) WriteRef(ref, value string) error { 32 | path := refs.r.GitFile(ref) 33 | err := os.MkdirAll(filepath.Dir(path), 0755) 34 | if err != nil { 35 | return fmt.Errorf("creating ref dir: %v", err) 36 | } 37 | return os.WriteFile(path, []byte(value+"\n"), 0644) 38 | } 39 | 40 | func (refs *Refs) ReadHEAD() (string, error) { 41 | head, err := refs.ReadRef("HEAD") 42 | if err != nil { 43 | return "", err 44 | } 45 | if strings.HasPrefix(head, "ref: ") { 46 | return refs.ReadRef(strings.TrimPrefix(head, "ref: ")) 47 | } 48 | return head, nil // Direct SHA-1 (detached HEAD) 49 | } 50 | 51 | func (refs *Refs) WriteHEAD(value string) error { 52 | return refs.WriteRef("HEAD", value) 53 | } 54 | -------------------------------------------------------------------------------- /repo/repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | type Repository struct { 12 | Worktree string // Path to the working directory 13 | Gitdir string // Path to the .git directory 14 | } 15 | 16 | func NewRepository(path string) (*Repository, error) { 17 | gitdir := filepath.Join(path, ".opengit") 18 | info, err := os.Stat(gitdir) 19 | if err != nil { 20 | if os.IsNotExist(err) { 21 | return nil, fmt.Errorf(".opengit directory not found") 22 | } 23 | return nil, fmt.Errorf("accessing .opengit directory: %v", err) 24 | } 25 | if !info.IsDir() { 26 | return nil, fmt.Errorf(".opengit is not a directory") 27 | } 28 | return &Repository{ 29 | Worktree: path, 30 | Gitdir: gitdir, 31 | }, nil 32 | } 33 | 34 | func (r *Repository) GitFile(parts ...string) string { 35 | return filepath.Join(append([]string{r.Gitdir}, parts...)...) 36 | } 37 | 38 | func (r *Repository) WriteObject(data []byte, hash string) error { 39 | var buf bytes.Buffer 40 | w, err := zlib.NewWriterLevel(&buf, zlib.BestCompression) 41 | if err != nil { 42 | return fmt.Errorf("creating zlib writer: %v", err) 43 | } 44 | _, err = w.Write(data) 45 | w.Close() 46 | if err != nil { 47 | return fmt.Errorf("compressing data: %v", err) 48 | } 49 | 50 | objPath := r.GitFile("objects", hash[:2], hash[2:]) 51 | err = os.MkdirAll(filepath.Dir(objPath), 0755) 52 | if err != nil { 53 | return fmt.Errorf("creating object dir: %v", err) 54 | } 55 | return os.WriteFile(objPath, buf.Bytes(), 0644) 56 | } 57 | 58 | func (r *Repository) ReadObject(hash string) ([]byte, error) { 59 | objPath := r.GitFile("objects", hash[:2], hash[2:]) 60 | compressed, err := os.ReadFile(objPath) 61 | if err != nil { 62 | return nil, fmt.Errorf("reading object %s: %v", hash, err) 63 | } 64 | 65 | rdr, err := zlib.NewReader(bytes.NewReader(compressed)) 66 | if err != nil { 67 | return nil, fmt.Errorf("decompressing object %s: %v", hash, err) 68 | } 69 | defer rdr.Close() 70 | 71 | var buf bytes.Buffer 72 | _, err = buf.ReadFrom(rdr) 73 | if err != nil { 74 | return nil, fmt.Errorf("reading decompressed data: %v", err) 75 | } 76 | return buf.Bytes(), nil 77 | } 78 | --------------------------------------------------------------------------------