diff --git a/README.md b/README.md index c6a6a8a..17c44f4 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,10 @@ Learning Git Internals by writing a Git in Go. +## Library Usage + +See `./integration_test.go` + +## CLI Usage + +See `go run ./cmd/gitg --help` \ No newline at end of file diff --git a/config.go b/config.go index 80c8120..2e978bf 100644 --- a/config.go +++ b/config.go @@ -20,6 +20,10 @@ const ( DefaultPackfileDirectory = "pack" ) +func init() { + Configure() +} + var config Cnf type ( diff --git a/fs.go b/fs.go index a897bd7..8f2fe54 100644 --- a/fs.go +++ b/fs.go @@ -3,7 +3,9 @@ package g import ( "bytes" "encoding/hex" + "errors" "fmt" + "io" "log" "os" "path/filepath" @@ -35,8 +37,8 @@ const ( type ( File struct { Path string - IdxStatus IndexStatus - WdStatus WDStatus + idxStatus IndexStatus + wdStatus WDStatus Sha Sha Finfo os.FileInfo } @@ -202,11 +204,11 @@ func NewFileSet(files []*File) *FileSet { func (fs *FileSet) Merge(fss *FileSet) { for _, v := range fss.idx { if _, ok := fs.idx[v.Path]; ok { - if fs.idx[v.Path].IdxStatus == IndexNotUpdated { - fs.idx[v.Path].IdxStatus = v.IdxStatus + if fs.idx[v.Path].idxStatus == IndexNotUpdated { + fs.idx[v.Path].idxStatus = v.idxStatus } - if fs.idx[v.Path].WdStatus == WDIndexAndWorkingTreeMatch { - fs.idx[v.Path].WdStatus = v.WdStatus + if fs.idx[v.Path].wdStatus == WDIndexAndWorkingTreeMatch { + fs.idx[v.Path].wdStatus = v.wdStatus } } else { fs.files = append(fs.files, v) @@ -222,19 +224,19 @@ func (fs *FileSet) MergeFromIndex(fss *FileSet) { // in index but not in commit files fs.files = append(fs.files, v) fs.idx[v.Path] = v - v.IdxStatus = IndexAddedInIndex + v.idxStatus = IndexAddedInIndex continue } fs.idx[v.Path].Finfo = v.Finfo if !bytes.Equal(v.Sha.AsByteSlice(), fs.idx[v.Path].Sha.AsByteSlice()) { - fs.idx[v.Path].IdxStatus = IndexUpdatedInIndex + fs.idx[v.Path].idxStatus = IndexUpdatedInIndex continue } } for _, v := range fs.files { if _, ok := fss.idx[v.Path]; !ok { // file exists in commit but not in index - v.IdxStatus = IndexDeletedInIndex + v.idxStatus = IndexDeletedInIndex } } } @@ -246,20 +248,23 @@ func (fs *FileSet) MergeFromWD(fss *FileSet) { // in working directory but not in index or commit files fs.files = append(fs.files, v) fs.idx[v.Path] = v - v.WdStatus = WDUntracked - v.IdxStatus = IndexUntracked + v.wdStatus = WDUntracked + v.idxStatus = IndexUntracked continue } if fs.idx[v.Path].Finfo == nil { // this is a commit file and not in the index // @todo should this be able to happen ? - fs.idx[v.Path].WdStatus = WDUntracked - fs.idx[v.Path].IdxStatus = IndexUntracked + fs.idx[v.Path].wdStatus = WDUntracked + fs.idx[v.Path].idxStatus = IndexUntracked } else { if v.Finfo.ModTime() != fs.idx[v.Path].Finfo.ModTime() { - fs.idx[v.Path].WdStatus = WDWorktreeChangedSinceIndex + fs.idx[v.Path].wdStatus = WDWorktreeChangedSinceIndex fs.idx[v.Path].Finfo = v.Finfo + // flag that the object needs to be indexed + // perhaps index add should be smarter instead ? + fs.idx[v.Path].Sha = Sha{} continue } @@ -268,7 +273,7 @@ func (fs *FileSet) MergeFromWD(fss *FileSet) { for _, v := range fs.files { if _, ok := fss.idx[v.Path]; !ok { // file exists in commit but not in index - v.WdStatus = WDDeletedInWorktree + v.wdStatus = WDDeletedInWorktree } } } @@ -329,3 +334,149 @@ func Init() error { // set default main branch return os.WriteFile(GitHeadPath(), []byte(fmt.Sprintf("ref: %s\n", filepath.Join(RefsHeadPrefix(), DefaultBranch()))), 0644) } + +func (f File) IndexStatus() IndexStatus { + return f.idxStatus +} + +func (f File) WorkingDirectoryStatus() WDStatus { + return f.wdStatus +} + +// SwitchToBranch updates the repository content to match that of a specified branch name +// or returns an error when it is not safe to do so. This should likely be cahnged to +// SwitchToCommit in the future to handle the broader use-case. +func SwitchToBranch(name string) error { + + // get commit sha + commitSha, err := HeadSHA(name) + if err != nil { + return err + } + + if !commitSha.IsSet() { + return fmt.Errorf("fatal: invalid reference: %s", name) + } + + // index + idx, err := ReadIndex() + if err != nil { + return err + } + + currentCommit, err := LastCommit() + if err != nil { + // @todo error types to check for e.g no previous commits as source of error + return err + } + + // + + currentStatus, err := Status(idx, currentCommit) + if err != nil { + return err + } + + // get commit files + commitFiles, err := CommittedFiles(commitSha) + if err != nil { + return err + } + + commitSet := NewFileSet(commitFiles) + + var errorWdFiles []*File + var errorIdxFiles []*File + var deleteFiles []*File + + for _, v := range currentStatus.Files() { + if v.IndexStatus() == IndexUpdatedInIndex { + errorIdxFiles = append(errorIdxFiles, v) + continue + } + if _, ok := commitSet.Contains(v.Path); ok { + if v.WorkingDirectoryStatus() == WDUntracked { + errorWdFiles = append(errorWdFiles, v) + continue + } + } else { + // should be deleted + deleteFiles = append(deleteFiles, v) + } + } + var errMsg = "" + if len(errorIdxFiles) > 0 { + filestr := "" + for _, v := range errorIdxFiles { + filestr += fmt.Sprintf("\t%s\n", v.Path) + } + errMsg = fmt.Sprintf("error: The following untracked working tree files would be overwritten by checkout:\n %sPlease move or remove them before you switch branches.\nAborting", filestr) + } + if len(errorWdFiles) > 0 { + filestr := "" + for _, v := range errorWdFiles { + filestr += fmt.Sprintf("\t%s\n", v.Path) + } + if errMsg != "" { + errMsg += "\n" + } + errMsg += fmt.Sprintf("error: The following untracked working tree files would be overwritten by checkout:\n %sPlease move or remove them before you switch branches.\nAborting", filestr) + } + + if errMsg != "" { + return errors.New(errMsg) + } + + for _, v := range deleteFiles { + if err := os.Remove(filepath.Join(Path(), v.Path)); err != nil { + return err + } + } + + idx = NewIndex() + + for _, v := range commitFiles { + obj, err := ReadObject(v.Sha) + if err != nil { + return err + } + r, err := obj.ReadCloser() + if err != nil { + return err + } + buf := make([]byte, obj.HeaderLength) + if _, err := r.Read(buf); err != nil { + return err + } + f, err := os.OpenFile(filepath.Join(Path(), v.Path), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0655) + if err != nil { + return err + } + + if _, err := io.Copy(f, r); err != nil { + return err + } + if err := r.Close(); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + + v.wdStatus = WDUntracked + if err := idx.Add(v); err != nil { + return err + } + } + + if err := idx.Write(); err != nil { + return err + } + + // update HEAD + if err := UpdateHead(name); err != nil { + return err + } + + return nil +} diff --git a/git/add.go b/git/add.go index 15222f6..e14f88c 100644 --- a/git/add.go +++ b/git/add.go @@ -23,7 +23,7 @@ func Add(paths ...string) error { if p == "." { // special case meaning add everything for _, v := range wdFiles.Files() { - switch v.WdStatus { + switch v.WorkingDirectoryStatus() { case g.WDUntracked, g.WDWorktreeChangedSinceIndex, g.WDDeletedInWorktree: updates = append(updates, v) } @@ -32,7 +32,7 @@ func Add(paths ...string) error { found := false for _, v := range wdFiles.Files() { if v.Path == p { - switch v.WdStatus { + switch v.WorkingDirectoryStatus() { case g.WDUntracked, g.WDWorktreeChangedSinceIndex, g.WDDeletedInWorktree: updates = append(updates, v) } @@ -44,7 +44,7 @@ func Add(paths ...string) error { // try directory @todo more efficient implementation for _, v := range wdFiles.Files() { if strings.HasPrefix(v.Path, p+string(filepath.Separator)) { - switch v.WdStatus { + switch v.WorkingDirectoryStatus() { case g.WDUntracked, g.WDWorktreeChangedSinceIndex, g.WDDeletedInWorktree: updates = append(updates, v) } @@ -59,15 +59,6 @@ func Add(paths ...string) error { } } for _, v := range updates { - switch v.WdStatus { - case g.WDUntracked, g.WDWorktreeChangedSinceIndex: - // add the file to the object store - obj, err := g.WriteBlob(v.Path) - if err != nil { - return err - } - v.Sha = obj.Sha - } if err := idx.Add(v); err != nil { return err } diff --git a/git/restore.go b/git/restore.go index e346245..2c62816 100644 --- a/git/restore.go +++ b/git/restore.go @@ -32,11 +32,11 @@ func Restore(path string, staged bool) error { fileStatus, ok := currentStatus.Contains(path) // if the path not found or is untracked working directory fileStatus then error - if !ok || fileStatus.WdStatus == g.WDUntracked { + if !ok || fileStatus.WorkingDirectoryStatus() == g.WDUntracked { return fmt.Errorf("error: pathspec '%s' did not match any fileStatus(s) known to git", path) } // if in index but not committed - if fileStatus.IdxStatus == g.IndexAddedInIndex && fileStatus.WdStatus != g.WDWorktreeChangedSinceIndex { + if fileStatus.IndexStatus() == g.IndexAddedInIndex && fileStatus.WorkingDirectoryStatus() != g.WDWorktreeChangedSinceIndex { // there is nothing to do, right ? ... return nil } diff --git a/git/status.go b/git/status.go index 3b1c579..d44e54d 100644 --- a/git/status.go +++ b/git/status.go @@ -14,10 +14,10 @@ func Status(o io.Writer) error { return err } for _, v := range files.Files() { - if v.IdxStatus == g.IndexNotUpdated && v.WdStatus == g.WDIndexAndWorkingTreeMatch { + if v.IndexStatus() == g.IndexNotUpdated && v.WorkingDirectoryStatus() == g.WDIndexAndWorkingTreeMatch { continue } - if _, err := fmt.Fprintf(o, "%s%s %s\n", v.IdxStatus, v.WdStatus, v.Path); err != nil { + if _, err := fmt.Fprintf(o, "%s%s %s\n", v.IndexStatus(), v.WorkingDirectoryStatus(), v.Path); err != nil { return err } } diff --git a/git/switch.go b/git/switch.go index b5d6677..8eb03ac 100644 --- a/git/switch.go +++ b/git/switch.go @@ -1,143 +1,9 @@ package git import ( - "errors" - "fmt" "github.com/richardjennings/g" - "io" - "os" - "path/filepath" ) func SwitchBranch(name string) error { - - // index - idx, err := g.ReadIndex() - if err != nil { - return err - } - - // get commit sha - commitSha, err := g.HeadSHA(name) - if err != nil { - return err - } - - if !commitSha.IsSet() { - return fmt.Errorf("fatal: invalid reference: %s", name) - } - - currentCommit, err := g.LastCommit() - if err != nil { - // @todo error types to check for e.g no previous commits as source of error - return err - } - - currentStatus, err := g.Status(idx, currentCommit) - if err != nil { - return err - } - - // get commit files - commitFiles, err := g.CommittedFiles(commitSha) - if err != nil { - return err - } - - commitSet := g.NewFileSet(commitFiles) - - var errorWdFiles []*g.File - var errorIdxFiles []*g.File - var deleteFiles []*g.File - - for _, v := range currentStatus.Files() { - if v.IdxStatus == g.IndexUpdatedInIndex { - errorIdxFiles = append(errorIdxFiles, v) - continue - } - if _, ok := commitSet.Contains(v.Path); ok { - if v.WdStatus == g.WDUntracked { - errorWdFiles = append(errorWdFiles, v) - continue - } - } else { - // should be deleted - deleteFiles = append(deleteFiles, v) - } - } - var errMsg = "" - if len(errorIdxFiles) > 0 { - filestr := "" - for _, v := range errorIdxFiles { - filestr += fmt.Sprintf("\t%s\n", v.Path) - } - errMsg = fmt.Sprintf("error: The following untracked working tree files would be overwritten by checkout:\n %sPlease move or remove them before you switch branches.\nAborting", filestr) - } - if len(errorWdFiles) > 0 { - filestr := "" - for _, v := range errorWdFiles { - filestr += fmt.Sprintf("\t%s\n", v.Path) - } - if errMsg != "" { - errMsg += "\n" - } - errMsg += fmt.Sprintf("error: The following untracked working tree files would be overwritten by checkout:\n %sPlease move or remove them before you switch branches.\nAborting", filestr) - } - - if errMsg != "" { - return errors.New(errMsg) - } - - for _, v := range deleteFiles { - if err := os.Remove(filepath.Join(g.Path(), v.Path)); err != nil { - return err - } - } - - idx = g.NewIndex() - - for _, v := range commitFiles { - obj, err := g.ReadObject(v.Sha) - if err != nil { - return err - } - r, err := obj.ReadCloser() - if err != nil { - return err - } - buf := make([]byte, obj.HeaderLength) - if _, err := r.Read(buf); err != nil { - return err - } - f, err := os.OpenFile(filepath.Join(g.Path(), v.Path), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0655) - if err != nil { - return err - } - - if _, err := io.Copy(f, r); err != nil { - return err - } - if err := r.Close(); err != nil { - return err - } - if err := f.Close(); err != nil { - return err - } - v.WdStatus = g.WDUntracked - if err := idx.Add(v); err != nil { - return err - } - } - - if err := idx.Write(); err != nil { - return err - } - - // update HEAD - if err := g.UpdateHead(name); err != nil { - return err - } - - return nil - + return g.SwitchToBranch(name) } diff --git a/index.go b/index.go index 3ce3f97..6202cba 100644 --- a/index.go +++ b/index.go @@ -82,8 +82,16 @@ func (idx *Index) Rm(path string) error { // Add adds a fs.File to the Index Struct. A call to idx.Write is required // to flush the changes to the filesystem. func (idx *Index) Add(f *File) error { + if !f.Sha.IsSet() { + o, err := WriteBlob(f.Path) + if err != nil { + return err + } + f.Sha = o.Sha + } + // if delete, remove from Index - if f.WdStatus == WDDeletedInWorktree { + if f.wdStatus == WDDeletedInWorktree { for i, v := range idx.items { if string(v.Name) == f.Path { idx.items = append(idx.items[0:i], idx.items[i+1:]...) @@ -92,7 +100,7 @@ func (idx *Index) Add(f *File) error { } } return errors.New("somehow the file was not found in Index items to be removed") - } else if f.WdStatus == WDUntracked { + } else if f.wdStatus == WDUntracked { // just add and sort all of them for now item, err := item(f) if err != nil { @@ -104,7 +112,7 @@ func (idx *Index) Add(f *File) error { sort.Slice(idx.items, func(i, j int) bool { return string(idx.items[i].Name) < string(idx.items[j].Name) }) - } else if f.WdStatus == WDWorktreeChangedSinceIndex { + } else if f.wdStatus == WDWorktreeChangedSinceIndex { for i, v := range idx.items { if string(v.Name) == f.Path { item, err := item(f) diff --git a/index_test.go b/index_test.go index e9f1c0b..d813682 100644 --- a/index_test.go +++ b/index_test.go @@ -7,7 +7,7 @@ import ( func TestAdd_WDUntracked(t *testing.T) { idx := NewIndex() - err := idx.Add(&File{Path: "test_assets/test.file", Sha: Sha{}, WdStatus: WDUntracked}) + err := idx.Add(&File{Path: "test_assets/test.file", Sha: Sha{}, wdStatus: WDUntracked}) assert.Nil(t, err) files := idx.Files() assert.Equal(t, 1, len(files)) diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..5c9b76c --- /dev/null +++ b/integration_test.go @@ -0,0 +1,134 @@ +package g + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "testing" + "time" +) + +func Test_Library(t *testing.T) { + + // create a working directory + dir, err := os.MkdirTemp("", "") + e(err, t) + + // add a couple of files + e(os.WriteFile(filepath.Join(dir, "a"), []byte("a"), 0644), t) + e(os.WriteFile(filepath.Join(dir, "b"), []byte("b"), 0644), t) + + // configure with dir working directory + e(Configure(WithPath(dir)), t) + + // init, creating a .git folder + e(Init(), t) + + // create a representation of the repository state + fs, err := CurrentStatus() + e(err, t) + + a, ok := fs.Contains("a") + if !ok { + e(errors.New("expected file 'a' to be in the current status"), t) + } + + // check the status of 'a' + if a.IndexStatus() != IndexUntracked { + e(errors.New("expected file 'a' to be untracked in the index"), t) + } + + if a.WorkingDirectoryStatus() != WDUntracked { + e(errors.New("expected file 'a' to be untracked in the working directory"), t) + } + + // get the index + idx, err := ReadIndex() + e(err, t) + + // add file 'a' to the index and object store + e(idx.Add(a), t) + + // write the index + e(idx.Write(), t) + + // create a new representation of the repository state + fs, err = CurrentStatus() + e(err, t) + + // get file 'a' again + a, ok = fs.Contains("a") + if !ok { + e(errors.New("expected file 'a' to be in the current status"), t) + } + + // check the status of 'a' + if a.IndexStatus() != IndexAddedInIndex { + e(errors.New("expected file 'a' to be added in the index"), t) + } + + if a.WorkingDirectoryStatus() != WDIndexAndWorkingTreeMatch { + e(errors.New("expected file 'a' to have index and working tree match status"), t) + } + + // create a tree of objects in the index + tree := ObjectTree(idx.Files()) + + // write the tree to the object store + treeSha, err := tree.WriteTree() + + // check for no previous commits + pc, err := PreviousCommits() + e(err, t) + if pc != nil { + e(errors.New("expected no previous commits"), t) + } + + // create a commit + commit := &Commit{ + Tree: treeSha, + Parents: pc, + Author: fmt.Sprintf("%s <%s>", "tester", "tester@test.com"), + AuthoredTime: time.Now(), + Committer: fmt.Sprintf("%s <%s>", "tester", "tester@test.com"), + CommittedTime: time.Now(), + Message: []byte("this is a commit message"), + } + + // write the commit + commitSha, err := WriteCommit(commit) + e(err, t) + if !commitSha.IsSet() { + e(errors.New("expected commit SHA to be set"), t) + } + + // get current branch + branch, err := CurrentBranch() + e(err, t) + if branch != "main" { + e(errors.New("expected branch to be 'main'"), t) + } + + // get current commit sha + headSha, err := HeadSHA(branch) + e(err, t) + if headSha.String() != commitSha.String() { + e(errors.New("expected head SHA to match previous commit SHA"), t) + } + + // read the current commit + commit, err = ReadCommit(headSha) + e(err, t) + + if string(commit.Message) != "this is a commit message\n" { + e(errors.New("expected commit message to be 'this is a commit message'"), t) + } + +} + +func e(err error, t *testing.T) { + if err != nil { + t.Fatal(err) + } +} diff --git a/object.go b/object.go index abcdc51..564027e 100644 --- a/object.go +++ b/object.go @@ -540,7 +540,7 @@ func WriteBlob(path string) (*Object, error) { } header := []byte(fmt.Sprintf("blob %d%s", finfo.Size(), string(byte(0)))) sha, err := WriteObject(header, nil, path, ObjectPath()) - return &Object{Sha: sha, Path: path}, err + return &Object{Sha: sha, Path: path, Typ: ObjectTypeBlob}, err } func WriteCommit(c *Commit) (Sha, error) { diff --git a/packfile_test.go b/packfile_test.go index 1a61b69..da35088 100644 --- a/packfile_test.go +++ b/packfile_test.go @@ -29,7 +29,7 @@ func TestPackfile_lookupInPackfiles(t *testing.T) { if err != nil { t.Fatal(err) } - if files.Files()[0].IdxStatus != IndexNotUpdated { - t.Errorf("idxStatus = %d, want %d", files.Files()[0].IdxStatus, IndexNotUpdated) + if files.Files()[0].idxStatus != IndexNotUpdated { + t.Errorf("idxStatus = %d, want %d", files.Files()[0].idxStatus, IndexNotUpdated) } }