1
// TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2020-2021, 2023 - TortoiseGit
5 // This program is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License
7 // as published by the Free Software Foundation; either version 2
8 // of the License, or (at your option) any later version.
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software Foundation,
17 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #include "StagingOperations.h"
24 bool StagingOperations::IsWithinFileHeader(int line
) const
26 DiffLineTypes type
= m_lines
->GetLineType(line
);
27 if (m_lines
->IsNoNewlineComment(line
))
29 return (type
== DiffLineTypes::COMMAND
|| type
== DiffLineTypes::COMMENT
|| type
== DiffLineTypes::HEADER
);
32 // From (and including) given line, looks backwards for a hunk start line (@@xxxxxxxx@@),
33 // up to and including given topBoundaryLine. Returns -1 if no hunk start is found.
34 int StagingOperations::FindHunkStartBackwardsFrom(int line
, int topBoundaryLine
) const
36 for (int i
= line
; i
>= topBoundaryLine
; --i
)
38 if (m_lines
->GetLineType(i
) == DiffLineTypes::POSITION
)
44 // From (and including) given line, looks forwards for a hunk start line (@@xxxxxxxx@@),
45 // up to and including given bottomBoundaryLine. Returns -1 if no hunk start is found.
46 int StagingOperations::FindHunkStartForwardsFrom(int line
, int bottomBoundaryLine
) const
48 for (int i
= line
; i
<= bottomBoundaryLine
; ++i
)
50 if (m_lines
->GetLineType(i
) == DiffLineTypes::POSITION
)
56 // Try to find the last line within a hunk looking forwards from given line up to the end of the patch.
57 // Works by first looking backwards until a hunk start is found, up to and including given topBoundaryLine,
58 // then finds out the hunk's length from the counts in the hunk start pattern.
59 // Returns -1 if no hunk start is found.
60 // If the end of the patch is reached, returns the line number of the last line.
61 int StagingOperations::FindHunkEndForwardsFrom(int line
, int topBoundaryLine
) const
63 const int hunkStart
= FindHunkStartBackwardsFrom(line
, topBoundaryLine
);
66 auto strHunkStart
= m_lines
->GetFullLineByLineNumber(hunkStart
);
68 int oldCount
, newCount
;
69 if (!CDiffLinesForStaging::GetOldAndNewLinesCountFromHunk(strHunkStart
, &oldCount
, &newCount
))
72 return FindHunkEndGivenHunkStartAndCounts(hunkStart
, oldCount
, newCount
);
75 // Given the line number of a hunk header and its old and new lines counts, iterates its lines
76 // to find out the line number of the last line within the hunk and returns it.
77 // If the end of the patch is reached, returns the line number of the last line.
78 // It will not work for files that were added or deleted, i.e., when either oldCount or
79 // newCount is 0, but that does not matter since the partial staging
80 // functionality is meant to be used only with *modified* files.
81 int StagingOperations::FindHunkEndGivenHunkStartAndCounts(int hunkStart
, int oldCount
, int newCount
) const
83 if (oldCount
== 0 || newCount
== 0)
84 return -1; // Not applicable (file was added or deleted)
85 const int lastDocumentLine
= m_lines
->GetLastDocumentLine();
86 int i
= hunkStart
+ 1;
87 for (; i
<= lastDocumentLine
; ++i
)
89 DiffLineTypes type
= m_lines
->GetLineType(i
);
90 if (type
== DiffLineTypes::DELETED
)
92 else if (type
== DiffLineTypes::ADDED
)
94 else if (type
== DiffLineTypes::DEFAULT
)
100 if (oldCount
== 0 && newCount
== 0)
102 if (i
+ 1 <= lastDocumentLine
&& m_lines
->IsNoNewlineComment(i
+ 1))
109 return -1; // corrupt diff
112 // From (and including) given line, looks backwards for a line that is neither a hunk header (@@xxxxx@@) nor a
113 // context/added/deleted line. From there, it goes on looking backwards for a "diff" line. A buffer is then returned
114 // containing those lines and any lines between them, including EOL characters. Returns nullptr if no such sequence is found.
115 std::string
StagingOperations::FindFileHeaderBackwardsFrom(int line
) const
120 DiffLineTypes type
= m_lines
->GetLineType(i
);
121 if (type
!= DiffLineTypes::POSITION
&& type
!= DiffLineTypes::DEFAULT
&& type
!= DiffLineTypes::ADDED
&& type
!= DiffLineTypes::DELETED
&& !m_lines
->IsNoNewlineComment(i
))
126 const int fileHeaderLastLine
= i
;
129 if (m_lines
->GetLineType(i
) == DiffLineTypes::COMMAND
)
134 const int fileHeaderFirstLine
= i
;
135 return m_lines
->GetFullTextOfLineRange(fileHeaderFirstLine
, fileHeaderLastLine
);
138 // According to the user selection, returns a buffer holding a temporary patch which must be written to a temporary
139 // file and applied to the index with git apply --cached (for staging) or git apply --cached -R (for unstaging).
140 // Even though this feature is intended for single-file diffs only, this code should also work for multi-file diffs.
141 std::string
StagingOperations::CreatePatchBufferToStageOrUnstageSelectedHunks() const
143 // Try to find a hunk backwards up to and including the first line in the patch
144 int startline
= FindHunkStartBackwardsFrom(m_lines
->GetFirstLineNumberSelected(), 0);
145 // If the selection starts before the first hunk in the patch, startline is now -1
147 // If the selection starts within the headers between files, set startline = -1
148 // so that the code below will go looking forwards instead
149 if (IsWithinFileHeader(m_lines
->GetFirstLineNumberSelected()))
154 // Try to find a hunk forwards up to and including the last line selected
155 startline
= FindHunkStartForwardsFrom(m_lines
->GetFirstLineNumberSelected(), m_lines
->GetLastLineNumberSelected());
157 return {}; // No part of a hunk is selected, bail
159 const int endline
= FindHunkEndForwardsFrom(m_lines
->GetLastLineNumberSelected(), startline
);
163 if (endline
<= startline
) // this should never happen
165 auto hunksWithoutFirstFileHeader
= m_lines
->GetFullTextOfLineRange(startline
, endline
);
166 auto firstFileHeader
= FindFileHeaderBackwardsFrom(startline
);
168 std::string fullTempPatch
;
169 fullTempPatch
.append(firstFileHeader
);
170 fullTempPatch
.append(hunksWithoutFirstFileHeader
);
171 return fullTempPatch
;
174 // According to the user selection, returns a buffer holding a temporary patch which must be written to a temporary
175 // file and applied to the index with git apply --cached (for staging) or git apply --cached -R (for unstaging).
176 // This will not work for multi-file diffs.
177 // This needs to take as parameter whether we're doing a staging or an unstaging, since the handling for those is different.
178 std::string
StagingOperations::CreatePatchBufferToStageOrUnstageSelectedLines(StagingType stagingType
) const
180 // Try to find a hunk backwards up to and including the first line in the patch
181 int firstHunkStartLine
= FindHunkStartBackwardsFrom(m_lines
->GetFirstLineNumberSelected(), 0);
182 // If the selection starts before the first hunk in the patch, startline is now -1
184 if (firstHunkStartLine
== -1)
186 // Try to find a hunk forwards up to and including the last line selected
187 firstHunkStartLine
= FindHunkStartForwardsFrom(m_lines
->GetFirstLineNumberSelected(), m_lines
->GetLastLineNumberSelected());
188 if (firstHunkStartLine
== -1)
189 return {}; // No part of a hunk is selected, bail
192 std::string fullTempPatch
;
193 std::string firstHunkWithoutStartLine
;
194 std::string lastHunkWithoutStartLine
;
196 auto strFirstHunkStartLine
= m_lines
->GetFullLineByLineNumber(firstHunkStartLine
);
197 int firstHunkOldCount
, firstHunkNewCount
;
198 if (!CDiffLinesForStaging::GetOldAndNewLinesCountFromHunk(strFirstHunkStartLine
, &firstHunkOldCount
, &firstHunkNewCount
))
200 int firstHunkLastLine
= FindHunkEndGivenHunkStartAndCounts(firstHunkStartLine
, firstHunkOldCount
, firstHunkNewCount
);
201 if (firstHunkLastLine
== -1)
204 const int firstLineSelected
= m_lines
->GetFirstLineNumberSelected();
205 const int lastLineSelected
= m_lines
->GetLastLineNumberSelected();
207 bool includeFirstHunkAtAll
= ParseHunkOnEitherSelectionBoundary(firstHunkWithoutStartLine
, firstHunkStartLine
, firstHunkLastLine
, firstLineSelected
, lastLineSelected
, &firstHunkOldCount
, &firstHunkNewCount
, stagingType
);
209 auto firstFileHeader
= FindFileHeaderBackwardsFrom(firstHunkStartLine
);
210 fullTempPatch
.append(firstFileHeader
);
212 // If no modified line is selected in the first (or the last) hunk, we must discard it entirely or else git would complain
213 // about a corrupt patch (unless we passed --recount to git apply, but that could potentially cause other issues)
214 if (includeFirstHunkAtAll
)
216 auto strHunkStartLineChanged
= ChangeOldAndNewLinesCount(std::string(strFirstHunkStartLine
), firstHunkOldCount
, firstHunkNewCount
);
218 fullTempPatch
.append(strHunkStartLineChanged
);
219 fullTempPatch
.append(firstHunkWithoutStartLine
);
222 const int lastHunkStartLine
= FindHunkStartBackwardsFrom(lastLineSelected
, firstHunkStartLine
); // firstHunkLastLine + 1);
223 if (lastHunkStartLine
== -1)
226 // For line staging, we only support one file at a time, so we just assume the next hunk starts
227 // at the next line from the end of the first hunk and don't bother looking for a file header again
228 auto inBetweenLines
= m_lines
->GetFullTextOfLineRange(firstHunkLastLine
+ 1, lastHunkStartLine
- 1);
229 if (!inBetweenLines
.empty())
230 fullTempPatch
.append(inBetweenLines
);
232 const int lastHunkLastLine
= FindHunkEndForwardsFrom(lastHunkStartLine
, lastHunkStartLine
);
233 if (lastHunkLastLine
== -1)
235 if (firstHunkStartLine
== lastHunkStartLine
)
237 if (includeFirstHunkAtAll
)
238 return fullTempPatch
;
242 auto strLastHunkStartLine
= m_lines
->GetFullLineByLineNumber(lastHunkStartLine
);
243 int lastHunkOldCount
, lastHunkNewCount
;
244 if (!CDiffLinesForStaging::GetOldAndNewLinesCountFromHunk(strLastHunkStartLine
, &lastHunkOldCount
, &lastHunkNewCount
))
247 const bool includeLastHunkAtAll
= ParseHunkOnEitherSelectionBoundary(lastHunkWithoutStartLine
, lastHunkStartLine
, lastHunkLastLine
, firstLineSelected
, lastLineSelected
, &lastHunkOldCount
, &lastHunkNewCount
, stagingType
);
248 if (includeLastHunkAtAll
)
250 auto strHunkStartLineChanged
= ChangeOldAndNewLinesCount(std::string(strLastHunkStartLine
), lastHunkOldCount
, lastHunkNewCount
);
252 fullTempPatch
.append(strHunkStartLineChanged
);
253 fullTempPatch
.append(lastHunkWithoutStartLine
);
256 if (!includeFirstHunkAtAll
&& inBetweenLines
.empty() && !includeLastHunkAtAll
)
259 return fullTempPatch
;
262 // Takes a buffer containing the first line of a hunk (@@xxxxxx@@)
263 // Returns a new buffer with its old lines count and new lines count changed to the given ones.
264 std::string
StagingOperations::ChangeOldAndNewLinesCount(const std::string
& strHunkStart
, int oldCount
, int newCount
) const
266 std::string pattern
= "^@@ -(\\d+?),(\\d+?) \\+(\\d+?),(\\d+?) @@";
267 std::regex
rx(pattern
, std::regex_constants::ECMAScript
);
269 auto fmt
= std::make_unique
<char[]>(1024);
270 sprintf_s(fmt
.get(), 1024, "@@ -$1,%d +$3,%d @@", oldCount
, newCount
);
271 return std::regex_replace(strHunkStart
, rx
, fmt
.get());
274 // For line staging/unstaging.
275 // Writes to the given hunkWithoutStartLine all the lines of a hunk that need to be included in the temporary
276 // patch for staging/unstaging, according to the user selection. The starting @@xxxxx@@ is not written.
277 // The algorithm for determining which lines need to be included is (for staging):
278 // "-" lines outside the user selection are turned into context (" ") lines
279 // "+" lines outside the user selection are removed
280 // For unstaging, it's the same as above except that "-" and "+" are swapped:
281 // "-" lines outside the user selection are removed
282 // "+" lines outside the user selection are turned into context (" ") lines
283 // The given newCount and oldCount are modified accordingly.
284 // Returns true if at least one + or - line is within the user selection, false otherwise (meaning the hunk must be discarded entirely)
285 // This needs to take as parameter whether we're doing a staging or an unstaging, since the handling for those is different.
286 bool StagingOperations::ParseHunkOnEitherSelectionBoundary(std::string
& hunkWithoutStartLine
, int hunkStartLine
, int hunkLastLine
, int firstLineSelected
, int lastLineSelected
, int* oldCount
, int* newCount
, StagingType stagingType
) const
288 bool includeHunkAtAll
= false;
289 for (int i
= hunkStartLine
+ 1; i
<= hunkLastLine
; ++i
)
291 const DiffLineTypes type
= m_lines
->GetLineType(i
);
292 auto strLine
= m_lines
->GetFullLineByLineNumber(i
);
293 if (type
== DiffLineTypes::DEFAULT
|| type
== DiffLineTypes::NO_NEWLINE_BOTHFILES
)
294 hunkWithoutStartLine
.append(strLine
);
295 else if (type
== DiffLineTypes::ADDED
|| type
== DiffLineTypes::NO_NEWLINE_NEWFILE
)
297 if (i
< firstLineSelected
|| i
> lastLineSelected
) // outside the user selection
299 if (stagingType
== StagingType::StageLines
)
301 if (type
== DiffLineTypes::ADDED
) // hunk counts do not consider "\ No newline at end of file"
304 else if (stagingType
== StagingType::UnstageLines
)
306 if (type
== DiffLineTypes::ADDED
) // hunk counts do not consider "\ No newline at end of file"
308 // Turn it into a context line
309 hunkWithoutStartLine
.append(" ");
310 strLine
.remove_prefix(1);
313 hunkWithoutStartLine
.append(strLine
);
318 if (type
== DiffLineTypes::ADDED
)
319 includeHunkAtAll
= true;
320 hunkWithoutStartLine
.append(strLine
);
323 else if (type
== DiffLineTypes::DELETED
|| type
== DiffLineTypes::NO_NEWLINE_OLDFILE
)
325 if (i
< firstLineSelected
|| i
> lastLineSelected
) // outside the user selection
327 if (stagingType
== StagingType::StageLines
)
329 if (type
== DiffLineTypes::DELETED
) // hunk counts do not consider "\ No newline at end of file"
331 // Turn it into a context line
332 hunkWithoutStartLine
.append(" ");
333 strLine
.remove_prefix(1);
336 hunkWithoutStartLine
.append(strLine
);
338 else if (stagingType
== StagingType::UnstageLines
)
340 if (type
== DiffLineTypes::DELETED
) // hunk counts do not consider "\ No newline at end of file"
346 if (type
== DiffLineTypes::DELETED
)
347 includeHunkAtAll
= true;
348 hunkWithoutStartLine
.append(strLine
);
352 return includeHunkAtAll
;
355 // Creates a temporary file and writes to it the given buffer.
356 // Returns the path of the created file.
357 CString
StagingOperations::WritePatchBufferToTemporaryFile(const std::string
& data
)
359 CString tempFile
= ::GetTempFile();
360 if (tempFile
.IsEmpty())
364 _wfopen_s(&fp
, tempFile
, L
"w+b");
368 fwrite(data
.c_str(), sizeof(char), data
.length(), fp
);