Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chezmoi2: Add diff choice when target has been modified #1015

Merged
merged 3 commits into from
Jan 26, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
chezmoi2: Factor out diffPatch and fix diffs of new files
  • Loading branch information
twpayne committed Jan 26, 2021
commit c4d1551bd8af3e7e91b15b601ff85044d27bc001
14 changes: 12 additions & 2 deletions chezmoi2/internal/chezmoi/actualstateentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ func (s *ActualStateDir) Remove(system System) error {

// EntryState returns d's entry state.
func (s *ActualStateFile) EntryState() (*EntryState, error) {
contents, err := s.Contents()
if err != nil {
return nil, err
}
contentsSHA256, err := s.ContentsSHA256()
if err != nil {
return nil, err
Expand All @@ -133,6 +137,7 @@ func (s *ActualStateFile) EntryState() (*EntryState, error) {
Type: EntryStateTypeFile,
Mode: s.perm,
ContentsSHA256: hexBytes(contentsSHA256),
contents: contents,
}, nil
}

Expand All @@ -148,13 +153,18 @@ func (s *ActualStateFile) Remove(system System) error {

// EntryState returns d's entry state.
func (s *ActualStateSymlink) EntryState() (*EntryState, error) {
contentsSHA256, err := s.LinknameSHA256()
linkname, err := s.Linkname()
if err != nil {
return nil, err
}
linknameSHA256, err := s.LinknameSHA256()
if err != nil {
return nil, err
}
return &EntryState{
Type: EntryStateTypeSymlink,
ContentsSHA256: hexBytes(contentsSHA256),
ContentsSHA256: hexBytes(linknameSHA256),
contents: []byte(linkname),
}, nil
}

Expand Down
117 changes: 117 additions & 0 deletions chezmoi2/internal/chezmoi/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package chezmoi

import (
"net/http"
"os"
"strings"
"time"

"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/format/diff"
"github.com/sergi/go-diff/diffmatchpatch"
)

var gitDiffOperation = map[diffmatchpatch.Operation]diff.Operation{
diffmatchpatch.DiffDelete: diff.Delete,
diffmatchpatch.DiffEqual: diff.Equal,
diffmatchpatch.DiffInsert: diff.Add,
}

type gitDiffChunk struct {
content string
operation diff.Operation
}

func (c *gitDiffChunk) Content() string { return c.content }
func (c *gitDiffChunk) Type() diff.Operation { return c.operation }

type gitDiffFile struct {
hash plumbing.Hash
fileMode filemode.FileMode
relPath RelPath
}

func (f *gitDiffFile) Hash() plumbing.Hash { return f.hash }
func (f *gitDiffFile) Mode() filemode.FileMode { return f.fileMode }
func (f *gitDiffFile) Path() string { return string(f.relPath) }

type gitDiffFilePatch struct {
isBinary bool
from, to diff.File
chunks []diff.Chunk
}

func (fp *gitDiffFilePatch) IsBinary() bool { return fp.isBinary }
func (fp *gitDiffFilePatch) Files() (diff.File, diff.File) { return fp.from, fp.to }
func (fp *gitDiffFilePatch) Chunks() []diff.Chunk { return fp.chunks }

type gitDiffPatch struct {
filePatches []diff.FilePatch
message string
}

func (p *gitDiffPatch) FilePatches() []diff.FilePatch { return p.filePatches }
func (p *gitDiffPatch) Message() string { return p.message }

func diffChunks(from, to string) []diff.Chunk {
dmp := diffmatchpatch.New()
dmp.DiffTimeout = time.Second
fromRunes, toRunes, runesToLines := dmp.DiffLinesToRunes(from, to)
diffs := dmp.DiffCharsToLines(dmp.DiffMainRunes(fromRunes, toRunes, false), runesToLines)
chunks := make([]diff.Chunk, 0, len(diffs))
for _, d := range diffs {
chunk := &gitDiffChunk{
content: d.Text,
operation: gitDiffOperation[d.Type],
}
chunks = append(chunks, chunk)
}
return chunks
}

func diffPatch(path RelPath, fromData []byte, fromMode os.FileMode, toData []byte, toMode os.FileMode) (diff.Patch, error) {
isBinary := isBinary(fromData) || isBinary(toData)

var from diff.File
if fromData != nil || fromMode != 0 {
fromFileMode, err := filemode.NewFromOSFileMode(fromMode)
if err != nil {
return nil, err
}
from = &gitDiffFile{
fileMode: fromFileMode,
relPath: path,
hash: plumbing.ComputeHash(plumbing.BlobObject, fromData),
}
}

toFileMode, err := filemode.NewFromOSFileMode(toMode)
if err != nil {
return nil, err
}

var chunks []diff.Chunk
if !isBinary {
chunks = diffChunks(string(fromData), string(toData))
}

return &gitDiffPatch{
filePatches: []diff.FilePatch{
&gitDiffFilePatch{
isBinary: isBinary,
from: from,
to: &gitDiffFile{
fileMode: toFileMode,
relPath: path,
hash: plumbing.ComputeHash(plumbing.BlobObject, toData),
},
chunks: chunks,
},
},
}, nil
}

func isBinary(data []byte) bool {
return len(data) != 0 && !strings.HasPrefix(http.DetectContentType(data), "text/")
}
6 changes: 6 additions & 0 deletions chezmoi2/internal/chezmoi/entrystate.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ type EntryState struct {
Type EntryStateType `json:"type" toml:"type" yaml:"type"`
Mode os.FileMode `json:"mode,omitempty" toml:"mode,omitempty" yaml:"mode,omitempty"`
ContentsSHA256 hexBytes `json:"contentsSHA256,omitempty" toml:"contentsSHA256,omitempty" yaml:"contentsSHA256,omitempty"`
contents []byte
}

// Contents returns s's contents, if available.
func (s *EntryState) Contents() []byte {
return s.contents
}

// Equal returns true if s is equal to other.
Expand Down
107 changes: 10 additions & 97 deletions chezmoi2/internal/chezmoi/gitdiffsystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,32 @@ package chezmoi

import (
"io"
"net/http"
"os"
"os/exec"
"strings"
"time"

"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/format/diff"
"github.com/sergi/go-diff/diffmatchpatch"
vfs "github.com/twpayne/go-vfs"
)

// A GitDiffSystem wraps a System and logs all of the actions executed as a git
// diff.
type GitDiffSystem struct {
system System
dir AbsPath
dirAbsPath AbsPath
unifiedEncoder *diff.UnifiedEncoder
}

// NewGitDiffSystem returns a new GitDiffSystem.
func NewGitDiffSystem(system System, w io.Writer, dir AbsPath, color bool) *GitDiffSystem {
func NewGitDiffSystem(system System, w io.Writer, dirAbsPath AbsPath, color bool) *GitDiffSystem {
unifiedEncoder := diff.NewUnifiedEncoder(w, diff.DefaultContextLines)
if color {
unifiedEncoder.SetColor(diff.NewColorConfig())
}
return &GitDiffSystem{
system: system,
dir: dir,
dirAbsPath: dirAbsPath,
unifiedEncoder: unifiedEncoder,
}
}
Expand Down Expand Up @@ -221,53 +217,32 @@ func (s *GitDiffSystem) UnderlyingFS() vfs.FS {

// WriteFile implements System.WriteFile.
func (s *GitDiffSystem) WriteFile(filename AbsPath, data []byte, perm os.FileMode) error {
fromFileMode, _, err := s.fileMode(filename)
var fromData []byte
switch {
var fromMode os.FileMode
switch fromInfo, err := s.system.Stat(filename); {
case err == nil:
fromData, err = s.system.ReadFile(filename)
if err != nil {
return err
}
fromMode = fromInfo.Mode()
case os.IsNotExist(err):
default:
return err
}
toFileMode, err := filemode.NewFromOSFileMode(perm)
diffPatch, err := diffPatch(s.trimPrefix(filename), fromData, fromMode, data, perm)
if err != nil {
return err
}
path := s.trimPrefix(filename)
isBinary := isBinary(fromData) || isBinary(data)
var chunks []diff.Chunk
if !isBinary {
chunks = diffChunks(string(fromData), string(data))
}
if err := s.unifiedEncoder.Encode(&gitDiffPatch{
filePatches: []diff.FilePatch{
&gitDiffFilePatch{
isBinary: isBinary,
from: &gitDiffFile{
fileMode: fromFileMode,
relPath: path,
hash: plumbing.ComputeHash(plumbing.BlobObject, fromData),
},
to: &gitDiffFile{
fileMode: toFileMode,
relPath: path,
hash: plumbing.ComputeHash(plumbing.BlobObject, data),
},
chunks: chunks,
},
},
}); err != nil {
if err := s.unifiedEncoder.Encode(diffPatch); err != nil {
return err
}
return s.system.WriteFile(filename, data, perm)
}

// WriteSymlink implements System.WriteSymlink.
func (s *GitDiffSystem) WriteSymlink(oldname string, newname AbsPath) error {
// FIXME if newname already exists then we should
if err := s.unifiedEncoder.Encode(&gitDiffPatch{
filePatches: []diff.FilePatch{
&gitDiffFilePatch{
Expand Down Expand Up @@ -300,67 +275,5 @@ func (s *GitDiffSystem) fileMode(name AbsPath) (filemode.FileMode, os.FileInfo,
}

func (s *GitDiffSystem) trimPrefix(absPath AbsPath) RelPath {
return absPath.MustTrimDirPrefix(s.dir)
}

var gitDiffOperation = map[diffmatchpatch.Operation]diff.Operation{
diffmatchpatch.DiffDelete: diff.Delete,
diffmatchpatch.DiffEqual: diff.Equal,
diffmatchpatch.DiffInsert: diff.Add,
}

type gitDiffChunk struct {
content string
operation diff.Operation
}

func (c *gitDiffChunk) Content() string { return c.content }
func (c *gitDiffChunk) Type() diff.Operation { return c.operation }

type gitDiffFile struct {
hash plumbing.Hash
fileMode filemode.FileMode
relPath RelPath
}

func (f *gitDiffFile) Hash() plumbing.Hash { return f.hash }
func (f *gitDiffFile) Mode() filemode.FileMode { return f.fileMode }
func (f *gitDiffFile) Path() string { return string(f.relPath) }

type gitDiffFilePatch struct {
isBinary bool
from, to diff.File
chunks []diff.Chunk
}

func (fp *gitDiffFilePatch) IsBinary() bool { return fp.isBinary }
func (fp *gitDiffFilePatch) Files() (diff.File, diff.File) { return fp.from, fp.to }
func (fp *gitDiffFilePatch) Chunks() []diff.Chunk { return fp.chunks }

type gitDiffPatch struct {
filePatches []diff.FilePatch
message string
}

func (p *gitDiffPatch) FilePatches() []diff.FilePatch { return p.filePatches }
func (p *gitDiffPatch) Message() string { return p.message }

func diffChunks(from, to string) []diff.Chunk {
dmp := diffmatchpatch.New()
dmp.DiffTimeout = time.Second
fromRunes, toRunes, runesToLines := dmp.DiffLinesToRunes(from, to)
diffs := dmp.DiffCharsToLines(dmp.DiffMainRunes(fromRunes, toRunes, false), runesToLines)
chunks := make([]diff.Chunk, 0, len(diffs))
for _, d := range diffs {
chunk := &gitDiffChunk{
content: d.Text,
operation: gitDiffOperation[d.Type],
}
chunks = append(chunks, chunk)
}
return chunks
}

func isBinary(data []byte) bool {
return len(data) != 0 && !strings.HasPrefix(http.DetectContentType(data), "text/")
return absPath.MustTrimDirPrefix(s.dirAbsPath)
}
10 changes: 10 additions & 0 deletions chezmoi2/internal/chezmoi/targetstateentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ func (t *TargetStateFile) Apply(system System, persistentState PersistentState,

// EntryState returns t's entry state.
func (t *TargetStateFile) EntryState() (*EntryState, error) {
contents, err := t.Contents()
if err != nil {
return nil, err
}
contentsSHA256, err := t.ContentsSHA256()
if err != nil {
return nil, err
Expand All @@ -179,6 +183,7 @@ func (t *TargetStateFile) EntryState() (*EntryState, error) {
Type: EntryStateTypeFile,
Mode: t.perm,
ContentsSHA256: hexBytes(contentsSHA256),
contents: contents,
}, nil
}

Expand Down Expand Up @@ -391,13 +396,18 @@ func (t *TargetStateSymlink) Apply(system System, persistentState PersistentStat

// EntryState returns t's entry state.
func (t *TargetStateSymlink) EntryState() (*EntryState, error) {
linkname, err := t.Linkname()
if err != nil {
return nil, err
}
linknameSHA256, err := t.LinknameSHA256()
if err != nil {
return nil, err
}
return &EntryState{
Type: EntryStateTypeSymlink,
ContentsSHA256: linknameSHA256,
contents: []byte(linkname),
}, nil
}

Expand Down
Loading