Git 客户端,采用 C# 编写。
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

618 lines
22 KiB

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using GitCommands;
using GitCommands.Settings;
namespace PatchApply
{
public class PatchManager
{
private List<Patch> _patches = new List<Patch>();
public string PatchFileName { get; set; }
public string DirToPatch { get; set; }
public List<Patch> Patches
{
get { return _patches; }
set { _patches = value; }
}
public static byte[] GetResetUnstagedLinesAsPatch(GitModule module, string text, int selectionPosition, int selectionLength, bool staged, Encoding fileContentEncoding)
{
string header;
ChunkList selectedChunks = ChunkList.GetSelectedChunks(text, selectionPosition, selectionLength, staged, out header);
if (selectedChunks == null)
return null;
string body = selectedChunks.ToResetUnstagedLinesPatch();
//git apply has problem with dealing with autocrlf
//I noticed that patch applies when '\r' chars are removed from patch if autocrlf is set to true
if (body != null && module.EffectiveConfigFile.core.autocrlf.Value == AutoCRLFType.True)
body = body.Replace("\r", "");
if (header == null || body == null)
return null;
else
return GetPatchBytes(header, body, fileContentEncoding);
}
public static byte[] GetSelectedLinesAsPatch(GitModule module, string text, int selectionPosition, int selectionLength, bool staged, Encoding fileContentEncoding, bool isNewFile)
{
string header;
ChunkList selectedChunks = ChunkList.GetSelectedChunks(text, selectionPosition, selectionLength, staged, out header);
if (selectedChunks == null)
return null;
//if file is new, --- /dev/null has to be replaced by --- a/fileName
if (isNewFile)
header = CorrectHeaderForNewFile(header);
string body = selectedChunks.ToStagePatch(staged);
if (header == null || body == null)
return null;
else
return GetPatchBytes(header, body, fileContentEncoding);
}
private static string CorrectHeaderForNewFile(string header)
{
string[] headerLines = header.Split(new string[] {"\n"}, StringSplitOptions.RemoveEmptyEntries);
string pppLine = null;
foreach (string line in headerLines)
if (line.StartsWith("+++"))
pppLine = "---" + line.Substring(3);
StringBuilder sb = new StringBuilder();
foreach (string line in headerLines)
{
if (line.StartsWith("---"))
sb.Append(pppLine + "\n");
else if (!line.StartsWith("new file mode"))
sb.Append(line + "\n");
}
return sb.ToString();
}
public static byte[] GetSelectedLinesAsNewPatch(GitModule module, string newFileName, string text, int selectionPosition, int selectionLength, Encoding fileContentEncoding, bool reset)
{
StringBuilder sb = new StringBuilder();
string fileMode = "100000";//given fake mode to satisfy patch format, git will override this
sb.Append(string.Format("diff --git a/{0} b/{0}", newFileName));
sb.Append("\n");
if (!reset)
{
sb.Append("new file mode " + fileMode);
sb.Append("\n");
}
sb.Append("index 0000000..0000000");
sb.Append("\n");
if (reset)
sb.Append("--- a/" + newFileName);
else
sb.Append("--- /dev/null");
sb.Append("\n");
sb.Append("+++ b/" + newFileName);
sb.Append("\n");
string header = sb.ToString();
ChunkList selectedChunks = ChunkList.FromNewFile(module, text, selectionPosition, selectionLength, reset);
if (selectedChunks == null)
return null;
string body = selectedChunks.ToStagePatch(false);
//git apply has problem with dealing with autocrlf
//I noticed that patch applies when '\r' chars are removed from patch if autocrlf is set to true
if (reset && body != null && module.EffectiveConfigFile.core.autocrlf.Value == AutoCRLFType.True)
body = body.Replace("\r", "");
if (header == null || body == null)
return null;
else
return GetPatchBytes(header, body, fileContentEncoding);
}
public static byte[] GetPatchBytes(string header, string body, Encoding fileContentEncoding)
{
byte[] hb = EncodingHelper.ConvertTo(GitModule.SystemEncoding, header);
byte[] bb = EncodingHelper.ConvertTo(fileContentEncoding, body);
byte[] result = new byte[hb.Length + bb.Length];
hb.CopyTo(result, 0);
bb.CopyTo(result, hb.Length);
return result;
}
public string GetMD5Hash(string input)
{
IEnumerable<byte> bs = GetUTF8EncodedBytes(input);
var s = new System.Text.StringBuilder();
foreach (byte b in bs)
{
s.Append(b.ToString("x2").ToLower());
}
return s.ToString();
}
private static IEnumerable<byte> GetUTF8EncodedBytes(string input)
{
var x = new System.Security.Cryptography.MD5CryptoServiceProvider();
byte[] bs = System.Text.Encoding.UTF8.GetBytes(input);
bs = x.ComputeHash(bs);
return bs;
}
//TODO encoding for each file in patch should be obtained separately from .gitattributes
public void LoadPatch(string text, bool applyPatch, Encoding filesContentEncoding)
{
PatchProcessor patchProcessor = new PatchProcessor(filesContentEncoding);
_patches = patchProcessor.CreatePatchesFromString(text).ToList();
if (!applyPatch)
return;
foreach (Patch patchApply in _patches)
{
if (patchApply.Apply)
patchApply.ApplyPatch(filesContentEncoding);
}
}
public void LoadPatchFile(bool applyPatch, Encoding filesContentEncoding)
{
using (var re = new StreamReader(PatchFileName, GitModule.LosslessEncoding))
{
LoadPatchStream(re, applyPatch, filesContentEncoding);
}
}
private void LoadPatchStream(TextReader reader, bool applyPatch, Encoding filesContentEncoding)
{
LoadPatch(reader.ReadToEnd(), applyPatch, filesContentEncoding);
}
}
internal class PatchLine
{
public string Text { get; set; }
public bool Selected { get; set; }
}
internal class SubChunk
{
public List<PatchLine> PreContext = new List<PatchLine>();
public List<PatchLine> RemovedLines = new List<PatchLine>();
public List<PatchLine> AddedLines = new List<PatchLine>();
public List<PatchLine> PostContext = new List<PatchLine>();
public string WasNoNewLineAtTheEnd = null;
public string IsNoNewLineAtTheEnd = null;
public string ToStagePatch(ref int addedCount, ref int removedCount, ref bool wereSelectedLines, bool staged)
{
string diff = null;
string removePart = null;
string addPart = null;
string prePart = null;
string postPart = null;
bool inPostPart = false;
bool selectedLastLine = false;
addedCount += PreContext.Count + PostContext.Count;
removedCount += PreContext.Count + PostContext.Count;
foreach (PatchLine line in PreContext)
diff = diff.Combine("\n", line.Text);
for (int i = 0; i < RemovedLines.Count; i++)
{
PatchLine removedLine = RemovedLines[i];
selectedLastLine = removedLine.Selected;
if (removedLine.Selected)
{
wereSelectedLines = true;
inPostPart = true;
removePart = removePart.Combine("\n", removedLine.Text);
removedCount++;
}
else if (!staged)
{
if (inPostPart)
removePart = removePart.Combine("\n", " " + removedLine.Text.Substring(1));
else
prePart = prePart.Combine("\n", " " + removedLine.Text.Substring(1));
addedCount++;
removedCount++;
}
}
bool selectedLastRemovedLine = selectedLastLine;
for (int i = 0; i < AddedLines.Count; i++)
{
PatchLine addedLine = AddedLines[i];
selectedLastLine = addedLine.Selected;
if (addedLine.Selected)
{
wereSelectedLines = true;
inPostPart = true;
addPart = addPart.Combine("\n", addedLine.Text);
addedCount++;
}
else if (staged)
{
if (inPostPart)
postPart = postPart.Combine("\n", " " + addedLine.Text.Substring(1));
else
prePart = prePart.Combine("\n", " " + addedLine.Text.Substring(1));
addedCount++;
removedCount++;
}
}
diff = diff.Combine("\n", prePart);
diff = diff.Combine("\n", removePart);
if (PostContext.Count == 0 && (!staged || selectedLastRemovedLine))
diff = diff.Combine("\n", WasNoNewLineAtTheEnd);
diff = diff.Combine("\n", addPart);
diff = diff.Combine("\n", postPart);
foreach (PatchLine line in PostContext)
diff = diff.Combine("\n", line.Text);
//stage no new line at the end only if last +- line is selected
if (PostContext.Count == 0 && (selectedLastLine || staged))
diff = diff.Combine("\n", IsNoNewLineAtTheEnd);
if (PostContext.Count > 0)
diff = diff.Combine("\n", WasNoNewLineAtTheEnd);
return diff;
}
//patch base is changed file
public string ToResetUnstagedLinesPatch(ref int addedCount, ref int removedCount, ref bool wereSelectedLines)
{
string diff = null;
string removePart = null;
string addPart = null;
string prePart = null;
string postPart = null;
bool inPostPart = false;
addedCount += PreContext.Count + PostContext.Count;
removedCount += PreContext.Count + PostContext.Count;
foreach (PatchLine line in PreContext)
diff = diff.Combine("\n", line.Text);
foreach (PatchLine removedLine in RemovedLines)
{
if (removedLine.Selected)
{
wereSelectedLines = true;
inPostPart = true;
addPart = addPart.Combine("\n", "+" + removedLine.Text.Substring(1));
addedCount++;
}
}
foreach (PatchLine addedLine in AddedLines)
{
if (addedLine.Selected)
{
wereSelectedLines = true;
inPostPart = true;
removePart = removePart.Combine("\n", "-" + addedLine.Text.Substring(1));
removedCount++;
}
else
{
if (inPostPart)
postPart = postPart.Combine("\n", " " + addedLine.Text.Substring(1));
else
prePart = prePart.Combine("\n", " " + addedLine.Text.Substring(1));
addedCount++;
removedCount++;
}
}
diff = diff.Combine("\n", prePart);
diff = diff.Combine("\n", removePart);
if (PostContext.Count == 0)
diff = diff.Combine("\n", WasNoNewLineAtTheEnd);
diff = diff.Combine("\n", addPart);
diff = diff.Combine("\n", postPart);
foreach (PatchLine line in PostContext)
diff = diff.Combine("\n", line.Text);
if (PostContext.Count == 0)
diff = diff.Combine("\n", IsNoNewLineAtTheEnd);
else
diff = diff.Combine("\n", WasNoNewLineAtTheEnd);
return diff;
}
}
internal delegate string SubChunkToPatchFnc(SubChunk subChunk, ref int addedCount, ref int removedCount, ref bool wereSelectedLines);
internal class Chunk
{
private int StartLine;
private List<SubChunk> SubChunks = new List<SubChunk>();
private SubChunk _CurrentSubChunk = null;
public SubChunk CurrentSubChunk
{
get
{
if (_CurrentSubChunk == null)
{
_CurrentSubChunk = new SubChunk();
SubChunks.Add(_CurrentSubChunk);
}
return _CurrentSubChunk;
}
}
public void AddContextLine(PatchLine line, bool preContext)
{
if (preContext)
CurrentSubChunk.PreContext.Add(line);
else
CurrentSubChunk.PostContext.Add(line);
}
public void AddDiffLine(PatchLine line, bool removed)
{
//if postContext is not empty @line comes from next SubChunk
if (CurrentSubChunk.PostContext.Count > 0)
_CurrentSubChunk = null;//start new SubChunk
if (removed)
CurrentSubChunk.RemovedLines.Add(line);
else
CurrentSubChunk.AddedLines.Add(line);
}
public bool ParseHeader(string header)
{
header = header.SkipStr("-");
header = header.TakeUntilStr(",");
return int.TryParse(header, out StartLine);
}
public static Chunk ParseChunk(string chunkStr, int currentPos, int selectionPosition, int selectionLength)
{
string[] lines = chunkStr.Split('\n');
if (lines.Length < 2)
return null;
bool inPatch = true;
bool inPreContext = true;
int i = 1;
Chunk result = new Chunk();
result.ParseHeader(lines[0]);
currentPos += lines[0].Length + 1;
while (i < lines.Length)
{
string line = lines[i];
if (inPatch)
{
PatchLine patchLine = new PatchLine()
{
Text = line
};
//do not refactor, there are no break points condition in VS Experss
if (currentPos <= selectionPosition + selectionLength && currentPos + line.Length >= selectionPosition)
patchLine.Selected = true;
if (line.StartsWith(" "))
result.AddContextLine(patchLine, inPreContext);
else if (line.StartsWith("-"))
{
inPreContext = false;
result.AddDiffLine(patchLine, true);
}
else if (line.StartsWith("+"))
{
inPreContext = false;
result.AddDiffLine(patchLine, false);
}
else if (line.StartsWith("\\"))
{
if (line.Contains("No newline at end of file"))
if (result.CurrentSubChunk.AddedLines.Count > 0 && result.CurrentSubChunk.PostContext.Count == 0)
result.CurrentSubChunk.IsNoNewLineAtTheEnd = line;
else
result.CurrentSubChunk.WasNoNewLineAtTheEnd = line;
}
else
inPatch = false;
}
currentPos += line.Length + 1;
i++;
}
return result;
}
public static Chunk FromNewFile(GitModule module, string fileText, int selectionPosition, int selectionLength, bool reset)
{
Chunk result = new Chunk();
result.StartLine = 0;
int currentPos = 0;
string gitEol = module.GetEffectiveSetting("core.eol");
string eol;
if ("crlf".Equals(gitEol))
eol = "\r\n";
else if ("native".Equals(gitEol))
eol = Environment.NewLine;
else
eol = "\n";
int eolLength = eol.Length;
string[] lines = fileText.Split(new string[] { eol }, StringSplitOptions.None);
int i = 0;
while (i < lines.Length)
{
string line = lines[i];
PatchLine patchLine = new PatchLine()
{
Text = (reset ? "-" : "+") + line
};
//do not refactor, there are no breakpoints condition in VS Experss
if (currentPos <= selectionPosition + selectionLength && currentPos + line.Length >= selectionPosition)
patchLine.Selected = true;
if (i == lines.Length - 1)
{
if (!line.Equals(string.Empty))
{
result.CurrentSubChunk.IsNoNewLineAtTheEnd = "\\ No newline at end of file";
result.AddDiffLine(patchLine, reset);
}
}
else
result.AddDiffLine(patchLine, reset);
currentPos += line.Length + eolLength;
i++;
}
return result;
}
public string ToPatch(SubChunkToPatchFnc subChunkToPatch)
{
bool wereSelectedLines = false;
string diff = null;
int addedCount = 0;
int removedCount = 0;
foreach (SubChunk subChunk in SubChunks)
{
string subDiff = subChunkToPatch(subChunk, ref addedCount, ref removedCount, ref wereSelectedLines);
diff = diff.Combine("\n", subDiff);
}
if (!wereSelectedLines)
return null;
diff = "@@ -" + StartLine + "," + removedCount + " +" + StartLine + "," + addedCount + " @@".Combine("\n", diff);
return diff;
}
}
internal class ChunkList : List<Chunk>
{
public static ChunkList GetSelectedChunks(string text, int selectionPosition, int selectionLength, bool staged, out string header)
{
header = null;
//When there is no patch, return nothing
if (string.IsNullOrEmpty(text))
return null;
// Divide diff into header and patch
int patchPos = text.IndexOf("@@");
if (patchPos < 1)
return null;
header = text.Substring(0, patchPos);
string diff = text.Substring(patchPos - 1);
string[] chunks = diff.Split(new string[] { "\n@@" }, StringSplitOptions.RemoveEmptyEntries);
ChunkList selectedChunks = new ChunkList();
int i = 0;
int currentPos = patchPos - 1;
while (i < chunks.Length && currentPos <= selectionPosition + selectionLength)
{
string chunkStr = chunks[i];
currentPos += 3;
//if selection intersects with chunsk
if (currentPos + chunkStr.Length >= selectionPosition)
{
Chunk chunk = Chunk.ParseChunk(chunkStr, currentPos, selectionPosition, selectionLength);
if (chunk != null)
selectedChunks.Add(chunk);
}
currentPos += chunkStr.Length;
i++;
}
return selectedChunks;
}
public static ChunkList FromNewFile(GitModule module, string text, int selectionPosition, int selectionLength, bool reset)
{
Chunk chunk = Chunk.FromNewFile(module, text, selectionPosition, selectionLength, reset);
ChunkList result = new ChunkList();
result.Add(chunk);
return result;
}
public string ToResetUnstagedLinesPatch()
{
SubChunkToPatchFnc subChunkToPatch = (SubChunk subChunk, ref int addedCount, ref int removedCount, ref bool wereSelectedLines) =>
{
return subChunk.ToResetUnstagedLinesPatch(ref addedCount, ref removedCount, ref wereSelectedLines);
};
return ToPatch(subChunkToPatch);
}
public string ToStagePatch(bool staged)
{
SubChunkToPatchFnc subChunkToPatch = (SubChunk subChunk, ref int addedCount, ref int removedCount, ref bool wereSelectedLines) =>
{
return subChunk.ToStagePatch(ref addedCount, ref removedCount, ref wereSelectedLines, staged);
};
return ToPatch(subChunkToPatch);
}
protected string ToPatch(SubChunkToPatchFnc subChunkToPatch)
{
string result = null;
foreach (Chunk chunk in this)
result = result.Combine("\n", chunk.ToPatch(subChunkToPatch));
if (result != null)
{
result = result.Combine("\n", "--");
result = result.Combine("\n", Application.ProductName + " " + AppSettings.ProductVersion);
}
return result;
}
}
}