code.gitea.io/gitea@v1.19.3/modules/git/diff.go (about)

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package git
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  
    17  	"code.gitea.io/gitea/modules/log"
    18  )
    19  
    20  // RawDiffType type of a raw diff.
    21  type RawDiffType string
    22  
    23  // RawDiffType possible values.
    24  const (
    25  	RawDiffNormal RawDiffType = "diff"
    26  	RawDiffPatch  RawDiffType = "patch"
    27  )
    28  
    29  // GetRawDiff dumps diff results of repository in given commit ID to io.Writer.
    30  func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer io.Writer) error {
    31  	return GetRepoRawDiffForFile(repo, "", commitID, diffType, "", writer)
    32  }
    33  
    34  // GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer.
    35  func GetReverseRawDiff(ctx context.Context, repoPath, commitID string, writer io.Writer) error {
    36  	stderr := new(bytes.Buffer)
    37  	cmd := NewCommand(ctx, "show", "--pretty=format:revert %H%n", "-R").AddDynamicArguments(commitID)
    38  	if err := cmd.Run(&RunOpts{
    39  		Dir:    repoPath,
    40  		Stdout: writer,
    41  		Stderr: stderr,
    42  	}); err != nil {
    43  		return fmt.Errorf("Run: %w - %s", err, stderr)
    44  	}
    45  	return nil
    46  }
    47  
    48  // GetRepoRawDiffForFile dumps diff results of file in given commit ID to io.Writer according given repository
    49  func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
    50  	commit, err := repo.GetCommit(endCommit)
    51  	if err != nil {
    52  		return err
    53  	}
    54  	var files []string
    55  	if len(file) > 0 {
    56  		files = append(files, file)
    57  	}
    58  
    59  	cmd := NewCommand(repo.Ctx)
    60  	switch diffType {
    61  	case RawDiffNormal:
    62  		if len(startCommit) != 0 {
    63  			cmd.AddArguments("diff", "-M").AddDynamicArguments(startCommit, endCommit).AddDashesAndList(files...)
    64  		} else if commit.ParentCount() == 0 {
    65  			cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...)
    66  		} else {
    67  			c, _ := commit.Parent(0)
    68  			cmd.AddArguments("diff", "-M").AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...)
    69  		}
    70  	case RawDiffPatch:
    71  		if len(startCommit) != 0 {
    72  			query := fmt.Sprintf("%s...%s", endCommit, startCommit)
    73  			cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(query).AddDashesAndList(files...)
    74  		} else if commit.ParentCount() == 0 {
    75  			cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...)
    76  		} else {
    77  			c, _ := commit.Parent(0)
    78  			query := fmt.Sprintf("%s...%s", endCommit, c.ID.String())
    79  			cmd.AddArguments("format-patch", "--no-signature", "--stdout").AddDynamicArguments(query).AddDashesAndList(files...)
    80  		}
    81  	default:
    82  		return fmt.Errorf("invalid diffType: %s", diffType)
    83  	}
    84  
    85  	stderr := new(bytes.Buffer)
    86  	if err = cmd.Run(&RunOpts{
    87  		Dir:    repo.Path,
    88  		Stdout: writer,
    89  		Stderr: stderr,
    90  	}); err != nil {
    91  		return fmt.Errorf("Run: %w - %s", err, stderr)
    92  	}
    93  	return nil
    94  }
    95  
    96  // ParseDiffHunkString parse the diffhunk content and return
    97  func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHunk int) {
    98  	ss := strings.Split(diffhunk, "@@")
    99  	ranges := strings.Split(ss[1][1:], " ")
   100  	leftRange := strings.Split(ranges[0], ",")
   101  	leftLine, _ = strconv.Atoi(leftRange[0][1:])
   102  	if len(leftRange) > 1 {
   103  		leftHunk, _ = strconv.Atoi(leftRange[1])
   104  	}
   105  	if len(ranges) > 1 {
   106  		rightRange := strings.Split(ranges[1], ",")
   107  		rightLine, _ = strconv.Atoi(rightRange[0])
   108  		if len(rightRange) > 1 {
   109  			righHunk, _ = strconv.Atoi(rightRange[1])
   110  		}
   111  	} else {
   112  		log.Debug("Parse line number failed: %v", diffhunk)
   113  		rightLine = leftLine
   114  		righHunk = leftHunk
   115  	}
   116  	return leftLine, leftHunk, rightLine, righHunk
   117  }
   118  
   119  // Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
   120  var hunkRegex = regexp.MustCompile(`^@@ -(?P<beginOld>[0-9]+)(,(?P<endOld>[0-9]+))? \+(?P<beginNew>[0-9]+)(,(?P<endNew>[0-9]+))? @@`)
   121  
   122  const cmdDiffHead = "diff --git "
   123  
   124  func isHeader(lof string, inHunk bool) bool {
   125  	return strings.HasPrefix(lof, cmdDiffHead) || (!inHunk && (strings.HasPrefix(lof, "---") || strings.HasPrefix(lof, "+++")))
   126  }
   127  
   128  // CutDiffAroundLine cuts a diff of a file in way that only the given line + numberOfLine above it will be shown
   129  // it also recalculates hunks and adds the appropriate headers to the new diff.
   130  // Warning: Only one-file diffs are allowed.
   131  func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLine int) (string, error) {
   132  	if line == 0 || numbersOfLine == 0 {
   133  		// no line or num of lines => no diff
   134  		return "", nil
   135  	}
   136  
   137  	scanner := bufio.NewScanner(originalDiff)
   138  	hunk := make([]string, 0)
   139  
   140  	// begin is the start of the hunk containing searched line
   141  	// end is the end of the hunk ...
   142  	// currentLine is the line number on the side of the searched line (differentiated by old)
   143  	// otherLine is the line number on the opposite side of the searched line (differentiated by old)
   144  	var begin, end, currentLine, otherLine int64
   145  	var headerLines int
   146  
   147  	inHunk := false
   148  
   149  	for scanner.Scan() {
   150  		lof := scanner.Text()
   151  		// Add header to enable parsing
   152  
   153  		if isHeader(lof, inHunk) {
   154  			if strings.HasPrefix(lof, cmdDiffHead) {
   155  				inHunk = false
   156  			}
   157  			hunk = append(hunk, lof)
   158  			headerLines++
   159  		}
   160  		if currentLine > line {
   161  			break
   162  		}
   163  		// Detect "hunk" with contains commented lof
   164  		if strings.HasPrefix(lof, "@@") {
   165  			inHunk = true
   166  			// Already got our hunk. End of hunk detected!
   167  			if len(hunk) > headerLines {
   168  				break
   169  			}
   170  			// A map with named groups of our regex to recognize them later more easily
   171  			submatches := hunkRegex.FindStringSubmatch(lof)
   172  			groups := make(map[string]string)
   173  			for i, name := range hunkRegex.SubexpNames() {
   174  				if i != 0 && name != "" {
   175  					groups[name] = submatches[i]
   176  				}
   177  			}
   178  			if old {
   179  				begin, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
   180  				end, _ = strconv.ParseInt(groups["endOld"], 10, 64)
   181  				// init otherLine with begin of opposite side
   182  				otherLine, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
   183  			} else {
   184  				begin, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
   185  				if groups["endNew"] != "" {
   186  					end, _ = strconv.ParseInt(groups["endNew"], 10, 64)
   187  				} else {
   188  					end = 0
   189  				}
   190  				// init otherLine with begin of opposite side
   191  				otherLine, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
   192  			}
   193  			end += begin // end is for real only the number of lines in hunk
   194  			// lof is between begin and end
   195  			if begin <= line && end >= line {
   196  				hunk = append(hunk, lof)
   197  				currentLine = begin
   198  				continue
   199  			}
   200  		} else if len(hunk) > headerLines {
   201  			hunk = append(hunk, lof)
   202  			// Count lines in context
   203  			switch lof[0] {
   204  			case '+':
   205  				if !old {
   206  					currentLine++
   207  				} else {
   208  					otherLine++
   209  				}
   210  			case '-':
   211  				if old {
   212  					currentLine++
   213  				} else {
   214  					otherLine++
   215  				}
   216  			case '\\':
   217  				// FIXME: handle `\ No newline at end of file`
   218  			default:
   219  				currentLine++
   220  				otherLine++
   221  			}
   222  		}
   223  	}
   224  	if err := scanner.Err(); err != nil {
   225  		return "", err
   226  	}
   227  
   228  	// No hunk found
   229  	if currentLine == 0 {
   230  		return "", nil
   231  	}
   232  	// headerLines + hunkLine (1) = totalNonCodeLines
   233  	if len(hunk)-headerLines-1 <= numbersOfLine {
   234  		// No need to cut the hunk => return existing hunk
   235  		return strings.Join(hunk, "\n"), nil
   236  	}
   237  	var oldBegin, oldNumOfLines, newBegin, newNumOfLines int64
   238  	if old {
   239  		oldBegin = currentLine
   240  		newBegin = otherLine
   241  	} else {
   242  		oldBegin = otherLine
   243  		newBegin = currentLine
   244  	}
   245  	// headers + hunk header
   246  	newHunk := make([]string, headerLines)
   247  	// transfer existing headers
   248  	copy(newHunk, hunk[:headerLines])
   249  	// transfer last n lines
   250  	newHunk = append(newHunk, hunk[len(hunk)-numbersOfLine-1:]...)
   251  	// calculate newBegin, ... by counting lines
   252  	for i := len(hunk) - 1; i >= len(hunk)-numbersOfLine; i-- {
   253  		switch hunk[i][0] {
   254  		case '+':
   255  			newBegin--
   256  			newNumOfLines++
   257  		case '-':
   258  			oldBegin--
   259  			oldNumOfLines++
   260  		default:
   261  			oldBegin--
   262  			newBegin--
   263  			newNumOfLines++
   264  			oldNumOfLines++
   265  		}
   266  	}
   267  	// construct the new hunk header
   268  	newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
   269  		oldBegin, oldNumOfLines, newBegin, newNumOfLines)
   270  	return strings.Join(newHunk, "\n"), nil
   271  }
   272  
   273  // GetAffectedFiles returns the affected files between two commits
   274  func GetAffectedFiles(repo *Repository, oldCommitID, newCommitID string, env []string) ([]string, error) {
   275  	stdoutReader, stdoutWriter, err := os.Pipe()
   276  	if err != nil {
   277  		log.Error("Unable to create os.Pipe for %s", repo.Path)
   278  		return nil, err
   279  	}
   280  	defer func() {
   281  		_ = stdoutReader.Close()
   282  		_ = stdoutWriter.Close()
   283  	}()
   284  
   285  	affectedFiles := make([]string, 0, 32)
   286  
   287  	// Run `git diff --name-only` to get the names of the changed files
   288  	err = NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID).
   289  		Run(&RunOpts{
   290  			Env:    env,
   291  			Dir:    repo.Path,
   292  			Stdout: stdoutWriter,
   293  			PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
   294  				// Close the writer end of the pipe to begin processing
   295  				_ = stdoutWriter.Close()
   296  				defer func() {
   297  					// Close the reader on return to terminate the git command if necessary
   298  					_ = stdoutReader.Close()
   299  				}()
   300  				// Now scan the output from the command
   301  				scanner := bufio.NewScanner(stdoutReader)
   302  				for scanner.Scan() {
   303  					path := strings.TrimSpace(scanner.Text())
   304  					if len(path) == 0 {
   305  						continue
   306  					}
   307  					affectedFiles = append(affectedFiles, path)
   308  				}
   309  				return scanner.Err()
   310  			},
   311  		})
   312  	if err != nil {
   313  		log.Error("Unable to get affected files for commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err)
   314  	}
   315  
   316  	return affectedFiles, err
   317  }