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 }