Fixed issue #4126: Capitalize the first letter in the Push dialog
[TortoiseGit.git] / src / TortoiseProc / StagingOperations.cpp
blob3311c83378e5e76c00f7c7285de27a618591a888
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.
19 #include <stdafx.h>
20 #include "StagingOperations.h"
21 #include <regex>
22 #include "Git.h"
24 bool StagingOperations::IsWithinFileHeader(int line) const
26 DiffLineTypes type = m_lines->GetLineType(line);
27 if (m_lines->IsNoNewlineComment(line))
28 return false;
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)
39 return i;
41 return -1;
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)
51 return i;
53 return -1;
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);
64 if (hunkStart == -1)
65 return -1;
66 auto strHunkStart = m_lines->GetFullLineByLineNumber(hunkStart);
68 int oldCount, newCount;
69 if (!CDiffLinesForStaging::GetOldAndNewLinesCountFromHunk(strHunkStart, &oldCount, &newCount))
70 return -1;
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)
91 --oldCount;
92 else if (type == DiffLineTypes::ADDED)
93 --newCount;
94 else if (type == DiffLineTypes::DEFAULT)
96 --oldCount;
97 --newCount;
100 if (oldCount == 0 && newCount == 0)
102 if (i + 1 <= lastDocumentLine && m_lines->IsNoNewlineComment(i + 1))
103 return i + 1;
104 else
105 return i;
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
117 int i = line;
118 for (; i > -1; --i)
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))
122 break;
124 if (i == -1)
125 return {};
126 const int fileHeaderLastLine = i;
127 for (; i > -1; --i)
129 if (m_lines->GetLineType(i) == DiffLineTypes::COMMAND)
130 break;
132 if (i == -1)
133 return {};
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()))
150 startline = -1;
152 if (startline == -1)
154 // Try to find a hunk forwards up to and including the last line selected
155 startline = FindHunkStartForwardsFrom(m_lines->GetFirstLineNumberSelected(), m_lines->GetLastLineNumberSelected());
156 if (startline == -1)
157 return {}; // No part of a hunk is selected, bail
159 const int endline = FindHunkEndForwardsFrom(m_lines->GetLastLineNumberSelected(), startline);
160 if (endline == -1)
161 return {};
163 if (endline <= startline) // this should never happen
164 return {};
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))
199 return {};
200 int firstHunkLastLine = FindHunkEndGivenHunkStartAndCounts(firstHunkStartLine, firstHunkOldCount, firstHunkNewCount);
201 if (firstHunkLastLine == -1)
202 return {};
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)
224 return {};
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)
234 return {};
235 if (firstHunkStartLine == lastHunkStartLine)
237 if (includeFirstHunkAtAll)
238 return fullTempPatch;
239 return {};
242 auto strLastHunkStartLine = m_lines->GetFullLineByLineNumber(lastHunkStartLine);
243 int lastHunkOldCount, lastHunkNewCount;
244 if (!CDiffLinesForStaging::GetOldAndNewLinesCountFromHunk(strLastHunkStartLine, &lastHunkOldCount, &lastHunkNewCount))
245 return {};
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)
257 return {};
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"
302 --(*newCount);
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);
311 ++(*oldCount);
313 hunkWithoutStartLine.append(strLine);
316 else
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);
334 ++(*newCount);
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"
341 --(*oldCount);
344 else
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())
361 return CString();
363 FILE* fp = nullptr;
364 _wfopen_s(&fp, tempFile, L"w+b");
365 if (!fp)
366 return CString();
368 fwrite(data.c_str(), sizeof(char), data.length(), fp);
369 fclose(fp);
371 return tempFile;