Prism VCS > Tree [f4699a2]
/main.go/
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* This Source Code Form is “Incompatible With Secondary Licenses”, as
* defined by the Mozilla Public License, v. 2.0. */
package main
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/gammazero/deque"
"github.com/klauspost/compress/zstd"
)
func FindVCSDirectory() string {
curDir, _ := os.Getwd()
homeDir, _ := os.UserHomeDir()
limit := 6 // Only allow to go up 6 levels of directories
if curDir == homeDir {
// If current directory is home directory, don't allow walking further up the filesystem
limit = 0
}
checkPath := curDir
result := path.Join(checkPath, ".prism")
_, err := os.Stat(result)
for errors.Is(err, fs.ErrNotExist) {
checkPath = path.Join(checkPath, "..")
limit--
if limit == 0 || (checkPath == homeDir && curDir != homeDir) || checkPath == "/" || (len(checkPath) >= 3 && checkPath[1] == ':' && checkPath[2] == '/') {
// Limit to going up 6 levels of directories, always stop at the home directory as long as it's not the current directory, and always stop at the root directory
result = ""
break
}
result = path.Join(checkPath, ".prism")
_, err = os.Stat(result)
}
return result
}
func main() {
if len(os.Args) == 1 {
fmt.Printf("Usage:\n")
fmt.Printf(" init [directory] Initialize a new repository in the current or specified directory.\n")
fmt.Printf(" status [path] Show the status for entire worktree, or a given directory.\n")
fmt.Printf(" stage Add a file or directory to staging.\n")
fmt.Printf(" unstage Remove a file or directory from staging. Does not delete form working directory.\n")
fmt.Printf(" commit Record staged files to the repository.\n")
fmt.Printf(" log [path] Show commit history for all changes or for a specific file.\n")
fmt.Printf(" diff Show changes between last commit and current staged changes.\n") // TODO
fmt.Printf(" about Show prism software license.\n")
fmt.Printf("\n")
fmt.Printf(" cat-file Display contents of a repo object.\n")
fmt.Printf(" check Check repository for integrity.\n")
fmt.Printf(" orphans Show all orphaned objects.\n")
fmt.Printf(" prune Remove orphaned objects.\n")
fmt.Printf(" edit-last-commit [msg] Edit the message of your last commit.\n")
// fmt.Printf(" find-commits-with-tree-hash Find commits that have the given tree hash.\n")
// fmt.Printf(" lineage Show lineage of commits (not yet implemented).\n")
// fmt.Printf(" rewrite-history Rewrite repository history with improved metadata.\n")
return
}
command := os.Args[1]
switch command {
case "init":
initCmd()
case "status":
directory := ""
if len(os.Args) >= 3 {
directory = os.Args[2]
}
statusCmd(directory)
case "stage", "add":
stageCmd()
case "unstage":
unstageCmd()
case "commit":
commitCmd()
case "lineage":
fmt.Printf("Lineages are not yet implemented.\n")
case "log":
logCmd()
case "diff":
diffCmd()
case "about":
aboutCmd()
case "cat-file":
catFileCmd()
case "edit-last-commit":
fixLastCommitCmd()
case "orphans":
OrphanedObjects(false)
case "prune":
OrphanedObjects(true)
case "rewrite-history":
rewriteHistoryCmd()
case "find-commits-with-tree-hash":
FindCommitsWithTreeHash(os.Args[2])
case "check":
if len(os.Args) >= 3 && (os.Args[2] == "--verbose" || os.Args[2] == "-v") {
CheckRepoCmd(true)
} else {
CheckRepoCmd(false)
}
default:
fmt.Printf("Unknown command.\n")
}
}
func aboutCmd() {
fmt.Printf("prism - Version control through a clearer lens\n")
fmt.Printf("Version: 0.1.0\n")
fmt.Printf("Copyright (c) 2025 Christian Lee Seibold\n\n")
fmt.Printf(`BSD 3-Clause License
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
`)
}
func initCmd() {
vcsDirectory := FindVCSDirectory()
// Set up VCS directories
if vcsDirectory == "" {
initDirectory, _ := os.Getwd()
if len(os.Args) > 1 {
initDirectory = path.Clean(os.Args[2])
}
entries, err := os.ReadDir(initDirectory)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defaultBranchName := "main"
if info, err := os.Stat(path.Join(initDirectory, defaultBranchName)); err == nil && info.IsDir() {
// Check if directory is empty
entries, _ := os.ReadDir(path.Join(initDirectory, defaultBranchName))
if len(entries) > 0 {
fmt.Printf("Error: Directory '%s' is not empty and conflicts with naming of default worktree. Cannot initialize repository.\n", path.Join(initDirectory, defaultBranchName))
return
}
}
// Create main VCS directories
vcsDirectory = path.Join(initDirectory, ".prism")
os.Mkdir(vcsDirectory, 0755)
os.Mkdir(path.Join(vcsDirectory, "objects"), 0755)
os.Mkdir(path.Join(vcsDirectory, "refs"), 0755)
os.Mkdir(path.Join(vcsDirectory, "refs/branches"), 0755)
// Checkout the main branch into a worktree
os.Mkdir(path.Join(initDirectory, defaultBranchName), 0755)
os.Mkdir(path.Join(vcsDirectory, "worktrees"), 0755)
os.Mkdir(path.Join(vcsDirectory, "worktrees", defaultBranchName), 0755)
os.WriteFile(path.Join(vcsDirectory, "worktrees", defaultBranchName, "staging"), []byte(""), 0644)
os.WriteFile(path.Join(vcsDirectory, "worktrees", defaultBranchName, "head"), []byte("ref: refs/branches/"+defaultBranchName+"\n"), 0644)
fmt.Printf("Initialized repository with '%s' branch. Worktree created at '%s/'.\n", defaultBranchName, path.Join(initDirectory, defaultBranchName))
if len(entries) > 0 {
fmt.Printf("There are files in the current directory. Do you want to move them into the worktree? [Y/N] ")
moveFilesIntoWorktree := false
reader := bufio.NewReader(os.Stdin)
for {
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
if input == "Y" || input == "y" {
moveFilesIntoWorktree = true
break
} else if input == "N" || input == "n" {
break
}
fmt.Printf("Please enter Y or N: ")
}
if moveFilesIntoWorktree {
for _, entry := range entries {
if entry.Name() == ".prism" {
continue
}
os.Rename(entry.Name(), path.Join(initDirectory, defaultBranchName, entry.Name()))
}
fmt.Printf("Files have been moved into the worktree.\n")
}
}
} else {
fmt.Printf("Already inside a repository.\n")
}
}
// Can be given an optional directory in which it will only output the status for that directory
func statusCmd(directory string) {
vcsDirectory := FindVCSDirectory()
currentWorktree := OpenCurrentWorktree(vcsDirectory)
defer currentWorktree.Close()
if vcsDirectory == "" {
fmt.Printf("Must init repository first.\n")
return
} else if currentWorktree == nil {
fmt.Printf("Not in worktree.\n")
return
}
if directory == "" {
directory = currentWorktree.WorkingDirectoryPath()
}
// Make directory an absolute path. Get the path relative to the worktree's path for use in the staging index entries map.
directory, _ = filepath.Abs(directory)
relativeDirectory, _ := filepath.Rel(currentWorktree.WorkingDirectoryPath(), directory)
relativeDirectory = filepath.ToSlash(relativeDirectory)
// Print staging area
idx := currentWorktree.ReadIndex()
currentBranch := currentWorktree.GetCurrentBranch()
fmt.Printf("Status for '%s' worktree. On branch '%s'.\n", currentWorktree.Name, currentBranch)
for _, entry := range idx.IterateStaged() {
fmt.Printf(" \033[32mstaged: \033[39m%s\n", entry.name)
}
// Print stuff that needs to be staged by checking if ModTime has changed, and if so, then checking if hash has changed.
fmt.Printf("\nChanges not staged:\n")
entries, _ := os.ReadDir(directory)
for _, e := range entries {
info, _ := e.Info()
if e.Name() == ".prism" || e.IsDir() {
continue
}
// Check if already in index, and if so, if modtime has changed
entry, exists := idx[path.Join(relativeDirectory, e.Name())]
if exists && entry.modtime.Before(info.ModTime().Truncate(time.Second)) {
// Check if hash has changed
file_contents, err := os.ReadFile(filepath.Join(directory, e.Name()))
if err != nil {
continue
}
header := fmt.Sprintf("blob %d\000", len(file_contents))
obj_buf := bytes.NewBuffer(make([]byte, 0, len(header)+len(file_contents)))
obj_buf.WriteString(header)
obj_buf.Write(file_contents)
obj_hash := SumHash(obj_buf.Bytes())
if entry.hash != obj_hash {
fmt.Printf(" \033[33mmodified: \033[39m%s\n", path.Join(relativeDirectory, e.Name()))
} else {
// TODO: If hash has not changed, then update the modtime of the staging index entry?
}
} else if !exists {
fmt.Printf(" \033[31mnew: \033[39m%s\n", path.Join(relativeDirectory, e.Name()))
}
}
}
func stageCmd() {
if len(os.Args) < 3 || os.Args[2] == "" {
fmt.Printf("Must specify a path.\n")
return
}
vcsDirectory := FindVCSDirectory()
currentWorktree := OpenCurrentWorktree(vcsDirectory)
defer currentWorktree.Close()
if vcsDirectory == "" {
fmt.Printf("Must init repository first.\n")
return
} else if currentWorktree == nil {
fmt.Printf("Not in worktree.\n")
return
}
// Stage a file or directory
givenPath := os.Args[2]
if strings.HasSuffix(givenPath, ".prism") {
fmt.Printf("Cannot stage .prism directory.\n")
return
}
// Make directory an absolute path. Get the path relative to the worktree's path for use in the staging index entries map.
givenPath, _ = filepath.Abs(givenPath)
relativePath, _ := filepath.Rel(currentWorktree.WorkingDirectoryPath(), givenPath)
relativePath = filepath.ToSlash(relativePath)
currentWorktree.WriteLock()
defer currentWorktree.WriteUnlock()
stat, err := os.Stat(givenPath)
if errors.Is(err, fs.ErrNotExist) {
fmt.Printf("Error: File does not exist.\n")
return
}
if stat.IsDir() {
// Walk the directory to stage every file and sub-directory.
} else {
// Add object to objects folder, and compress it
file_contents, _ := os.ReadFile(givenPath)
header := fmt.Sprintf("blob %d\000", len(file_contents))
obj_buf := bytes.NewBuffer(make([]byte, 0, len(header)+len(file_contents)))
obj_buf.WriteString(header)
obj_buf.Write(file_contents)
obj_hash := SumHash(obj_buf.Bytes())
var buf bytes.Buffer
zw, _ := zstd.NewWriter(&buf, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
zw.Write(obj_buf.Bytes())
zw.Close()
// Add to or update in staging index
idx := currentWorktree.ReadIndex()
status := "s"
entry, exists := idx[relativePath]
if exists && entry.hash == obj_hash {
// The file was already in the index and has already been staged. No need to write the file. Change status to "c".
status = "c"
} else if exists && entry.status == "s" {
// If a previous version was already staged, but not comitted, then remove that old object
os.WriteFile(path.Join(currentWorktree.vcsDirectory, "objects", obj_hash.HexString()), buf.Bytes(), 0644)
os.Remove(path.Join(currentWorktree.vcsDirectory, "objects", entry.hash.HexString()))
} else {
// Otherwise, write the new object, and keep the status as staged.
os.WriteFile(path.Join(currentWorktree.vcsDirectory, "objects", obj_hash.HexString()), buf.Bytes(), 0644)
}
isExecutable := false
if (stat.Mode().Perm() & 0100) != 0 {
isExecutable = true
}
if isExecutable {
idx[relativePath] = IndexEntry{status, "100755", relativePath, obj_hash, stat.ModTime().Truncate(time.Second)}
} else {
idx[relativePath] = IndexEntry{status, "100644", relativePath, obj_hash, stat.ModTime().Truncate(time.Second)}
}
currentWorktree.WriteIndex(idx)
}
}
func unstageCmd() {
if len(os.Args) < 3 || os.Args[2] == "" {
fmt.Printf("Must specify a path.\n")
return
}
vcsDirectory := FindVCSDirectory()
currentWorktree := OpenCurrentWorktree(vcsDirectory)
defer currentWorktree.Close()
if vcsDirectory == "" {
fmt.Printf("Must init repository first.\n")
return
} else if currentWorktree == nil {
fmt.Printf("Not in worktree.\n")
return
}
// Unstage a file or directory, removing it from the staging index.
givenPath := os.Args[2]
if givenPath == ".prism" {
return
}
// Make directory an absolute path. Get the path relative to the worktree's path for use in the staging index entries map.
givenPath, _ = filepath.Abs(givenPath)
relativePath, _ := filepath.Rel(currentWorktree.WorkingDirectoryPath(), givenPath)
relativePath = filepath.ToSlash(relativePath)
if relativePath == "" || relativePath == "." {
fmt.Printf("Cannot unstage entire worktree. Please unstage each file/folder directly.\n")
return
}
currentWorktree.WriteLock()
defer currentWorktree.WriteUnlock()
stat, err := os.Stat(givenPath)
if errors.Is(err, fs.ErrNotExist) {
fmt.Printf("Error: File does not exist.\n")
return
}
if stat.IsDir() {
// Walk the directory to stage every file and sub-directory.
} else {
idx := currentWorktree.ReadIndex()
if entry, exists := idx[relativePath]; !exists || entry.status != "s" {
fmt.Printf("Cannot unstage a file that has not been staged yet.\n")
return
}
objHashToDelete := idx[relativePath].hash
// Check if relativePath is in the head commit.
headFile := currentWorktree.ReadHead()
_, head, _ := strings.Cut(strings.TrimSpace(headFile), " ")
headCommitStr, _ := os.ReadFile(filepath.Join(vcsDirectory, head))
headCommitHash := ParseHashHex(strings.TrimSpace(string(headCommitStr)))
headCommit := DecodeCommit(vcsDirectory, headCommitHash)
headTree := DecodeTree(vcsDirectory, headCommit.TreeHash)
entry := headTree.FindEntryName(relativePath)
if entry == (TreeEntry{}) {
// If entry not found in last commit, just do the unstaging by removing the object from the index
delete(idx, relativePath)
} else {
// If found in last commit, revert the index entry to that of the last commit
if entry.t == "blob" {
idx[relativePath] = IndexEntry{"c", "100644", relativePath, entry.hash, headCommit.Date}
} else if entry.t == "tree" {
// TODO
}
}
currentWorktree.WriteIndex(idx)
// Delete the unstaged object, but only if it's not the same hash as that in the last commit.
if entry.hash != objHashToDelete {
os.Remove(filepath.Join(vcsDirectory, "objects", objHashToDelete.HexString()))
}
fmt.Printf("File '%s' has been unstaged.\n", relativePath)
}
}
func catFileCmd() {
vcsDirectory := FindVCSDirectory()
if vcsDirectory == "" {
fmt.Printf("Must init repository first.\n")
return
}
hash := ParseHashHex(os.Args[2])
file_contents, _ := os.ReadFile(path.Join(vcsDirectory, "objects", hash.HexString()))
zr, _ := zstd.NewReader(bytes.NewReader(file_contents))
result := new(bytes.Buffer)
result.ReadFrom(zr)
zr.Close()
header, content, _ := strings.Cut(result.String(), "\000")
fmt.Printf("Header: %s\n", header)
if strings.HasPrefix(header, "tree") {
// Pretty print tree
treeObj := DecodeTree(vcsDirectory, hash) // TODO: Loads the file again.
for _, e := range treeObj.entries {
fmt.Printf("%s %s %s\n", e.t, e.hash.HexString(), e.name)
}
} else {
//blob := DecodeBlob(vcsDirectory, hash)
fmt.Printf("%s\n", content)
}
}
func commitCmd() {
commitTime := time.Now()
vcsDirectory := FindVCSDirectory()
currentWorktree := OpenCurrentWorktree(vcsDirectory)
defer currentWorktree.Close()
if vcsDirectory == "" {
fmt.Printf("Must init repository first.\n")
return
} else if currentWorktree == nil {
fmt.Printf("Not in worktree.\n")
return
}
author := "Christian Lee Seibold "
comitter := author
idx := currentWorktree.ReadIndex()
if idx.CountStaged() == 0 {
fmt.Printf("Nothing has been staged.\n")
return
}
// Message provided on cli
commitMessage := ""
if len(os.Args) >= 3 {
commitMessage = strings.Join(os.Args[2:], " ")
} else {
// Otherwise, create a file for the commit message and open it in the editor, waiting for the editor to close (or it to close the file)
msgfile := path.Join(currentWorktree.WorktreeSettingsPath(), "COMMIT_EDITMSG")
os.WriteFile(msgfile, []byte{}, 0644)
editor := os.Getenv("EDITOR")
editor_parts := strings.Split(editor, " ")
if len(editor) == 0 {
editor = "/usr/bin/editor"
}
editor_parts = append(editor_parts, msgfile)
cmd := exec.Command(editor_parts[0], editor_parts[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
msgBytes, _ := os.ReadFile(msgfile)
commitMessage = strings.TrimSpace(string(msgBytes))
}
if commitMessage == "" {
fmt.Printf("Canceling commit. No message provided.")
return
}
currentWorktree.Commit(author, comitter, commitMessage, commitTime)
}
// Prints commit history of project, or a given file/directory path.
func logCmd() {
vcsDirectory := FindVCSDirectory()
currentWorktree := OpenCurrentWorktree(vcsDirectory)
defer currentWorktree.Close()
if vcsDirectory == "" {
fmt.Printf("Must init repository first.\n")
return
} else if currentWorktree == nil {
fmt.Printf("Not in worktree.\n")
return
}
// Check if we've been given a path. If so, we will only print commits that have modified this path (given the current state of the worktree).
// TODO: If the current state of the worktree has introduced an entirely new file with the same pathname as a previous file that was deleted,
// it will/should take that into account (i.e., we keep track of renames, moves, deletions, and additions of files). This means we must
// first read the staging index to see what file we are actually talking about and how it relates to the project's history. We will also
// stop at the commit that is an ancestor to the worktree's current head and at which the filepath was first added.
givenPath := ""
relativePath := ""
if len(os.Args) > 2 {
givenPath = os.Args[2]
// Make givenPath an absolute path. Get the path relative to the worktree's path.
givenPath, _ = filepath.Abs(givenPath)
relativePath, _ = filepath.Rel(currentWorktree.WorkingDirectoryPath(), givenPath)
relativePath = filepath.ToSlash(relativePath)
fmt.Printf("Printing history for file '%s'.\n\n", relativePath)
}
// Get the head file, get the branch ref, and print the commit messages
headFile := currentWorktree.ReadHead()
_, head, _ := strings.Cut(strings.TrimSpace(string(headFile)), " ")
if _, err := os.Stat(path.Join(vcsDirectory, head)); errors.Is(err, fs.ErrNotExist) {
_, branchName := path.Split(head)
fmt.Printf("Your current branch '%s' does not have any commits yet.\n", branchName)
return
}
// Get commit from the current head
contents, _ := os.ReadFile(path.Join(vcsDirectory, head))
currentCommitHash := ParseHashHex(strings.TrimSpace(string(contents)))
commit := DecodeCommit(vcsDirectory, currentCommitHash)
commitTree := DecodeTree(vcsDirectory, commit.TreeHash)
for {
parentCommit := DecodeCommit(vcsDirectory, commit.ParentHash)
parentCommitTree := DecodeTree(vcsDirectory, parentCommit.TreeHash)
printCommit := false
// Check if the givenPath has been changed in commit compared to parent commit by checking if the hash has changed. If so, print the commit.
if givenPath != "" && !parentCommit.Hash.IsEmpty() {
commitEntry := commitTree.FindEntryName(relativePath) // TODO: This doesn't work with nested trees yet!
parentCommitEntry := parentCommitTree.FindEntryName(relativePath)
if commitEntry.hash.IsEmpty() {
// If our current working file was added after this commit, then all previous commits with
// this file name are not the same file. So completely break from here.
break
} else if parentCommitEntry.hash.IsEmpty() == !commitEntry.hash.IsEmpty() {
// The file was added or deleted in this commit.
printCommit = true
} else if parentCommitEntry.hash != commitEntry.hash {
// The file's contents was changed. // TODO: Make this work with renames and moves.
printCommit = true
} else if parentCommitEntry.hash == commitEntry.hash {
// If no change in file, skip.
}
} else {
printCommit = true
}
if printCommit {
if (commit.Date != time.Time{}) {
fmt.Printf("\033[38;5;184mcommit %s\033[39m\033[49m\nAuthor: %s\nDate: %s\n\n%s\n", commit.Hash.HexString(), commit.Author, commit.Date.Format(time.RFC1123Z), commit.Message)
} else {
fmt.Printf("\033[38;5;184mcommit %s\033[39m\033[49m\nAuthor: %s\n\n%s\n", commit.Hash.HexString(), commit.Author, commit.Message)
}
if !strings.HasSuffix(commit.Message, "\n") {
fmt.Printf("\n")
}
}
if parentCommit.Hash.IsEmpty() {
break
}
commit = parentCommit
commitTree = parentCommitTree
}
}
func diffCmd() {
vcsDirectory := FindVCSDirectory()
currentWorktree := OpenCurrentWorktree(vcsDirectory)
defer currentWorktree.Close()
if vcsDirectory == "" {
fmt.Printf("Must init repository first.\n")
return
} else if currentWorktree == nil {
fmt.Printf("Not in worktree.\n")
return
}
// Get the head file, get the branch ref, and print the commit messages
headFile := currentWorktree.ReadHead()
_, head, _ := strings.Cut(strings.TrimSpace(string(headFile)), " ")
if _, err := os.Stat(path.Join(vcsDirectory, head)); errors.Is(err, fs.ErrNotExist) {
_, branchName := path.Split(head)
fmt.Printf("Your current branch '%s' does not have any commits yet.\n", branchName)
return
}
// Get Index and Commit from the current head
idx := currentWorktree.ReadIndex()
contents, _ := os.ReadFile(path.Join(vcsDirectory, head))
headCommitHash := ParseHashHex(strings.TrimSpace(string(contents)))
headCommit := DecodeCommit(vcsDirectory, headCommitHash)
headCommitTree := DecodeTree(vcsDirectory, headCommit.TreeHash)
// Go through each staged file of index.
// TODO: Make this track file renames and moves.
objectStore := ObjectStore{vcsDirectory: vcsDirectory}
for _, stagedEntry := range idx.IterateStaged() {
headEntry := headCommitTree.FindEntryName(stagedEntry.name)
// If the entry doesn't exist in the head commit, then it's a new file
if headEntry.hash.IsEmpty() {
fmt.Printf("diff -u a/null b/%s\n", stagedEntry.name)
fmt.Printf("new file mode %s\n", stagedEntry.mode)
fmt.Printf("--- /dev/null\n")
fmt.Printf("+++ b/%s\n", stagedEntry.name)
// Create a temporary file with the staged content
stageTempFile, err := os.CreateTemp("", "staged-*-"+stagedEntry.name)
if err != nil {
fmt.Printf("Error creating temp file: %v\n", err)
continue
}
stageReader, err := objectStore.GetObject(stagedEntry.hash)
if err != nil {
fmt.Printf("Error reading staged file: %v\n", err)
continue
}
io.Copy(stageTempFile, stageReader)
stageTempFile.Sync()
stageTempFile.Close()
stageReader.Close()
// Run diff to show changes
// Check if GIT_EXTERNAL_DIFF environment variable is set
externalDiff := os.Getenv("GIT_EXTERNAL_DIFF")
if externalDiff != "" {
// Use the external diff tool
cmd := exec.Command(externalDiff, "/dev/null", stageTempFile.Name())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
} else {
// Fall back to regular diff command
cmd := exec.Command("diff", "--color", "--ignore-trailing-space", "-u", "/dev/null", stageTempFile.Name())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
}
// Remove Temporary Files
os.Remove(filepath.Join(os.TempDir(), stageTempFile.Name()))
} else if headEntry.hash != stagedEntry.hash {
// The file was modified
fmt.Printf("diff -u a/%s b/%s\n", stagedEntry.name, stagedEntry.name)
fmt.Printf("--- a/%s\n", stagedEntry.name)
fmt.Printf("+++ b/%s\n", stagedEntry.name)
// Create temporary files for both versions
headTempFile, err := os.CreateTemp("", "head-*-"+stagedEntry.name)
if err != nil {
fmt.Printf("Error creating temp file: %v\n", err)
continue
}
stageTempFile, err := os.CreateTemp("", "staged-*-"+stagedEntry.name)
if err != nil {
fmt.Printf("Error creating temp file: %v\n", err)
continue
}
// Write head content to temp file
headReader, err := objectStore.GetObject(headEntry.hash)
if err != nil {
fmt.Printf("Error reading head file: %v\n", err)
continue
}
io.Copy(headTempFile, headReader)
headTempFile.Sync()
headTempFile.Close()
headReader.Close()
// Write staged content to temp file
stageReader, err := objectStore.GetObject(stagedEntry.hash)
if err != nil {
fmt.Printf("Error reading staged file: %v\n", err)
continue
}
io.Copy(stageTempFile, stageReader)
stageTempFile.Sync()
stageTempFile.Close()
stageReader.Close()
// Run diff to show changes
// Check if GIT_EXTERNAL_DIFF environment variable is set
externalDiff := os.Getenv("GIT_EXTERNAL_DIFF")
if externalDiff != "" {
// Use the external diff tool
cmd := exec.Command(externalDiff, headTempFile.Name(), stageTempFile.Name())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
} else {
// Fall back to regular diff command
cmd := exec.Command("diff", "--color", "--ignore-trailing-space", "-u", headTempFile.Name(), stageTempFile.Name())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
}
// Remove temporary files
os.Remove(headTempFile.Name())
os.Remove(stageTempFile.Name())
}
}
}
func fixLastCommitCmd() {
vcsDirectory := FindVCSDirectory()
currentWorktree := OpenCurrentWorktree(vcsDirectory)
defer currentWorktree.Close()
// Allows you to fix the message of the last commit you made. This only works if HEAD points to the last commit on the branch, the commit you want to edit.
if vcsDirectory == "" {
fmt.Printf("Must init repository first.\n")
return
} else if currentWorktree == nil {
fmt.Printf("Not in worktree.\n")
return
}
// Get the head file, get the branch ref, and print the commit messages
currentWorktree.WriteLock()
defer currentWorktree.WriteUnlock()
headFile := currentWorktree.ReadHead()
_, head, _ := strings.Cut(strings.TrimSpace(string(headFile)), " ")
if _, err := os.Stat(path.Join(vcsDirectory, head)); errors.Is(err, fs.ErrNotExist) {
_, branchName := path.Split(head)
fmt.Printf("Your current branch '%s' does not have any commits yet.\n", branchName)
return
}
// Make sure HEAD points to a branch ref
if !strings.HasPrefix(head, "refs/branches/") {
fmt.Printf("You can only fix the last commit of a branch. HEAD must be pointing to a branch.\n")
return
}
contents, _ := os.ReadFile(path.Join(vcsDirectory, head))
oldCommitHash := ParseHashHex(strings.TrimSpace(string(contents)))
commit := DecodeCommit(vcsDirectory, oldCommitHash)
commitMessage := ""
if len(os.Args) >= 3 {
commitMessage = strings.Join(os.Args[2:], " ")
} else {
// Otherwise, create a file for the commit message and open it in the editor, waiting for the editor to close (or it to close the file)
msgfile := path.Join(currentWorktree.WorktreeSettingsPath(), "COMMIT_EDITMSG")
os.WriteFile(msgfile, []byte(commit.Message), 0644)
editor := os.Getenv("EDITOR")
editor_parts := strings.Split(editor, " ")
if len(editor) == 0 {
editor = "/usr/bin/editor"
}
editor_parts = append(editor_parts, msgfile)
cmd := exec.Command(editor_parts[0], editor_parts[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
fmt.Printf("Error: %s\n", err.Error())
return
}
msgBytes, _ := os.ReadFile(msgfile)
commitMessage = strings.TrimSpace(string(msgBytes))
}
commit.Message = commitMessage
commit_contents := commit.ToString()
header := fmt.Sprintf("commit %d\000", len(commit_contents))
obj_buf := bytes.NewBuffer(make([]byte, 0, len(header)+len([]byte(commit_contents))))
obj_buf.WriteString(header)
obj_buf.WriteString(commit_contents)
obj_hash := SumHash(obj_buf.Bytes())
commit.Hash = obj_hash
var buf bytes.Buffer
zw, _ := zstd.NewWriter(&buf, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
zw.Write(obj_buf.Bytes())
zw.Close()
os.WriteFile(path.Join(currentWorktree.vcsDirectory, "objects", obj_hash.HexString()), buf.Bytes(), 0644)
// Remove old commit object
os.Remove(path.Join(currentWorktree.vcsDirectory, "objects", oldCommitHash.HexString()))
// Set the branch to the new commit hash.
os.WriteFile(path.Join(currentWorktree.vcsDirectory, head), []byte(obj_hash.HexString()), 0644)
}
// caseInsensitiveStringCompare compares two strings without allocating.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b.
func caseInsensitiveStringCompare(a, b string) int {
for {
if len(b) == 0 && len(a) == 0 {
return 0 // Same length and same runes
} else if len(b) == 0 {
return 1 // a is bigger (has more runes)
} else if len(a) == 0 {
return -1 // b is bigger (has more runes)
}
c, sizec := utf8.DecodeRuneInString(a)
d, sized := utf8.DecodeRuneInString(b)
lowerc := unicode.ToLower(c)
lowerd := unicode.ToLower(d)
if lowerc < lowerd {
return -1
} else if lowerc > lowerd {
return 1
}
a = a[sizec:]
b = b[sized:]
}
}
func rewriteHistoryCmd() {
yesterday := time.Now().Add(-24 * time.Hour)
dontDelete := make(map[string]bool)
vcsDirectory := FindVCSDirectory()
currentWorktree := OpenCurrentWorktree(vcsDirectory)
defer currentWorktree.Close()
currentWorktree.WriteLock()
defer currentWorktree.WriteUnlock()
// Add all objects in current staging index to dontDelete map
idx := currentWorktree.ReadIndex()
for _, entry := range idx {
dontDelete[entry.hash.HexString()] = true
}
// Get the main branch and go down the line to collect all commits
deque := deque.Deque[CommitObject]{}
headFile := currentWorktree.ReadHead()
_, head, _ := strings.Cut(strings.TrimSpace(string(headFile)), " ")
contents, _ := os.ReadFile(filepath.Join(vcsDirectory, head))
headCommitHash := ParseHashHex(strings.TrimSpace(string(contents)))
headCommit := DecodeCommit(vcsDirectory, headCommitHash)
deque.PushBack(headCommit)
currentCommit := headCommit
for !currentCommit.ParentHash.IsEmpty() {
currentCommit = DecodeCommit(vcsDirectory, currentCommit.ParentHash)
deque.PushBack(currentCommit)
}
// Go through the length of the deque, get each commit using deque.PopBack()
objectStore := ObjectStore{vcsDirectory: vcsDirectory}
parentHash := Hash{}
parentCommit := CommitObject{}
for range deque.Len() {
commit := deque.PopBack()
// Get the commit's tree and convert to new format
tree := DecodeTree(vcsDirectory, commit.TreeHash)
newTreeObjBytes := tree.Encode()
treeHeader := fmt.Sprintf("tree %d\000", len(newTreeObjBytes))
newBuf := bytes.NewBuffer(make([]byte, 0, len(treeHeader)+len(newTreeObjBytes)))
newBuf.WriteString(treeHeader)
newBuf.Write(newTreeObjBytes)
newTreeHash := SumHash(newBuf.Bytes())
err := objectStore.WriteObject(newTreeHash, bytes.NewReader(newBuf.Bytes()))
if err != nil && !errors.Is(err, ErrObjectExists) {
panic(err)
} else if errors.Is(err, ErrObjectExists) && newTreeHash == parentCommit.TreeHash && dontDelete[newTreeHash.HexString()] == true {
fmt.Printf("Found commit with tree %s same as direct parent. Removing commit (old hash %s) from history.\n", newTreeHash.HexString(), commit.Hash.HexString())
continue
}
dontDelete[newTreeHash.HexString()] = true
// Go through every entry in tree and add their hashes to the dontDelete map
for _, e := range tree.entries {
dontDelete[e.hash.HexString()] = true
}
// If commit doesn't have a date, give it one
if commit.Date == (time.Time{}) {
commit.Date = yesterday
}
// Generate new commit with updated date, parent, tree, and new hash
commit.TreeHash = newTreeHash
commit.ParentHash = parentHash
newCommitStr := commit.ToString()
commitHeader := fmt.Sprintf("commit %d\000", len(newCommitStr))
newCommitBuf := bytes.NewBuffer(make([]byte, 0, len(commitHeader)+len([]byte(newCommitStr))))
newCommitBuf.WriteString(commitHeader)
newCommitBuf.WriteString(newCommitStr)
newCommitHash := SumHash(newCommitBuf.Bytes())
err = objectStore.WriteObject(newCommitHash, bytes.NewReader(newCommitBuf.Bytes()))
if err != nil && !errors.Is(err, ErrObjectExists) {
panic(err)
}
dontDelete[newCommitHash.HexString()] = true
commit.Hash = newCommitHash
parentHash = newCommitHash
parentCommit = commit
}
// Set the branch to the last commit.
os.WriteFile(filepath.Join(currentWorktree.vcsDirectory, head), []byte(parentHash.HexString()), 0644)
// Delete everything that's not in the dontDelete map
objectFiles, _ := os.ReadDir(path.Join(currentWorktree.vcsDirectory, "objects"))
for _, object := range objectFiles {
if _, exists := dontDelete[object.Name()]; !exists {
os.Remove(path.Join(currentWorktree.vcsDirectory, "objects", object.Name()))
}
}
}
func OrphanedObjects(prune bool) {
nonOrphaned := make(map[string]bool)
vcsDirectory := FindVCSDirectory()
currentWorktree := OpenCurrentWorktree(vcsDirectory)
defer currentWorktree.Close()
// Add all objects in current staging index to nonOrphaned map
idx := currentWorktree.ReadIndex()
for _, entry := range idx {
nonOrphaned[entry.hash.HexString()] = true
}
// Get the main branch and go down the line to collect all commits
headFile := currentWorktree.ReadHead()
_, head, _ := strings.Cut(strings.TrimSpace(string(headFile)), " ")
contents, _ := os.ReadFile(filepath.Join(vcsDirectory, head))
headCommitHash := ParseHashHex(strings.TrimSpace(string(contents)))
headCommit := DecodeCommit(vcsDirectory, headCommitHash)
currentCommit := headCommit
for !currentCommit.Hash.IsEmpty() {
nonOrphaned[currentCommit.Hash.HexString()] = true
tree := DecodeTree(vcsDirectory, currentCommit.TreeHash)
nonOrphaned[tree.hash.HexString()] = true
for _, e := range tree.entries {
nonOrphaned[e.hash.HexString()] = true
}
currentCommit = DecodeCommit(vcsDirectory, currentCommit.ParentHash)
}
orphansFound := false
objectFiles, _ := os.ReadDir(path.Join(currentWorktree.vcsDirectory, "objects"))
for _, object := range objectFiles {
if _, exists := nonOrphaned[object.Name()]; !exists {
orphansFound = true
if prune {
fmt.Printf("Pruned object %s.\n", object.Name())
os.Remove(path.Join(currentWorktree.vcsDirectory, "objects", object.Name()))
} else {
fmt.Printf("%s\n", object.Name())
}
}
}
if !prune && orphansFound {
fmt.Printf("\nRun `prism prune` to delete these orphaned objects from the repo.\n")
} else if !orphansFound {
fmt.Printf("No orphans found.\n")
}
}
// Finds the commits that have the given tree hash. Useful for finding commits with duplicate trees.
func FindCommitsWithTreeHash(givenHashString string) {
givenHash := ParseHashHex(givenHashString)
vcsDirectory := FindVCSDirectory()
currentWorktree := OpenCurrentWorktree(vcsDirectory)
defer currentWorktree.Close()
currentWorktree.WriteLock()
defer currentWorktree.WriteUnlock()
// Get the main branch and go down the line to collect all commits
headFile := currentWorktree.ReadHead()
_, head, _ := strings.Cut(strings.TrimSpace(string(headFile)), " ")
contents, _ := os.ReadFile(filepath.Join(vcsDirectory, head))
headCommitHash := ParseHashHex(strings.TrimSpace(string(contents)))
headCommit := DecodeCommit(vcsDirectory, headCommitHash)
currentCommit := headCommit
for !currentCommit.Hash.IsEmpty() {
if currentCommit.TreeHash == givenHash {
fmt.Printf("Found commit %s with tree hash.\n", currentCommit.Hash.HexString())
}
currentCommit = DecodeCommit(vcsDirectory, currentCommit.ParentHash)
}
}
// Check repository for corruption by making sure all objects that are linked to are stored on the filesystem
func CheckRepoCmd(verbose bool) {
nonOrphaned := make(map[Hash]bool)
missingObject := make(map[Hash]bool)
vcsDirectory := FindVCSDirectory()
currentWorktree := OpenCurrentWorktree(vcsDirectory)
defer currentWorktree.Close()
// Add all objects in current staging index to nonOrphaned map, and check that they exist
idx := currentWorktree.ReadIndex()
for _, entry := range idx {
nonOrphaned[entry.hash] = true
if _, err := os.Stat(filepath.Join(vcsDirectory, "objects", entry.hash.HexString())); errors.Is(err, fs.ErrNotExist) {
missingObject[entry.hash] = true
if verbose {
fmt.Printf("Missing blob %s\n", entry.hash.HexString())
}
}
}
// Get the main branch and go down the line to get all commits. Check that they, their trees, and the objects of the trees all exist.
headFile := currentWorktree.ReadHead()
_, head, _ := strings.Cut(strings.TrimSpace(string(headFile)), " ")
contents, _ := os.ReadFile(filepath.Join(vcsDirectory, head))
headCommitHash := ParseHashHex(strings.TrimSpace(string(contents)))
currentCommitHash := headCommitHash
for !currentCommitHash.IsEmpty() {
currentCommit := DecodeCommit(vcsDirectory, currentCommitHash)
if currentCommit.Hash.IsEmpty() {
missingObject[currentCommitHash] = true
if verbose {
fmt.Printf("Missing commit %s\n", currentCommitHash.HexString())
}
break
}
nonOrphaned[currentCommit.Hash] = true
tree := DecodeTree(vcsDirectory, currentCommit.TreeHash)
nonOrphaned[tree.hash] = true
if tree.hash.IsEmpty() {
missingObject[currentCommit.TreeHash] = true
if verbose {
fmt.Printf("Missing tree %s.\n", currentCommit.TreeHash.HexString())
}
} else {
for _, e := range tree.entries {
nonOrphaned[e.hash] = true
if _, err := os.Stat(filepath.Join(vcsDirectory, "objects", e.hash.HexString())); errors.Is(err, fs.ErrNotExist) {
missingObject[e.hash] = true
if verbose {
fmt.Printf("Missing blob %s\n", e.hash.HexString())
}
}
}
}
currentCommitHash = currentCommit.ParentHash
}
// TODO: Orphaned Commits - commits that are not pointed to by anything, but may point themselves to parents.
// This can happen if a commit object is missing (and therefore a link from it to its parent is missing).
// TODO: Orphaned Tree Entries, when tree objects they are pointed from are missing.
store := ObjectStore{vcsDirectory}
orphansFound := false
objectFiles, _ := os.ReadDir(path.Join(currentWorktree.vcsDirectory, "objects"))
// Try to find commits and tree entries and check their trees
for _, object := range objectFiles {
objHash := ParseHashHex(object.Name())
objReader, _ := store.GetObject(objHash)
if _, exists := nonOrphaned[objHash]; !exists {
if objReader.Type == ObjectType_Commit {
objReader.Close()
commitHash := objHash
for !commitHash.IsEmpty() {
commit := DecodeCommit(vcsDirectory, commitHash)
if commit.Hash.IsEmpty() {
missingObject[commitHash] = true
if verbose {
fmt.Printf("Missing commit %s\n", commitHash.HexString())
}
break
}
tree := DecodeTree(vcsDirectory, commit.TreeHash)
nonOrphaned[tree.hash] = true
if tree.hash.IsEmpty() {
missingObject[commit.TreeHash] = true
if verbose {
fmt.Printf("Missing tree %s\n", commit.TreeHash.HexString())
}
} else {
for _, e := range tree.entries {
nonOrphaned[e.hash] = true
if _, err := os.Stat(filepath.Join(vcsDirectory, "objects", e.hash.HexString())); errors.Is(err, fs.ErrNotExist) {
missingObject[e.hash] = true
if verbose {
fmt.Printf("Missing blob %s\n", e.hash.HexString())
}
}
}
}
if !commit.ParentHash.IsEmpty() {
nonOrphaned[commit.ParentHash] = true
}
commitHash = commit.ParentHash
}
} else if objReader.Type == ObjectType_Tree {
tree := DecodeTree(vcsDirectory, objHash)
nonOrphaned[tree.hash] = true
for _, e := range tree.entries {
nonOrphaned[e.hash] = true
if _, err := os.Stat(filepath.Join(vcsDirectory, "objects", e.hash.HexString())); errors.Is(err, fs.ErrNotExist) {
missingObject[e.hash] = true
if verbose {
fmt.Printf("Missing blob %s\n", e.hash.HexString())
}
}
}
}
}
}
// Print out orphans
for _, object := range objectFiles {
objHash := ParseHashHex(object.Name())
if _, exists := nonOrphaned[objHash]; !exists {
orphansFound = true
fmt.Printf("Orphaned: %s\n", object.Name())
}
}
if orphansFound {
fmt.Printf("\nRun `prism prune` to delete these orphaned objects from the repo.\n")
} else if !orphansFound {
fmt.Printf("No orphans found.\n")
}
fmt.Printf("Found %d missing objects.\n", len(missingObject))
for obj, _ := range missingObject {
fmt.Printf("%s\n", obj.HexString())
}
}