aboutsummaryrefslogblamecommitdiffstats
path: root/plumbing/format/diff/unified_encoder.go
blob: 19c3f0b18ae60cbc36bfef4df39844a8b22ba544 (plain) (tree)
1
2
3
4
5
6
7
8
9


            

             
                
                 

                 
                                              

 

                                                              
 

                                                             
 




                                           
 




                                                   

 

                                                                               


                            


                                                                                    
 

                                                                           

 





                                                                       

 





                                                                            
                        
                                                    
                                
 



                                                      
                 
         
 




                                                                          


                 

                                              

 



                                                                                         
         
                                        
 
                                     
                

                                                      
                                                                                 
                                             

                                                                     
                 
                                             

                                                                        
                 
                                                            
                                                                                 
                                       
                                                                                                 
                 
                                
                                                                                        

                         



                                                                               
                       



                                                                                   
         
                                           

 
                                                                                                      
                     



                                                                                    
         

















                                                                      



                                                    


                                     


                                                      
                            

                                            

                         


                                                               
                         

                                          
                         


                                                               

                 

                                                             


                 
                      

 

                                                           



                            




                                                                          

         

                                                                         


                    

                                                                                   
                 

                                                                                      

         
                             

 

                                                                                                          


                                                              


                                                 


                               
                             
                        

                                     








                                                              


                                                                 


                      



                                                                        
                


                                                      
                 

                                                                    
 


                                                           



                                    
                                                    


                                      













                        


                                                                
 

                                                        
                


                                                         

         
                            
 

                                                      
                


                                                       

         

                                         
 




                                                 

         
                          
 

                                     


         

                                                 

                  
                              
                    
                                
                   

                                

         

                                                







                      







                                                                  
         
 
package diff

import (
	"fmt"
	"io"
	"regexp"
	"strconv"
	"strings"

	"github.com/go-git/go-git/v5/plumbing"
)

// DefaultContextLines is the default number of context lines.
const DefaultContextLines = 3

var (
	splitLinesRegexp = regexp.MustCompile(`[^\n]*(\n|$)`)

	operationChar = map[Operation]byte{
		Add:    '+',
		Delete: '-',
		Equal:  ' ',
	}

	operationColorKey = map[Operation]ColorKey{
		Add:    New,
		Delete: Old,
		Equal:  Context,
	}
)

// UnifiedEncoder encodes an unified diff into the provided Writer. It does not
// support similarity index for renames or sorting hash representations.
type UnifiedEncoder struct {
	io.Writer

	// contextLines is the count of unchanged lines that will appear surrounding
	// a change.
	contextLines int

	// colorConfig is the color configuration. The default is no color.
	color ColorConfig
}

// NewUnifiedEncoder returns a new UnifiedEncoder that writes to w.
func NewUnifiedEncoder(w io.Writer, contextLines int) *UnifiedEncoder {
	return &UnifiedEncoder{
		Writer:       w,
		contextLines: contextLines,
	}
}

// SetColor sets e's color configuration and returns e.
func (e *UnifiedEncoder) SetColor(colorConfig ColorConfig) *UnifiedEncoder {
	e.color = colorConfig
	return e
}

// Encode encodes patch.
func (e *UnifiedEncoder) Encode(patch Patch) error {
	sb := &strings.Builder{}

	if message := patch.Message(); message != "" {
		sb.WriteString(message)
		if !strings.HasSuffix(message, "\n") {
			sb.WriteByte('\n')
		}
	}

	for _, filePatch := range patch.FilePatches() {
		e.writeFilePatchHeader(sb, filePatch)
		g := newHunksGenerator(filePatch.Chunks(), e.contextLines)
		for _, hunk := range g.Generate() {
			hunk.writeTo(sb, e.color)
		}
	}

	_, err := e.Write([]byte(sb.String()))
	return err
}

func (e *UnifiedEncoder) writeFilePatchHeader(sb *strings.Builder, filePatch FilePatch) {
	from, to := filePatch.Files()
	if from == nil && to == nil {
		return
	}
	isBinary := filePatch.IsBinary()

	sb.WriteString(e.color[Meta])
	switch {
	case from != nil && to != nil:
		hashEquals := from.Hash() == to.Hash()
		fmt.Fprintf(sb, "diff --git a/%s b/%s\n", from.Path(), to.Path())
		if from.Mode() != to.Mode() {
			fmt.Fprintf(sb, "old mode %o\n", from.Mode())
			fmt.Fprintf(sb, "new mode %o\n", to.Mode())
		}
		if from.Path() != to.Path() {
			fmt.Fprintf(sb, "rename from %s\n", from.Path())
			fmt.Fprintf(sb, "rename to %s\n", to.Path())
		}
		if from.Mode() != to.Mode() && !hashEquals {
			fmt.Fprintf(sb, "index %s..%s\n", from.Hash(), to.Hash())
		} else if !hashEquals {
			fmt.Fprintf(sb, "index %s..%s %o\n", from.Hash(), to.Hash(), from.Mode())
		}
		if !hashEquals {
			e.writePathLines(sb, "a/"+from.Path(), "b/"+to.Path(), isBinary)
		}
	case from == nil:
		fmt.Fprintf(sb, "diff --git a/%s b/%s\n", to.Path(), to.Path())
		fmt.Fprintf(sb, "new file mode %o\n", to.Mode())
		fmt.Fprintf(sb, "index %s..%s\n", plumbing.ZeroHash, to.Hash())
		e.writePathLines(sb, "/dev/null", "b/"+to.Path(), isBinary)
	case to == nil:
		fmt.Fprintf(sb, "diff --git a/%s b/%s\n", from.Path(), from.Path())
		fmt.Fprintf(sb, "deleted file mode %o\n", from.Mode())
		fmt.Fprintf(sb, "index %s..%s\n", from.Hash(), plumbing.ZeroHash)
		e.writePathLines(sb, "a/"+from.Path(), "/dev/null", isBinary)
	}
	sb.WriteString(e.color.Reset(Meta))
}

func (e *UnifiedEncoder) writePathLines(sb *strings.Builder, fromPath, toPath string, isBinary bool) {
	if isBinary {
		fmt.Fprintf(sb, "Binary files %s and %s differ\n", fromPath, toPath)
	} else {
		fmt.Fprintf(sb, "--- %s\n", fromPath)
		fmt.Fprintf(sb, "+++ %s\n", toPath)
	}
}

type hunksGenerator struct {
	fromLine, toLine            int
	ctxLines                    int
	chunks                      []Chunk
	current                     *hunk
	hunks                       []*hunk
	beforeContext, afterContext []string
}

func newHunksGenerator(chunks []Chunk, ctxLines int) *hunksGenerator {
	return &hunksGenerator{
		chunks:   chunks,
		ctxLines: ctxLines,
	}
}

func (g *hunksGenerator) Generate() []*hunk {
	for i, chunk := range g.chunks {
		lines := splitLines(chunk.Content())
		nLines := len(lines)

		switch chunk.Type() {
		case Equal:
			g.fromLine += nLines
			g.toLine += nLines
			g.processEqualsLines(lines, i)
		case Delete:
			if nLines != 0 {
				g.fromLine++
			}

			g.processHunk(i, chunk.Type())
			g.fromLine += nLines - 1
			g.current.AddOp(chunk.Type(), lines...)
		case Add:
			if nLines != 0 {
				g.toLine++
			}
			g.processHunk(i, chunk.Type())
			g.toLine += nLines - 1
			g.current.AddOp(chunk.Type(), lines...)
		}

		if i == len(g.chunks)-1 && g.current != nil {
			g.hunks = append(g.hunks, g.current)
		}
	}

	return g.hunks
}

func (g *hunksGenerator) processHunk(i int, op Operation) {
	if g.current != nil {
		return
	}

	var ctxPrefix string
	linesBefore := len(g.beforeContext)
	if linesBefore > g.ctxLines {
		ctxPrefix = g.beforeContext[linesBefore-g.ctxLines-1]
		g.beforeContext = g.beforeContext[linesBefore-g.ctxLines:]
		linesBefore = g.ctxLines
	}

	g.current = &hunk{ctxPrefix: strings.TrimSuffix(ctxPrefix, "\n")}
	g.current.AddOp(Equal, g.beforeContext...)

	switch op {
	case Delete:
		g.current.fromLine, g.current.toLine =
			g.addLineNumbers(g.fromLine, g.toLine, linesBefore, i, Add)
	case Add:
		g.current.toLine, g.current.fromLine =
			g.addLineNumbers(g.toLine, g.fromLine, linesBefore, i, Delete)
	}

	g.beforeContext = nil
}

// addLineNumbers obtains the line numbers in a new chunk.
func (g *hunksGenerator) addLineNumbers(la, lb int, linesBefore int, i int, op Operation) (cla, clb int) {
	cla = la - linesBefore
	// we need to search for a reference for the next diff
	switch {
	case linesBefore != 0 && g.ctxLines != 0:
		if lb > g.ctxLines {
			clb = lb - g.ctxLines + 1
		} else {
			clb = 1
		}
	case g.ctxLines == 0:
		clb = lb
	case i != len(g.chunks)-1:
		next := g.chunks[i+1]
		if next.Type() == op || next.Type() == Equal {
			// this diff will be into this chunk
			clb = lb + 1
		}
	}

	return
}

func (g *hunksGenerator) processEqualsLines(ls []string, i int) {
	if g.current == nil {
		g.beforeContext = append(g.beforeContext, ls...)
		return
	}

	g.afterContext = append(g.afterContext, ls...)
	if len(g.afterContext) <= g.ctxLines*2 && i != len(g.chunks)-1 {
		g.current.AddOp(Equal, g.afterContext...)
		g.afterContext = nil
	} else {
		ctxLines := g.ctxLines
		if ctxLines > len(g.afterContext) {
			ctxLines = len(g.afterContext)
		}
		g.current.AddOp(Equal, g.afterContext[:ctxLines]...)
		g.hunks = append(g.hunks, g.current)

		g.current = nil
		g.beforeContext = g.afterContext[ctxLines:]
		g.afterContext = nil
	}
}

func splitLines(s string) []string {
	out := splitLinesRegexp.FindAllString(s, -1)
	if out[len(out)-1] == "" {
		out = out[:len(out)-1]
	}
	return out
}

type hunk struct {
	fromLine int
	toLine   int

	fromCount int
	toCount   int

	ctxPrefix string
	ops       []*op
}

func (h *hunk) writeTo(sb *strings.Builder, color ColorConfig) {
	sb.WriteString(color[Frag])
	sb.WriteString("@@ -")

	if h.fromCount == 1 {
		sb.WriteString(strconv.Itoa(h.fromLine))
	} else {
		sb.WriteString(strconv.Itoa(h.fromLine))
		sb.WriteByte(',')
		sb.WriteString(strconv.Itoa(h.fromCount))
	}

	sb.WriteString(" +")

	if h.toCount == 1 {
		sb.WriteString(strconv.Itoa(h.toLine))
	} else {
		sb.WriteString(strconv.Itoa(h.toLine))
		sb.WriteByte(',')
		sb.WriteString(strconv.Itoa(h.toCount))
	}

	sb.WriteString(" @@")
	sb.WriteString(color.Reset(Frag))

	if h.ctxPrefix != "" {
		sb.WriteByte(' ')
		sb.WriteString(color[Func])
		sb.WriteString(h.ctxPrefix)
		sb.WriteString(color.Reset(Func))
	}

	sb.WriteByte('\n')

	for _, op := range h.ops {
		op.writeTo(sb, color)
	}
}

func (h *hunk) AddOp(t Operation, ss ...string) {
	n := len(ss)
	switch t {
	case Add:
		h.toCount += n
	case Delete:
		h.fromCount += n
	case Equal:
		h.toCount += n
		h.fromCount += n
	}

	for _, s := range ss {
		h.ops = append(h.ops, &op{s, t})
	}
}

type op struct {
	text string
	t    Operation
}

func (o *op) writeTo(sb *strings.Builder, color ColorConfig) {
	colorKey := operationColorKey[o.t]
	sb.WriteString(color[colorKey])
	sb.WriteByte(operationChar[o.t])
	sb.WriteString(o.text)
	sb.WriteString(color.Reset(colorKey))
	if !strings.HasSuffix(o.text, "\n") {
		sb.WriteString("\n\\ No newline at end of file\n")
	}
}