// Copyright 2023 Harness, Inc.//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.package apiimport ("bufio""bytes""context""fmt""io""math""regexp""strconv""strings""github.com/harness/gitness/errors""github.com/harness/gitness/git/command""github.com/harness/gitness/git/parser""github.com/harness/gitness/git/sha""github.com/harness/gitness/types")type FileDiffRequest struct {Path string `json:"path"`StartLine int `json:"start_line"`EndLine int `json:"-"` // warning: changes are possible and this field may not exist in the future}type FileDiffRequests []FileDiffRequesttype DiffShortStat struct {Files intAdditions intDeletions int}// modifyHeader needs to modify diff hunk header with the new start line// and end line with calculated span.// if diff hunk header is -100, 50 +100, 50 and startLine = 120, endLine=140// then we need to modify header to -120,20 +120,20.// warning: changes are possible and param endLine may not exist in the future.func modifyHeader(hunk parser.HunkHeader, startLine, endLine int) []byte {oldStartLine := hunk.OldLinenewStartLine := hunk.NewLineoldSpan := hunk.OldSpannewSpan := hunk.NewSpanoldEndLine := oldStartLine + oldSpannewEndLine := newStartLine + newSpanif startLine > 0 {if startLine < oldEndLine {oldStartLine = startLine}if startLine < newEndLine {newStartLine = startLine}}if endLine > 0 {if endLine < oldEndLine {oldSpan = endLine - startLine} else if oldEndLine > startLine {oldSpan = oldEndLine - startLine}if endLine < newEndLine {newSpan = endLine - startLine} else if newEndLine > startLine {newSpan = newEndLine - startLine}}return fmt.Appendf(nil, "@@ -%d,%d +%d,%d @@",oldStartLine, oldSpan, newStartLine, newSpan)}// cutLinesFromFullFileDiff reads from r and writes to w headers and between// startLine and endLine. if startLine and endLine is equal to 0 then it uses io.Copy// warning: changes are possible and param endLine may not exist in the future////nolint:gocognitfunc cutLinesFromFullFileDiff(w io.Writer, r io.Reader, startLine, endLine int) error {if startLine < 0 {startLine = 0}if endLine < 0 {endLine = 0}if startLine == 0 && endLine > 0 {startLine = 1}if endLine < startLine {endLine = 0}// no need for processing lines just copy the dataif startLine == 0 && endLine == 0 {_, err := io.Copy(w, r)return err}linePos := 0start := falsescanner := bufio.NewScanner(r)for scanner.Scan() {line := scanner.Bytes()if start {linePos++}if endLine > 0 && linePos > endLine {break}if linePos > 0 &&(startLine > 0 && linePos < startLine) {continue}if len(line) >= 2 && bytes.HasPrefix(line, []byte{'@', '@'}) {hunk, ok := parser.ParseDiffHunkHeader(string(line)) // TBD: maybe reader?if !ok {return fmt.Errorf("failed to extract lines from diff, range [%d,%d] : %w",startLine, endLine, ErrParseDiffHunkHeader)}line = modifyHeader(hunk, startLine, endLine)start = true}if _, err := w.Write(line); err != nil {return err}if _, err := w.Write([]byte{'\n'}); err != nil {return err}}return scanner.Err()}//nolint:gocognitfunc (g *Git) RawDiff(ctx context.Context,w io.Writer,repoPath string,baseRef string,headRef string,mergeBase bool,ignoreWhitespace bool,alternates []string,files ...FileDiffRequest,) error {if repoPath == "" {return ErrRepositoryPathEmpty}baseTag, err := g.GetAnnotatedTag(ctx, repoPath, baseRef)if err == nil {baseRef = baseTag.TargetSHA.String()}headTag, err := g.GetAnnotatedTag(ctx, repoPath, headRef)if err == nil {headRef = headTag.TargetSHA.String()}if mergeBase {mergeBaseSHA, _, err := g.GetMergeBase(ctx, repoPath, "", baseRef, headRef, true)if err != nil {return fmt.Errorf("failed to get merge base for %s and %s: %w", baseRef, headRef, err)}baseRef = mergeBaseSHA.String()}cmd := command.New("diff",command.WithFlag("-M"),command.WithFlag("--full-index"),command.WithAlternateObjectDirs(alternates...),)if ignoreWhitespace {// Ignore whitespace when comparing lines.cmd.Add(command.WithFlag("-w"))}perFileDiffRequired := falsepaths := make([]string, 0, len(files))if len(files) > 0 {for _, file := range files {paths = append(paths, file.Path)if file.StartLine > 0 || file.EndLine > 0 {perFileDiffRequired = true}}}processed := 0again:startLine := 0endLine := 0newCmd := cmd.Clone()if len(files) > 0 {startLine = files[processed].StartLineendLine = files[processed].EndLine}if perFileDiffRequired {if startLine > 0 || endLine > 0 {newCmd.Add(command.WithFlag("-U" + strconv.Itoa(math.MaxInt32)))}paths = []string{files[processed].Path}}newCmd.Add(command.WithArg(baseRef, headRef))if len(paths) > 0 {newCmd.Add(command.WithPostSepArg(paths...))}pipeRead, pipeWrite := io.Pipe()go func() {var err errordefer func() {// If running of the command below fails, make the pipe reader also fail with the same error._ = pipeWrite.CloseWithError(err)}()err = newCmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(pipeWrite))if err != nil {err = processGitErrorf(err, "git diff failed between %q and %q", baseRef, headRef)if cErr := command.AsError(err); cErr != nil {if cErr.IsExitCode(128) && cErr.IsBadObject() {err = errors.NotFound("commit not found")}}}}()if err = cutLinesFromFullFileDiff(w, pipeRead, startLine, endLine); err != nil {return err}if perFileDiffRequired {processed++if processed < len(files) {goto again}}return nil}// CommitDiff will stream diff for provided ref.func (g *Git) CommitDiff(ctx context.Context,repoPath string,rev string,ignoreWhitespace bool,w io.Writer,) error {if repoPath == "" {return ErrRepositoryPathEmpty}if rev == "" {return errors.InvalidArgument("git revision cannot be empty")}cmd := command.New("show",command.WithFlag("--full-index"),command.WithFlag("--pretty=format:%b"),command.WithArg(rev),)if ignoreWhitespace {// Ignore whitespace when comparing lines.cmd.Add(command.WithFlag("-w"))}if err := cmd.Run(ctx,command.WithDir(repoPath),command.WithStdout(w),); err != nil {return processGitErrorf(err, "commit diff error")}return nil}func (g *Git) DiffShortStat(ctx context.Context,repoPath string,baseRef string,headRef string,useMergeBase bool,ignoreWhitespace bool,) (DiffShortStat, error) {if repoPath == "" {return DiffShortStat{}, ErrRepositoryPathEmpty}separator := ".."if useMergeBase {separator = "..."}shortstatArgs := []string{baseRef + separator + headRef}if len(baseRef) == 0 || baseRef == types.NilSHA {shortstatArgs = []string{sha.EmptyTree.String(), headRef}}stat, err := GetDiffShortStat(ctx, repoPath, ignoreWhitespace, shortstatArgs...)if err != nil {return DiffShortStat{}, processGitErrorf(err, "failed to get diff short stat between %s and %s",baseRef, headRef)}return stat, nil}// GetDiffHunkHeaders for each file in diff output returns file name (old and new to detect renames),// and all hunk headers. The diffs are generated with unified=0 parameter to create minimum sized hunks.// Hunks' body is ignored.// The purpose of this function is to get data based on which code comments could be repositioned.func (g *Git) GetDiffHunkHeaders(ctx context.Context,repoPath string,targetRef string,sourceRef string,) ([]*parser.DiffFileHunkHeaders, error) {if repoPath == "" {return nil, ErrRepositoryPathEmpty}pipeRead, pipeWrite := io.Pipe()stderr := &bytes.Buffer{}go func() {var err errordefer func() {// If running of the command below fails, make the pipe reader also fail with the same error._ = pipeWrite.CloseWithError(err)}()cmd := command.New("diff",command.WithFlag("--patch"),command.WithFlag("--full-index"),command.WithFlag("--no-color"),command.WithFlag("--unified=0"),command.WithArg(sourceRef),command.WithArg(targetRef),)err = cmd.Run(ctx,command.WithDir(repoPath),command.WithStdout(pipeWrite),command.WithStderr(stderr), // We capture stderr output in a buffer.)}()fileHunkHeaders, err := parser.GetHunkHeaders(pipeRead)// First check if there's something in the stderr buffer, if yes that's the errorif errStderr := parseDiffStderr(stderr); errStderr != nil {return nil, errStderr}// Next check if reading the git diff output caused an errorif err != nil {return nil, err}return fileHunkHeaders, nil}// DiffCut parses full file git diff output and returns lines specified with the parameters.// The purpose of this function is to get diff data with which code comments could be generated.////nolint:gocognitfunc (g *Git) DiffCut(ctx context.Context,repoPath string,targetRef string,sourceRef string,path string,ignoreWhitespace bool,params parser.DiffCutParams,) (parser.HunkHeader, parser.Hunk, error) {if repoPath == "" {return parser.HunkHeader{}, parser.Hunk{}, ErrRepositoryPathEmpty}// first fetch the list of the changed filespipeRead, pipeWrite := io.Pipe()go func() {var err errordefer func() {// If running of the command below fails, make the pipe reader also fail with the same error._ = pipeWrite.CloseWithError(err)}()cmd := command.New("diff",command.WithFlag("--raw"),command.WithFlag("--merge-base"),command.WithFlag("-z"),command.WithFlag("--find-renames"),command.WithArg(targetRef),command.WithArg(sourceRef))if ignoreWhitespace {// Ignore whitespace when comparing lines.cmd.Add(command.WithFlag("-w"))}err = cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(pipeWrite))}()diffEntries, err := parser.DiffRaw(pipeRead)if err != nil {return parser.HunkHeader{}, parser.Hunk{}, fmt.Errorf("failed to find the list of changed files: %w", err)}var oldSHA, newSHA stringfor _, entry := range diffEntries {if entry.Status == parser.DiffStatusRenamed || entry.Status == parser.DiffStatusCopied {// Entries with the status 'R' and 'C' output two paths: the old path and the new path.// Using the params.LineStartNew flag to match the path with the entry's old or new path.if entry.Path != path && entry.OldPath != path {continue}if params.LineStartNew && path == entry.OldPath {msg := "for renamed files provide the new file name if commenting the changed lines"return parser.HunkHeader{}, parser.Hunk{}, errors.InvalidArgument(msg)}if !params.LineStartNew && path == entry.Path {msg := "for renamed files provide the old file name if commenting the old lines"return parser.HunkHeader{}, parser.Hunk{}, errors.InvalidArgument(msg)}} else if entry.Path != path {// All other statuses output just one path: If the path doesn't match it, proceed with the next entry.continue}rawFileMode := entry.OldFileModeif params.LineStartNew {rawFileMode = entry.NewFileMode}fileType, _, err := parseTreeNodeMode(rawFileMode)if err != nil {return parser.HunkHeader{}, parser.Hunk{},fmt.Errorf("failed to parse file mode %s for path %s: %w", rawFileMode, path, err)}switch fileType {default:return parser.HunkHeader{}, parser.Hunk{}, errors.Internal(nil, "unrecognized object type")case TreeNodeTypeCommit:msg := "code comment is not allowed on a submodule"return parser.HunkHeader{}, parser.Hunk{}, errors.InvalidArgument(msg)case TreeNodeTypeTree:msg := "code comment is not allowed on a directory"return parser.HunkHeader{}, parser.Hunk{}, errors.InvalidArgument(msg)case TreeNodeTypeBlob:// a blob is what we want}var hunkHeader parser.HunkHeadervar hunk parser.Hunkswitch entry.Status {case parser.DiffStatusRenamed, parser.DiffStatusCopied, parser.DiffStatusModified:// For modified and renamed compare file blob SHAs directly.oldSHA = entry.OldBlobSHAnewSHA = entry.NewBlobSHAhunkHeader, hunk, err = g.diffCutFromHunk(ctx, repoPath, oldSHA, newSHA, ignoreWhitespace, params)case parser.DiffStatusAdded, parser.DiffStatusDeleted, parser.DiffStatusType:// for added and deleted files read the file content directlyif params.LineStartNew {hunkHeader, hunk, err = g.diffCutFromBlob(ctx, repoPath, true, entry.NewBlobSHA, params)} else {hunkHeader, hunk, err = g.diffCutFromBlob(ctx, repoPath, false, entry.OldBlobSHA, params)}default:return parser.HunkHeader{}, parser.Hunk{},fmt.Errorf("unrecognized git diff file status=%c for path=%s", entry.Status, path)}if err != nil {return parser.HunkHeader{}, parser.Hunk{},fmt.Errorf("failed to extract hunk for git diff file status=%c path=%s: %w", entry.Status, path, err)}// The returned diff hunk will be stored in the DB and will only be used for display, as a reference.// Therefore, we trim too long lines to protect the system and the DB.const maxLineLen = 200parser.LimitLineLen(&hunk.Lines, maxLineLen)return hunkHeader, hunk, nil}return parser.HunkHeader{}, parser.Hunk{}, errors.NotFound("path not found")}func (g *Git) diffCutFromHunk(ctx context.Context,repoPath string,oldSHA string,newSHA string,ignoreWhitespace bool,params parser.DiffCutParams,) (parser.HunkHeader, parser.Hunk, error) {pipeRead, pipeWrite := io.Pipe()stderr := bytes.NewBuffer(nil)go func() {var err errordefer func() {// If running of the command below fails, make the pipe reader also fail with the same error._ = pipeWrite.CloseWithError(err)}()cmd := command.New("diff",command.WithFlag("--patch"),command.WithFlag("--full-index"),command.WithFlag("--no-color"),command.WithFlag("--unified=100000000"),command.WithArg(oldSHA),command.WithArg(newSHA))if ignoreWhitespace {// Ignore whitespace when comparing lines.cmd.Add(command.WithFlag("-w"))}err = cmd.Run(ctx,command.WithDir(repoPath),command.WithStdout(pipeWrite),command.WithStderr(stderr))}()diffCutHeader, linesHunk, err := parser.DiffCut(pipeRead, params)if errStderr := parseDiffStderr(stderr); errStderr != nil {// First check if there's something in the stderr buffer, if yes that's the errorreturn parser.HunkHeader{}, parser.Hunk{}, errStderr}if err != nil {// Next check if reading the git diff output caused an errorreturn parser.HunkHeader{}, parser.Hunk{}, err}return diffCutHeader, linesHunk, nil}func (g *Git) diffCutFromBlob(ctx context.Context,repoPath string,asAdded bool,sha string,params parser.DiffCutParams,) (parser.HunkHeader, parser.Hunk, error) {pipeRead, pipeWrite := io.Pipe()go func() {var err errordefer func() {// If running of the command below fails, make the pipe reader also fail with the same error._ = pipeWrite.CloseWithError(err)}()cmd := command.New("cat-file",command.WithFlag("-p"),command.WithArg(sha))err = cmd.Run(ctx,command.WithDir(repoPath),command.WithStdout(pipeWrite))}()cutHeader, cut, err := parser.BlobCut(pipeRead, params)if err != nil {// Next check if reading the git diff output caused an errorreturn parser.HunkHeader{}, parser.Hunk{}, err}// Convert parser.CutHeader to parser.HunkHeader and parser.Cut to parser.Hunk.var hunkHeader parser.HunkHeadervar hunk parser.Hunkif asAdded {for i := range cut.Lines {cut.Lines[i] = "+" + cut.Lines[i]}hunkHeader = parser.HunkHeader{NewLine: cutHeader.Line,NewSpan: cutHeader.Span,}hunk = parser.Hunk{HunkHeader: parser.HunkHeader{NewLine: cut.Line, NewSpan: cut.Span},Lines: cut.Lines,}} else {for i := range cut.Lines {cut.Lines[i] = "-" + cut.Lines[i]}hunkHeader = parser.HunkHeader{OldLine: cutHeader.Line,OldSpan: cutHeader.Span,}hunk = parser.Hunk{HunkHeader: parser.HunkHeader{OldLine: cut.Line, OldSpan: cut.Span},Lines: cut.Lines,}}return hunkHeader, hunk, nil}func (g *Git) DiffFileName(ctx context.Context,repoPath string,baseRef string,headRef string,mergeBase bool,ignoreWhitespace bool,) ([]string, error) {cmd := command.New("diff", command.WithFlag("--name-only"))if mergeBase {cmd.Add(command.WithFlag("--merge-base"))}cmd.Add(command.WithArg(baseRef, headRef))if ignoreWhitespace {// Ignore whitespace when comparing lines.cmd.Add(command.WithFlag("-w"))}stdout := &bytes.Buffer{}err := cmd.Run(ctx,command.WithDir(repoPath),command.WithStdout(stdout),)if err != nil {return nil, processGitErrorf(err, "failed to trigger diff command")}return parseLinesToSlice(stdout.Bytes()), nil}// GetDiffShortStat counts number of changed files, number of additions and deletions.func GetDiffShortStat(ctx context.Context,repoPath string,ignoreWhitespace bool,args ...string,) (DiffShortStat, error) {// Now if we call:// $ git diff --shortstat 1ebb35b98889ff77299f24d82da426b434b0cca0...788b8b1440462d477f45b0088875// we get:// " 9902 files changed, 2034198 insertions(+), 298800 deletions(-)\n"cmd := command.New("diff",command.WithFlag("--shortstat"),command.WithArg(args...),)if ignoreWhitespace {// Ignore whitespace when comparing lines.cmd.Add(command.WithFlag("-w"))}stdout := &bytes.Buffer{}if err := cmd.Run(ctx,command.WithDir(repoPath),command.WithStdout(stdout),); err != nil {return DiffShortStat{}, err}return parseDiffStat(stdout.String())}var shortStatFormat = regexp.MustCompile(`\s*(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?`)func parseDiffStat(stdout string) (stat DiffShortStat, err error) {if len(stdout) == 0 || stdout == "\n" {return DiffShortStat{}, nil}groups := shortStatFormat.FindStringSubmatch(stdout)if len(groups) != 4 {return DiffShortStat{}, fmt.Errorf("unable to parse shortstat: %s groups: %s", stdout, groups)}stat.Files, err = strconv.Atoi(groups[1])if err != nil {return DiffShortStat{}, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumFiles %w",stdout, err)}if len(groups[2]) != 0 {stat.Additions, err = strconv.Atoi(groups[2])if err != nil {return DiffShortStat{}, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumAdditions %w",stdout, err)}}if len(groups[3]) != 0 {stat.Deletions, err = strconv.Atoi(groups[3])if err != nil {return DiffShortStat{}, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumDeletions %w",stdout, err)}}return stat, nil}func parseDiffStderr(stderr *bytes.Buffer) error {errRaw := stderr.String() // assume there will never be a lot of output to stdoutif len(errRaw) == 0 {return nil}if idx := strings.IndexByte(errRaw, '\n'); idx > 0 {errRaw = errRaw[:idx] // get only the first line of the output}errRaw = strings.TrimPrefix(errRaw, "fatal: ") // git errors start with the "fatal: " prefixif strings.Contains(errRaw, "bad revision") {return parser.ErrSHADoesNotMatch}return errors.New(errRaw)}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。