1// Copyright 2021 The Gitea Authors.
2// All rights reserved.
3// SPDX-License-Identifier: MIT
4
5package pull
6
7import (
8 "bufio"
9 "context"
10 "fmt"
11 "io"
12 "os"
13 "strconv"
14 "strings"
15
16 "forgejo.org/modules/git"
17 "forgejo.org/modules/log"
18)
19
20// lsFileLine is a Quadruplet struct (+error) representing a partially parsed line from ls-files
21type lsFileLine struct {
22 mode string
23 sha string
24 stage int
25 path string
26 err error
27}
28
29// SameAs checks if two lsFileLines are referring to the same path, sha and mode (ignoring stage)
30func (line *lsFileLine) SameAs(other *lsFileLine) bool {
31 if line == nil || other == nil {
32 return false
33 }
34
35 if line.err != nil || other.err != nil {
36 return false
37 }
38
39 return line.mode == other.mode &&
40 line.sha == other.sha &&
41 line.path == other.path
42}
43
44// String provides a string representation for logging
45func (line *lsFileLine) String() string {
46 if line == nil {
47 return "<nil>"
48 }
49 if line.err != nil {
50 return fmt.Sprintf("%d %s %s %s %v", line.stage, line.mode, line.path, line.sha, line.err)
51 }
52 return fmt.Sprintf("%d %s %s %s", line.stage, line.mode, line.path, line.sha)
53}
54
55// readUnmergedLsFileLines calls git ls-files -u -z and parses the lines into mode-sha-stage-path quadruplets
56// it will push these to the provided channel closing it at the end
57func readUnmergedLsFileLines(ctx context.Context, tmpBasePath string, outputChan chan *lsFileLine) {
58 defer func() {
59 // Always close the outputChan at the end of this function
60 close(outputChan)
61 }()
62
63 lsFilesReader, lsFilesWriter, err := os.Pipe()
64 if err != nil {
65 log.Error("Unable to open stderr pipe: %v", err)
66 outputChan <- &lsFileLine{err: fmt.Errorf("unable to open stderr pipe: %w", err)}
67 return
68 }
69 defer func() {
70 _ = lsFilesWriter.Close()
71 _ = lsFilesReader.Close()
72 }()
73
74 stderr := &strings.Builder{}
75 err = git.NewCommand(ctx, "ls-files", "-u", "-z").
76 Run(&git.RunOpts{
77 Dir: tmpBasePath,
78 Stdout: lsFilesWriter,
79 Stderr: stderr,
80 PipelineFunc: func(_ context.Context, _ context.CancelFunc) error {
81 _ = lsFilesWriter.Close()
82 defer func() {
83 _ = lsFilesReader.Close()
84 }()
85 bufferedReader := bufio.NewReader(lsFilesReader)
86
87 for {
88 line, err := bufferedReader.ReadString('\000')
89 if err != nil {
90 if err == io.EOF {
91 return nil
92 }
93 return err
94 }
95 toemit := &lsFileLine{}
96
97 split := strings.SplitN(line, " ", 3)
98 if len(split) < 3 {
99 return fmt.Errorf("malformed line: %s", line)
100 }
101 toemit.mode = split[0]
102 toemit.sha = split[1]
103
104 if len(split[2]) < 4 {
105 return fmt.Errorf("malformed line: %s", line)
106 }
107
108 toemit.stage, err = strconv.Atoi(split[2][0:1])
109 if err != nil {
110 return fmt.Errorf("malformed line: %s", line)
111 }
112
113 toemit.path = split[2][2 : len(split[2])-1]
114 outputChan <- toemit
115 }
116 },
117 })
118 if err != nil {
119 outputChan <- &lsFileLine{err: fmt.Errorf("git ls-files -u -z: %w", git.ConcatenateError(err, stderr.String()))}
120 }
121}
122
123// unmergedFile is triple (+error) of lsFileLines split into stages 1,2 & 3.
124type unmergedFile struct {
125 stage1 *lsFileLine
126 stage2 *lsFileLine
127 stage3 *lsFileLine
128 err error
129}
130
131// String provides a string representation of the an unmerged file for logging
132func (u *unmergedFile) String() string {
133 if u == nil {
134 return "<nil>"
135 }
136 if u.err != nil {
137 return fmt.Sprintf("error: %v\n%v\n%v\n%v", u.err, u.stage1, u.stage2, u.stage3)
138 }
139 return fmt.Sprintf("%v\n%v\n%v", u.stage1, u.stage2, u.stage3)
140}
141
142// unmergedFiles will collate the output from readUnstagedLsFileLines in to file triplets and send them
143// to the provided channel, closing at the end.
144func unmergedFiles(ctx context.Context, tmpBasePath string, unmerged chan *unmergedFile) {
145 defer func() {
146 // Always close the channel
147 close(unmerged)
148 }()
149
150 ctx, cancel := context.WithCancel(ctx)
151 lsFileLineChan := make(chan *lsFileLine, 10) // give lsFileLineChan a buffer
152 go readUnmergedLsFileLines(ctx, tmpBasePath, lsFileLineChan)
153 defer func() {
154 cancel()
155 for range lsFileLineChan {
156 // empty channel
157 }
158 }()
159
160 next := &unmergedFile{}
161 for line := range lsFileLineChan {
162 log.Trace("Got line: %v Current State:\n%v", line, next)
163 if line.err != nil {
164 log.Error("Unable to run ls-files -u -z! Error: %v", line.err)
165 unmerged <- &unmergedFile{err: fmt.Errorf("unable to run ls-files -u -z! Error: %w", line.err)}
166 return
167 }
168
169 // stages are always emitted 1,2,3 but sometimes 1, 2 or 3 are dropped
170 switch line.stage {
171 case 0:
172 // Should not happen as this represents successfully merged file - we will tolerate and ignore though
173 case 1:
174 if next.stage1 != nil || next.stage2 != nil || next.stage3 != nil {
175 // We need to handle the unstaged file stage1,stage2,stage3
176 unmerged <- next
177 }
178 next = &unmergedFile{stage1: line}
179 case 2:
180 if next.stage3 != nil || next.stage2 != nil || (next.stage1 != nil && next.stage1.path != line.path) {
181 // We need to handle the unstaged file stage1,stage2,stage3
182 unmerged <- next
183 next = &unmergedFile{}
184 }
185 next.stage2 = line
186 case 3:
187 if next.stage3 != nil || (next.stage1 != nil && next.stage1.path != line.path) || (next.stage2 != nil && next.stage2.path != line.path) {
188 // We need to handle the unstaged file stage1,stage2,stage3
189 unmerged <- next
190 next = &unmergedFile{}
191 }
192 next.stage3 = line
193 default:
194 log.Error("Unexpected stage %d for path %s in run ls-files -u -z!", line.stage, line.path)
195 unmerged <- &unmergedFile{err: fmt.Errorf("unexpected stage %d for path %s in git ls-files -u -z", line.stage, line.path)}
196 return
197 }
198 }
199 // We need to handle the unstaged file stage1,stage2,stage3
200 if next.stage1 != nil || next.stage2 != nil || next.stage3 != nil {
201 unmerged <- next
202 }
203}