Created
December 29, 2025 16:28
-
-
Save DartPower/e0cc42379933b3410057aca11469f153 to your computer and use it in GitHub Desktop.
Git Commit History Exporter in C# (Экспорт истории Git)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Diagnostics; | |
| using System.IO; | |
| using System.Reflection; | |
| using System.Text; | |
| using System.Xml; | |
| namespace GitCommitExporter | |
| { | |
| public class CommitInfo | |
| { | |
| public string Hash; | |
| public DateTime Date; | |
| public string Tag; | |
| public string BranchName; | |
| public string GitRef; | |
| public string Subject; | |
| } | |
| public enum ExportFormat | |
| { | |
| Subfolders, | |
| Zips | |
| } | |
| public enum SortOrder | |
| { | |
| Asc, | |
| Desc | |
| } | |
| [Flags] | |
| public enum MetadataType | |
| { | |
| None = 0, | |
| Json = 1, | |
| Xml = 2, | |
| Log = 4 | |
| } | |
| class Program | |
| { | |
| private static string _logFilePath = null; | |
| private static string _tempGitConfigPath = null; | |
| static void Main(string[] args) | |
| { | |
| MetadataWriter metadataWriter = null; | |
| try | |
| { | |
| SetupGitEnvironment(); | |
| SetupLogging(); | |
| if (args.Length < 1) | |
| { | |
| LogUsage(); | |
| return; | |
| } | |
| // 1. Парсинг путей | |
| bool isUrl = IsUrl(args[0]); | |
| string repoPath = ""; | |
| string outputPath = ""; | |
| int optionsStartIndex = 0; | |
| if (isUrl) | |
| { | |
| if (args.Length < 3) { LogError("Для URL нужны 2 пути: клонирование и экспорт."); LogUsage(); return; } | |
| string url = args[0]; | |
| repoPath = Path.GetFullPath(args[1]); | |
| outputPath = Path.GetFullPath(args[2]); | |
| optionsStartIndex = 3; | |
| LogMessage("URL: " + url); | |
| CloneRepository(url, repoPath); | |
| } | |
| else | |
| { | |
| if (args.Length < 2) { LogUsage(); return; } | |
| repoPath = Path.GetFullPath(args[0]); | |
| outputPath = Path.GetFullPath(args[1]); | |
| optionsStartIndex = 2; | |
| if (!Directory.Exists(repoPath)) { LogError("Репозиторий не найден: " + repoPath); return; } | |
| } | |
| // 2. Парсинг опций | |
| var parsedArgs = ParseArguments(args, optionsStartIndex); | |
| // Basic | |
| string formatStr = parsedArgs.ContainsKey("format") ? parsedArgs["format"] : "subfolders"; | |
| var format = (ExportFormat)Enum.Parse(typeof(ExportFormat), formatStr, true); | |
| string orderStr = parsedArgs.ContainsKey("order") ? parsedArgs["order"] : "asc"; | |
| var order = (SortOrder)Enum.Parse(typeof(SortOrder), orderStr, true); | |
| string startFromHash = parsedArgs.ContainsKey("continue") ? parsedArgs["continue"] : null; | |
| string toHash = parsedArgs.ContainsKey("to_hash") ? parsedArgs["to_hash"] : null; | |
| // Filters | |
| DateTime? fromDate = null; | |
| if (parsedArgs.ContainsKey("from_date")) try { fromDate = DateTime.Parse(parsedArgs["from_date"]); } catch { } | |
| DateTime? toDate = null; | |
| if (parsedArgs.ContainsKey("to_date")) try { toDate = DateTime.Parse(parsedArgs["to_date"]); } catch { } | |
| int limitCount = parsedArgs.ContainsKey("limit_count") ? int.Parse(parsedArgs["limit_count"]) : int.MaxValue; | |
| int limitSizeMB = parsedArgs.ContainsKey("limit_size_mb") ? int.Parse(parsedArgs["limit_size_mb"]) : int.MaxValue; | |
| // Metadata Settings (Log enabled by default) | |
| MetadataType exportMeta = MetadataType.Log; | |
| if (parsedArgs.ContainsKey("export")) | |
| { | |
| foreach (string p in parsedArgs["export"].Split(new char[] { ',' })) | |
| { | |
| string val = p.Trim().ToLower(); | |
| if (val == "xml") exportMeta |= MetadataType.Xml; | |
| if (val == "json") exportMeta |= MetadataType.Json; | |
| if (val == "log") exportMeta |= MetadataType.Log; | |
| } | |
| } | |
| LogMessage("Выход: " + outputPath); | |
| LogMessage("Формат: " + format + ", Порядок: " + order); | |
| LogMessage("Лимиты: Кол-во=" + (limitCount == int.MaxValue ? "∞" : limitCount.ToString()) + | |
| ", Размер=" + (limitSizeMB == int.MaxValue ? "∞" : limitSizeMB + "MB")); | |
| Directory.CreateDirectory(outputPath); | |
| // 3. Инициализация метаданных и Tee-лога | |
| string repoName = Path.GetFileName(repoPath.TrimEnd('\\', '/')); | |
| metadataWriter = new MetadataWriter(outputPath, repoName, exportMeta); | |
| metadataWriter.Open(); | |
| bool useRemoteBranches = isUrl; | |
| ExportAllCommits(repoPath, outputPath, format, order, startFromHash, toHash, fromDate, toDate, limitCount, limitSizeMB, useRemoteBranches, metadataWriter); | |
| LogMessage("Экспорт завершен успешно."); | |
| } | |
| catch (Exception ex) | |
| { | |
| LogError("Критическая ошибка: " + ex.Message); | |
| } | |
| finally | |
| { | |
| if (metadataWriter != null) metadataWriter.Close(); | |
| if (Trace.Listeners.Count > 1) ((TextWriterTraceListener)Trace.Listeners[1]).Flush(); | |
| CleanupGitEnvironment(); | |
| } | |
| } | |
| private static bool IsUrl(string input) | |
| { | |
| if (IsNullOrWhiteSpace(input)) return false; | |
| string lower = input.ToLower(); | |
| return lower.StartsWith("http://") || lower.StartsWith("https://") || lower.StartsWith("git@"); | |
| } | |
| private static void CloneRepository(string url, string targetPath) | |
| { | |
| LogMessage("Клонирование..."); | |
| if (Directory.Exists(targetPath)) | |
| { | |
| if (Directory.Exists(Path.Combine(targetPath, ".git"))) | |
| { | |
| RunGitCommand(targetPath, "fetch --all"); | |
| RunGitCommand(targetPath, "fetch --tags"); | |
| return; | |
| } | |
| } | |
| RunGitCommand(Path.GetDirectoryName(targetPath), string.Format("clone --recursive \"{0}\" \"{1}\"", url, targetPath)); | |
| RunGitCommand(targetPath, "fetch --all"); | |
| RunGitCommand(targetPath, "fetch --tags"); | |
| } | |
| private static int RunGitCommand(string workDir, string arguments) | |
| { | |
| ProcessStartInfo psi = new ProcessStartInfo("git", arguments); | |
| psi.WorkingDirectory = workDir; | |
| psi.UseShellExecute = false; | |
| psi.RedirectStandardOutput = true; | |
| psi.RedirectStandardError = true; | |
| psi.CreateNoWindow = true; | |
| ApplyGitEnvironment(psi); | |
| using (Process p = Process.Start(psi)) | |
| { | |
| p.WaitForExit(); | |
| if (p.ExitCode != 0) LogError("Git Err: " + p.StandardError.ReadToEnd()); | |
| return p.ExitCode; | |
| } | |
| } | |
| // --- TeeTraceListener для export=log --- | |
| private class TeeTraceListener : TraceListener | |
| { | |
| private string _logPath; | |
| private object _lock = new object(); | |
| public TeeTraceListener(string logPath) | |
| { | |
| _logPath = logPath; | |
| Directory.CreateDirectory(Path.GetDirectoryName(_logPath)); | |
| try { File.WriteAllText(_logPath, "EXPORT LOG STARTED: " + DateTime.Now + "\r\n"); } | |
| catch { } | |
| } | |
| public override void Write(string message) | |
| { | |
| try { lock (_lock) { File.AppendAllText(_logPath, message); } } catch { } | |
| } | |
| public override void WriteLine(string message) | |
| { | |
| try { lock (_lock) { File.AppendAllText(_logPath, message + "\r\n"); } } catch { } | |
| } | |
| } | |
| // --- MetadataWriter --- | |
| public class MetadataWriter : IDisposable | |
| { | |
| private string _outputPath; | |
| private string _repoName; | |
| private MetadataType _type; | |
| private StreamWriter _jsonWriter = null; | |
| private XmlTextWriter _xmlWriter = null; | |
| public MetadataWriter(string outputPath, string repoName, MetadataType type) | |
| { | |
| _outputPath = outputPath; | |
| _repoName = repoName; | |
| _type = type; | |
| } | |
| public void Open() | |
| { | |
| if ((_type & MetadataType.Json) != 0) | |
| { | |
| string path = Path.Combine(_outputPath, _repoName + "_metadata.json"); | |
| _jsonWriter = new StreamWriter(path, true, Encoding.UTF8); | |
| HealBrokenFile(_jsonWriter.BaseStream, "}\n"); | |
| } | |
| if ((_type & MetadataType.Xml) != 0) | |
| { | |
| string path = Path.Combine(_outputPath, _repoName + "_metadata.xml"); | |
| _xmlWriter = new XmlTextWriter(path, Encoding.UTF8); | |
| _xmlWriter.Formatting = Formatting.Indented; | |
| _xmlWriter.Indentation = 2; | |
| HealBrokenFile(_xmlWriter.BaseStream, ">\r\n"); | |
| } | |
| if ((_type & MetadataType.Log) != 0) | |
| { | |
| string logPath = Path.Combine(_outputPath, _repoName + "_run.log"); | |
| Trace.Listeners.Add(new TeeTraceListener(logPath)); | |
| } | |
| } | |
| private void HealBrokenFile(Stream stream, string expectedEndPattern) | |
| { | |
| try | |
| { | |
| long len = stream.Length; | |
| if (len == 0) return; | |
| int readSize = 1024; | |
| if (len < readSize) readSize = (int)len; | |
| stream.Seek(-readSize, SeekOrigin.End); | |
| byte[] buffer = new byte[readSize]; | |
| int bytesRead = stream.Read(buffer, 0, readSize); | |
| string tail = Encoding.UTF8.GetString(buffer, 0, bytesRead); | |
| int lastLineIdx = tail.LastIndexOf('\n'); | |
| if (lastLineIdx < 0 || lastLineIdx < tail.Length - 1) | |
| { | |
| long newLen = stream.Position - (tail.Length - (lastLineIdx < 0 ? 0 : lastLineIdx + 1)); | |
| stream.SetLength(newLen); | |
| } | |
| } | |
| catch { } | |
| } | |
| public void WriteEntry(CommitInfo commit, string filePath, long size) | |
| { | |
| try | |
| { | |
| if (_jsonWriter != null) | |
| { | |
| _jsonWriter.WriteLine("{{\"Repo\":\"{0}\",\"Branch\":\"{1}\",\"Hash\":\"{2}\",\"Date\":\"{3}\",\"Subject\":\"{4}\",\"Tag\":\"{5}\",\"Path\":\"{6}\",\"Size\":{7}}}", | |
| EscapeJson(_repoName), EscapeJson(commit.BranchName), commit.Hash, commit.Date.ToString("o"), EscapeJson(commit.Subject), | |
| EscapeJson(commit.Tag ?? ""), EscapeJson(filePath), size); | |
| _jsonWriter.Flush(); | |
| } | |
| if (_xmlWriter != null) | |
| { | |
| _xmlWriter.WriteStartElement("Item"); | |
| _xmlWriter.WriteAttributeString("Repo", _repoName); | |
| _xmlWriter.WriteAttributeString("Branch", commit.BranchName); | |
| _xmlWriter.WriteAttributeString("Hash", commit.Hash); | |
| _xmlWriter.WriteAttributeString("Date", commit.Date.ToString("o")); | |
| _xmlWriter.WriteAttributeString("Subject", commit.Subject); | |
| _xmlWriter.WriteAttributeString("Tag", commit.Tag ?? ""); | |
| _xmlWriter.WriteAttributeString("Path", filePath); | |
| _xmlWriter.WriteAttributeString("Size", size.ToString()); | |
| _xmlWriter.WriteEndElement(); | |
| _xmlWriter.WriteWhitespace("\r\n"); | |
| _xmlWriter.Flush(); | |
| } | |
| } | |
| catch { } | |
| } | |
| private string EscapeJson(string s) | |
| { | |
| if (s == null) return ""; | |
| return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r"); | |
| } | |
| public void Close() | |
| { | |
| try { if (_jsonWriter != null) { _jsonWriter.Flush(); _jsonWriter.Close(); } } catch { } | |
| try { if (_xmlWriter != null) { _xmlWriter.Flush(); _xmlWriter.Close(); } } catch { } | |
| } | |
| public void Dispose() { Close(); } | |
| } | |
| // --- Проверка целостности (упрощенная и надежная) --- | |
| private static bool VerifyExportIntegrity(string path, CommitInfo commit, ExportFormat format) | |
| { | |
| if ((format == ExportFormat.Zips && !File.Exists(path)) || | |
| (format == ExportFormat.Subfolders && !Directory.Exists(path))) | |
| { | |
| return false; | |
| } | |
| // Проверка размера | |
| try | |
| { | |
| long size = 0; | |
| if (format == ExportFormat.Zips) size = new FileInfo(path).Length; | |
| else | |
| { | |
| // Для папок считаем размер, если папка пустая - что-то не так | |
| // Но для скорости можно просто проверить наличие папки | |
| DirectoryInfo di = new DirectoryInfo(path); | |
| if (!di.Exists) return false; | |
| } | |
| if (format == ExportFormat.Zips && size == 0) return false; | |
| } | |
| catch { return false; } | |
| // Проверка даты | |
| try | |
| { | |
| DateTime fileDate; | |
| if (format == ExportFormat.Zips) fileDate = File.GetLastWriteTime(path); | |
| else fileDate = Directory.GetLastWriteTime(path); | |
| // Допуск разницы в 1 секунду | |
| if (Math.Abs((fileDate - commit.Date).TotalSeconds) > 2) | |
| { | |
| // Если дата не совпадает, считаем, что экспорт не был завершен (свет вырубился во время записи) | |
| return false; | |
| } | |
| } | |
| catch { return false; } | |
| // УБРАЛИ ПРОВЕРКУ ЧЕРЕЗ SHELL.APPLICATION. | |
| // Она вызывала ложные срабатывания на валидных файлах. | |
| // Для .NET 2.0 без библиотек лучше полагаться на размер и дату. | |
| // Если git archive завершился успешно, и дата проставилась - файл ок. | |
| return true; | |
| } | |
| // --- Вспомогательные методы --- | |
| private static bool IsNullOrWhiteSpace(string value) | |
| { | |
| if (value == null) return true; | |
| for (int i = 0; i < value.Length; i++) if (!char.IsWhiteSpace(value[i])) return false; | |
| return true; | |
| } | |
| private static long GetDirectorySize(string path) | |
| { | |
| try | |
| { | |
| DirectoryInfo di = new DirectoryInfo(path); | |
| long size = 0; | |
| FileInfo[] files = di.GetFiles("*", SearchOption.AllDirectories); | |
| foreach (FileInfo fi in files) size += fi.Length; | |
| return size; | |
| } | |
| catch { return 0; } | |
| } | |
| private static string SanitizeFileName(string name) | |
| { | |
| if (IsNullOrWhiteSpace(name)) return "unknown"; | |
| char[] invalidChars = Path.GetInvalidFileNameChars(); | |
| string sanitizedName = string.Join("_", name.Split(invalidChars)); | |
| sanitizedName = sanitizedName.Trim().TrimEnd('.'); | |
| return string.IsNullOrEmpty(sanitizedName) ? "unknown" : sanitizedName; | |
| } | |
| private static void SetupGitEnvironment() | |
| { | |
| try { _tempGitConfigPath = Path.Combine(Path.GetTempPath(), "git_cfg_" + Guid.NewGuid() + ".tmp"); File.WriteAllText(_tempGitConfigPath, "[safe]\n\tdirectory = *"); } | |
| catch { } | |
| } | |
| private static void CleanupGitEnvironment() { try { if (_tempGitConfigPath != null && File.Exists(_tempGitConfigPath)) File.Delete(_tempGitConfigPath); } catch { } } | |
| private static void ApplyGitEnvironment(ProcessStartInfo psi) { if (_tempGitConfigPath != null) { psi.EnvironmentVariables["GIT_CONFIG_GLOBAL"] = _tempGitConfigPath; psi.EnvironmentVariables["GIT_CONFIG_NOSYSTEM"] = "1"; } } | |
| private static List<string> GetBranches(string repoPath, bool useRemotes) | |
| { | |
| var list = new List<string>(); | |
| ProcessStartInfo psi = new ProcessStartInfo("git", useRemotes ? "branch -r" : "branch --format=\"%(refname:short)\""); | |
| psi.WorkingDirectory = repoPath; | |
| psi.UseShellExecute = false; psi.RedirectStandardOutput = true; psi.CreateNoWindow = true; | |
| ApplyGitEnvironment(psi); | |
| using (Process p = Process.Start(psi)) | |
| { | |
| string outp = p.StandardOutput.ReadToEnd(); p.WaitForExit(); | |
| foreach (string l in outp.Split(new char[] { '\n' })) if (!IsNullOrWhiteSpace(l) && !l.Contains("->")) list.Add(l.Trim()); | |
| } | |
| return list; | |
| } | |
| private static Dictionary<string, string> ParseArguments(string[] args, int start) | |
| { | |
| var d = new Dictionary<string, string>(); | |
| for (int i = start; i < args.Length; i++) | |
| { | |
| if (args[i].IndexOf('=') >= 0) | |
| { | |
| string[] p = args[i].Split(new char[] { '=' }, 2); | |
| d[p[0]] = p[1]; | |
| } | |
| else | |
| { | |
| string v = args[i].ToLower(); | |
| if (v == "zips" || v == "subfolders") d["format"] = v; | |
| else if (v == "asc" || v == "desc") d["order"] = v; | |
| else d[args[i]] = "true"; | |
| } | |
| } | |
| return d; | |
| } | |
| private static void SetupLogging() | |
| { | |
| try | |
| { | |
| Directory.CreateDirectory("Logs"); | |
| _logFilePath = "Logs\\Git_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".log"; | |
| Trace.Listeners.Add(new ConsoleTraceListener()); | |
| Trace.Listeners.Add(new TextWriterTraceListener(_logFilePath)); | |
| Trace.AutoFlush = true; | |
| } | |
| catch { } | |
| } | |
| private static void LogMessage(string m) { Trace.WriteLine("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + m); } | |
| private static void LogError(string m) { Trace.WriteLine("[" + DateTime.Now.ToString("HH:mm:ss") + "] ERR: " + m); } | |
| private static void LogUsage() | |
| { | |
| string u = "Использование: GitCommitExporter.exe <input> <output> [options]\n" + | |
| " input: URL или локальный путь\n" + | |
| " output: Путь для экспорта\n" + | |
| "Опции:\n" + | |
| " format: subfolders | zips\n" + | |
| " order: asc | desc\n" + | |
| " continue: hash (начать с)\n" + | |
| " to_hash: hash (закончить на)\n" + | |
| " from_date/to_date: YYYY-MM-DD\n" + | |
| " limit_count: N\n" + | |
| " limit_size_mb: N\n" + | |
| " export: xml,json,log (по умолчанию log)\n"; | |
| Console.WriteLine(u); | |
| } | |
| private static void ExportAllCommits(string repoPath, string outputPath, ExportFormat format, SortOrder order, | |
| string startHash, string toHash, DateTime? fromDate, DateTime? toDate, int limitCount, int limitSizeMB, | |
| bool useRemotes, MetadataWriter metaWriter) | |
| { | |
| var branches = GetBranches(repoPath, useRemotes); | |
| int globalProcessed = 0; | |
| long globalSizeBytes = 0; | |
| bool stopRequested = false; | |
| foreach (var branchRef in branches) | |
| { | |
| if (stopRequested) break; | |
| string branchName = branchRef.Contains("/") ? branchRef.Substring(branchRef.LastIndexOf('/') + 1) : branchRef; | |
| LogMessage("\n--- Ветка: " + branchName + " ---"); | |
| var allCommits = GetGitCommits(repoPath, branchRef, order); | |
| List<CommitInfo> toProcess = new List<CommitInfo>(); | |
| // Применение фильтров | |
| bool startFound = string.IsNullOrEmpty(startHash); | |
| foreach (var c in allCommits) | |
| { | |
| if (!startFound) | |
| { | |
| if (c.Hash.StartsWith(startHash, StringComparison.OrdinalIgnoreCase)) startFound = true; | |
| else continue; | |
| } | |
| if (!string.IsNullOrEmpty(toHash) && c.Hash.StartsWith(toHash, StringComparison.OrdinalIgnoreCase)) | |
| { | |
| toProcess.Add(c); | |
| break; | |
| } | |
| if (!string.IsNullOrEmpty(toHash)) continue; | |
| if (fromDate.HasValue && c.Date < fromDate.Value) continue; | |
| if (toDate.HasValue && c.Date > toDate.Value) continue; | |
| toProcess.Add(c); | |
| } | |
| int skipped = 0; | |
| // ИСПРАВЛЕНИЕ СЧЕТЧИКА: показываем прогресс внутри ветки | |
| for (int i = 0; i < toProcess.Count; i++) | |
| { | |
| var c = toProcess[i]; | |
| if (globalProcessed >= limitCount) { LogMessage("Достигнут лимит count."); stopRequested = true; break; } | |
| if (format == ExportFormat.Zips && limitSizeMB != int.MaxValue) | |
| { | |
| double sizeMB = globalSizeBytes / (1024.0 * 1024.0); | |
| if (sizeMB >= limitSizeMB) { LogMessage("Достигнут лимит size."); stopRequested = true; break; } | |
| } | |
| string safeBranch = SanitizeFileName(branchName); | |
| string safeTag = string.IsNullOrEmpty(c.Tag) ? "" : "_" + SanitizeFileName(c.Tag); | |
| string baseName = Path.GetFileName(outputPath.TrimEnd('\\', '/')) + "_" + safeBranch + "_tree_" + c.Hash + "_" + c.Date.ToString("yyyyMMdd_HHmmss") + safeTag; | |
| string targetPath = Path.Combine(outputPath, baseName + (format == ExportFormat.Zips ? ".zip" : "")); | |
| bool isValid = false; | |
| if ((format == ExportFormat.Zips && File.Exists(targetPath)) || | |
| (format == ExportFormat.Subfolders && Directory.Exists(targetPath))) | |
| { | |
| if (VerifyExportIntegrity(targetPath, c, format)) | |
| { | |
| isValid = true; | |
| skipped++; | |
| } | |
| else | |
| { | |
| try | |
| { | |
| if (format == ExportFormat.Zips) | |
| { | |
| string badPath = targetPath + "_probably_corrupted"; | |
| if (File.Exists(badPath)) File.Delete(badPath); | |
| File.Move(targetPath, badPath); | |
| } | |
| else | |
| { | |
| Directory.Move(targetPath, targetPath + "_probably_corrupted"); | |
| } | |
| } | |
| catch { } | |
| } | |
| } | |
| if (!isValid) | |
| { | |
| // ИСПРАВЛЕНИЕ ЛОГИРОВАНИЯ: Используем Trace.Write, чтобы попадало в логи | |
| // Формат: [Текущий/Всего] Хэш - Тема | |
| string progressInfo = string.Format("[{0}/{1}] {2} - {3}", i + 1, toProcess.Count, c.Hash.Substring(0, 7), c.Subject); | |
| if (format == ExportFormat.Zips) | |
| { | |
| Trace.Write(progressInfo + " ... "); | |
| } | |
| else | |
| { | |
| LogMessage(progressInfo); | |
| } | |
| try | |
| { | |
| if (format == ExportFormat.Subfolders) ExportToFolder(repoPath, c.Hash, targetPath); | |
| else | |
| { | |
| ExportToZip(repoPath, c.Hash, targetPath, c.Date); | |
| Trace.WriteLine("ОК"); // Trace.WriteLine, чтобы попало в логи | |
| } | |
| long size = 0; | |
| try | |
| { | |
| if (format == ExportFormat.Zips) size = new FileInfo(targetPath).Length; | |
| else size = GetDirectorySize(targetPath); | |
| globalSizeBytes += size; | |
| globalProcessed++; | |
| if (metaWriter != null) metaWriter.WriteEntry(c, targetPath, size); | |
| } | |
| catch (Exception metaEx) { LogError("Ошибка записи метаданных: " + metaEx.Message); } | |
| } | |
| catch (Exception ex) | |
| { | |
| if (format == ExportFormat.Zips) Trace.WriteLine("FAIL"); | |
| LogError("Ошибка экспорта " + c.Hash + ": " + ex.Message); | |
| } | |
| } | |
| } | |
| LogMessage("Ветка завершена. Пропущено: " + skipped); | |
| } | |
| } | |
| private static List<CommitInfo> GetGitCommits(string repoPath, string branchRef, SortOrder order) | |
| { | |
| var list = new List<CommitInfo>(); | |
| ProcessStartInfo psi = new ProcessStartInfo("git", "log " + (order == SortOrder.Asc ? "--reverse " : "") + branchRef + " --decorate=short --pretty=format:\"%H|%ci|%D|%s\""); | |
| psi.WorkingDirectory = repoPath; psi.UseShellExecute = false; psi.RedirectStandardOutput = true; psi.CreateNoWindow = true; | |
| ApplyGitEnvironment(psi); | |
| using (Process p = Process.Start(psi)) | |
| { | |
| string outp = p.StandardOutput.ReadToEnd(); p.WaitForExit(); | |
| foreach (string line in outp.Split(new char[] { '\n' })) | |
| { | |
| if (!IsNullOrWhiteSpace(line)) | |
| { | |
| string[] parts = line.Split(new char[] { '|' }); | |
| if (parts.Length >= 2) | |
| { | |
| var c = new CommitInfo(); | |
| c.Hash = parts[0]; | |
| try { c.Date = DateTime.Parse(parts[1]); } catch { c.Date = DateTime.MinValue; } | |
| c.Subject = parts.Length >= 4 ? parts[3] : ""; | |
| c.BranchName = branchRef.Contains("/") ? branchRef.Substring(branchRef.LastIndexOf('/') + 1) : branchRef; | |
| c.GitRef = branchRef; | |
| if (parts.Length >= 3 && !IsNullOrWhiteSpace(parts[2])) | |
| { | |
| foreach (string r in parts[2].Split(',')) if (r.Trim().StartsWith("tag: ")) { c.Tag = r.Trim().Substring(5); break; } | |
| } | |
| list.Add(c); | |
| } | |
| } | |
| } | |
| } | |
| return list; | |
| } | |
| private static void ExportToFolder(string repoPath, string hash, string outPath) | |
| { | |
| string tmp = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".zip"); | |
| try { CreateArchive(repoPath, hash, tmp); Unzip(tmp, outPath); } | |
| finally { if (File.Exists(tmp)) File.Delete(tmp); } | |
| } | |
| private static void ExportToZip(string repoPath, string hash, string outPath, DateTime date) | |
| { | |
| CreateArchive(repoPath, hash, outPath); | |
| try { File.SetCreationTime(outPath, date); File.SetLastWriteTime(outPath, date); } catch { } | |
| } | |
| private static void CreateArchive(string repoPath, string hash, string path) | |
| { | |
| ProcessStartInfo psi = new ProcessStartInfo("git", "archive --format=zip --output=\"" + path + "\" " + hash); | |
| psi.WorkingDirectory = repoPath; psi.UseShellExecute = false; psi.RedirectStandardError = true; psi.CreateNoWindow = true; | |
| ApplyGitEnvironment(psi); | |
| using (Process p = Process.Start(psi)) | |
| { | |
| p.WaitForExit(); | |
| if (p.ExitCode != 0) throw new Exception("Git archive fail: " + p.StandardError.ReadToEnd()); | |
| } | |
| } | |
| private static void Unzip(string zipPath, string targetPath) | |
| { | |
| if (!Directory.Exists(targetPath)) Directory.CreateDirectory(targetPath); | |
| Type t = Type.GetTypeFromProgID("Shell.Application"); | |
| object s = Activator.CreateInstance(t); | |
| object src = t.InvokeMember("NameSpace", BindingFlags.InvokeMethod, null, s, new object[] { zipPath }); | |
| object dst = t.InvokeMember("NameSpace", BindingFlags.InvokeMethod, null, s, new object[] { targetPath }); | |
| object items = src.GetType().InvokeMember("Items", BindingFlags.GetProperty, null, src, null); | |
| int cnt = (int)items.GetType().InvokeMember("Count", BindingFlags.GetProperty, null, items, null); | |
| dst.GetType().InvokeMember("CopyHere", BindingFlags.InvokeMethod, null, dst, new object[] { items, 4 | 16 | 1024 }); | |
| int wait = 0; | |
| while (wait < 120) | |
| { | |
| System.Threading.Thread.Sleep(500); | |
| object dItems = dst.GetType().InvokeMember("Items", BindingFlags.GetProperty, null, dst, null); | |
| int dCnt = (int)dItems.GetType().InvokeMember("Count", BindingFlags.GetProperty, null, dItems, null); | |
| if (dCnt >= cnt) { System.Threading.Thread.Sleep(500); break; } | |
| wait++; | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment