Позвольте мне рассказать вам историю. Жили-были два разработчика: Сэм и Боб. Они вместе работали над проектом, в котором была база данных. Когда разработчик хотел внести в неё изменения, он обязан был создать файл stepNNN.sql
, где NNN — некоторое число. Чтобы избежать конфликтов этих чисел между различными разработчиками, они использовали простой Web-сервис. Каждый разработчик прежде чем начать писать SQL-файл должен был зайти на этот сервис и зарезервировать за собой новое число для step-файла.
В этот раз Сэму и Бобу обоим нужно было внести изменения в базу данных. Сэм послушно отправился на сервис и зарезервировал за собой число 333. А Боб забыл сделать это. Он просто использовал 333 для своего step-файла. Так случилось, что в этот раз Боб первым залил свои изменения в систему контроля версий. Когда Сэм был готов залиться, он обнаружил, что файл step333.sql
уже существует. Он связался с Бобом, объяснил ему, что номер 333 был зарезервирован за ним и попросил исправить конфликт. Но Боб ответил:
— Чувак, мой код уже в 'master'е, куча разработчиков уже используют его. К тому же он уже выкачен на production. Так что просто исправь там у себя всё, что нужно.
Надеюсь, вы заметили, что произошло. Наказанным оказался человек, который следовал всем правилам. Сэму пришлось менять свои файлы, править свою локальную базу данных и т.д. Лично я ненавижу такие ситуации. Давайте посмотрим, как мы можем избежать её.
Основная идея
Как нам избежать подобных вещей? Что, если бы Боб не смог залить свой код, если он не зарезервировал за собой соответствующее число на Web-сервисе?
И мы на самом деле можем добиться этого. Мы можем использовать Git hook'и чтобы выполнять пользовательский код перед каждым commit'ом. Этот код будет проверять все заливаемые изменения. Если они содержат новый step-файл, код будет связываться с Web-сервисом и проверять, зарезервирован ли номер step-файла за текущим разработчиком. И если номер не зарезервирован, код будет запрещать заливку.
Такова основная идея. Давайте же перейдём к деталям.
Git hook'и на C#
Git не ограничивает вас в том, на каких языках вы должны писать hook'и. Как C#-разработчик, я бы предпочёл использовать хорошо знакомый мне C# для этих целей. Могу ли я это сделать?
Да, могу. Основная идея взята мною из этой статьи, написанной Max Hamulyák. В ней требуется, чтобы мы использовали глобальный инструмент dotnet-script. Этот инструмент требует наличия .NET Core 2.1 + SDK на машине разработчика. Я полагаю, что это разумное требование для тех, кто занимается .NET разработкой. Установка dotnet-script
очень проста:
> dotnet tool install -g dotnet-script
Теперь мы можем писать Git hook'и на C#. Для этого нужно перейти в папку .git\hooks
вашего проекта и создать файл pre-commit
(безо всякого расширения):
#!/usr/bin/env dotnet-script
Console.WriteLine("Git hook");
С этого момента всякий раз, когда вы делаете git commit
, вы будете видеть текст Git hook
в вашей консоли.
Несколько обработчиков на один hook
Что ж, начало положено. Теперь мы можем писать что угодно в файле pre-commit
. Но мне не очень нравится эта идея.
Во-первых, работать со скрипт-файлом не очень удобно. Я бы предпочёл использовать мой любимый IDE со всеми его возможностями. И я предпочёл бы иметь возможность разбить сложный код на несколько файлов.
Но есть и ещё кое-что, что мне не нравится. Представьте себе следующую ситуацию. Вы создали pre-commit
с какими-нибудь проверками. Но позже вам потребовалось добавить ещё проверок. Вам придётся открывать файл, решать, куда вставить ваш код, как он будет взаимодействовать со старым кодом и т.п. Лично я предпочитаю писать новый код, а не копаться в старом.
Давайте разберёмся с этими проблемами по одной.
Вызов внешнего кода
Вот как мы поступим. Давайте создадим отдельную папку (например, gitHookAssemblies
). В эту папку я положу .NET Core сборку (например, GitHooks
). Мой скрипт в файле pre-commit
будет просто вызывать некоторый метод из этой сборки.
public class RunHooks
{
public static void RunPreCommitHook()
{
Console.WriteLine("Git hook from assembly");
}
}
Я могу создавать эту сборку в моём любимом IDE и использовать любые инструменты.
Теперь в файле pre-commit
я могу написать:
#!/usr/bin/env dotnet-script
#r "../../gitHookAssemblies/GitHooks.dll"
GitHooks.RunHooks.RunPreCommitHook();
Здорово, не правда ли! Теперь я могу делать изменения только в моей сборке GitHooks
. Код файла pre-commit
уже никогда не будет меняться. Когда мне потребуется добавить какую-нибудь проверку, я изменю код метода RunPreCommitHook
, пересоберу сборку и положу её в папку gitHookAssemblies
. И всё!
Ну, не совсем.
Борьба с кэшем
Давайте попробуем последовать нашему процессу. Поменяем сообщение в Console.WriteLine
на что-нибудь другое, пересоберём сборку и положим результат в папку gitHookAssemblies
. После этого вызовем git commit
снова. Что же мы увидим? Старое сообщение. Наши изменения не подцепились. Почему?
Пусть, для определённости, ваш проект находится в папке c:\project
. Это означает, что скрипты Git hook'ов расположены в папке c:\project\.git\hooks
. Теперь, если вы используете Windows 10, перейдите в папку c:\Users\<UserName>\AppData\Local\Temp\scripts\c\project\.git\hooks\
. Здесь <UserName>
— имя вашего текущего пользователя. Что же мы увидим здесь? Когда мы запускаем скрипт pre-commit
, в этой папке создаётся скомпилированная версия этого скрипта. Здесь же вы можете найти все сборки, на которые ссылается скрипт (включая нашу GitHooks.dll
). И в подпапке execution-cache
вы можете найти SHA256-файл. Я могу предположить, что он содержит SHA256-хэш нашего файла pre-commit
. В тот момент, когда мы запускаем скрипт, среда исполнения сравнивает текущий хэш файла с сохранённым хэшем. Если они равны, будет использована сохранённая версия скомпилированного скрипта.
Это означает, что поскольку мы никогда не меняем файл pre-commit
, изменения в GitHooks.dll
никогда не достигнут кэша и никогда не будут использованы.
Что мы можем сделать в этой ситуации? Ну, нам поможет Reflection. Я перепишу мой скрипт так, чтобы он использовал Reflection вместо прямой ссылки на сборку GitHooks
. Вот как после этого будет выглядеть наш файл pre-commit
:
#!/usr/bin/env dotnet-script
#r "nuget: System.Runtime.Loader, 4.3.0"
using System.IO;
using System.Runtime.Loader;
var hooksDirectory = Path.Combine(Environment.CurrentDirectory, "gitHookAssemblies");
var assemblyPath = Path.Combine(hooksDirectory, "GitHooks.dll");
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
if(assembly == null)
{
Console.WriteLine($"Can't load assembly from '{assemblyPath}'.");
}
var collectorsType = assembly.GetType("GitHooks.RunHooks");
if(collectorsType == null)
{
Console.WriteLine("Can't find entry type.");
}
var method = collectorsType.GetMethod("RunPreCommitHook", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if(method == null)
{
Console.WriteLine("Can't find method for pre-commit hooks.");
}
method.Invoke(null, new object[0]);
Теперь мы можем обновлять GitHook.dll
в нашей папке gitHookAssemblies
в любой момент, и все изменения будут подхвачены тем же самым скриптом. Модифицировать сам скрипт больше не нужно.
Всё это звучит замечательно, но есть ещё одна проблема, которую следует решить перед тем, как двигаться далее. Я говорю о сборках, на которые ссылается наш код.
Используемые сборки
Всё работает прекрасно, пока единственной вещью, которую делает метод RunHooks.RunPreCommitHook
, является вывод строки на консоль. Но, откровенно говоря, обычно вывод текста на экран не представляет никакого интереса. Нам нужно делать более сложные вещи. А для этого нам требуется использовать другие сборки и NuGet-пакеты. Давайте посмотрим, как сделать это.
Я изменю RunHooks.RunPreCommitHook
так, чтобы он использовал пакет LibGit2Sharp
:
public static void RunPreCommitHook()
{
using var repo = new Repository(Environment.CurrentDirectory);
Console.WriteLine(repo.Info.WorkingDirectory);
}
Теперь, если я выполню git commit
, я получу следующее сообщение об ошибке:
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
---> System.IO.FileLoadException: Could not load file or assembly 'LibGit2Sharp, Version=0.26.0.0, Culture=neutral, PublicKeyToken=7cbde695407f0333'. General Exception (0x80131500)
Ясно, что нам требуется некоторый способ обеспечить загрузку сборок, на которые мы ссылаемся. Основная идея здесь такова. Я буду класть все требуемые для выполнения кода сборки в ту же папку gitHookAssemblies
вместе с моей GitHooks.dll
. Чтобы получить все требуемые сборки, вы можете воспользоваться командой dotnet publish
. В нашем случае нам нужно поместить в эту папку LibGit2Sharp.dll
и git2-7ce88e6.dll
.
Так же нам придётся изменить pre-commit
. Мы добавим в него следующий код:
#!/usr/bin/env dotnet-script
#r "nuget: System.Runtime.Loader, 4.3.0"
using System.IO;
using System.Runtime.Loader;
var hooksDirectory = Path.Combine(Environment.CurrentDirectory, "gitHookAssemblies");
var assemblyPath = Path.Combine(hooksDirectory, "GitHooks.dll");
AssemblyLoadContext.Default.Resolving += (context, assemblyName) => {
var assemblyPath = Path.Combine(hooksDirectory, $"{assemblyName.Name}.dll");
if(File.Exists(assemblyPath))
{
return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
}
return null;
};
...
Этот код будет пытаться загружать все сборки, которые среда исполнения не смогла найти самостоятельно, из папки gitHookAssemblies
.
Теперь можно запускать git commit
и он выполнится без проблем.
Улучшение расширяемости
Наш файл pre-commit
завершён. Больше нам не требуется изменять его. Но в случае необходимости внесения изменений, нам придётся изменять метод RunHooks.RunPreCommitHook
. Так что мы просто перенесли проблему на другой уровень. Лично я предпочёл бы иметь некоторую систему плагинов. Каждый раз, когда мне потребуется добавить некоторое действие, которое нужно выполнять перед заливкой кода, я просто будут писать новый плагин, и ничего изменять не потребуется. Насколько трудно этого добиться?
Совсем не трудно. Давайте воспользуемся MEF. Вот как он работает.
Сперва нужно определить интерфейс для наших обработчиков hook'ов:
public interface IPreCommitHook
{
bool Process(IList<string> args);
}
Каждый обработчик может получать от Git некоторые строковые аргументы. Эти аргументы будут передаваться через параметр args
. Метод Process
будет возвращать true
, если он разрешает заливку изменений. В противном случае будет возвращён false
.
Подобные интерфейсы можно определить для всех hook'ов, но в этой статье мы сосредоточимся только на pre-commit.
Теперь нужно написать реализацию этого интерфейса:
[Export(typeof(IPreCommitHook))]
public class MessageHook : IPreCommitHook
{
public bool Process(IList<string> args)
{
Console.WriteLine("Message hook...");
if(args != null)
{
Console.WriteLine("Arguments are:");
foreach(var arg in args)
{
Console.WriteLine(arg);
}
}
return true;
}
}
Такие классы можно создавать в разных сборках, если вам так хочется. Буквально нет никаких ограничений. Атрибут Export
берётся из NuGet-пакета System.ComponentModel.Composition
.
Так же давайте создадим вспомогательный метод, который будет собирать все реализации интерфейса IPreCommitHook
, помеченные атрибутом Export
, запускать их всех и возвращать информацию о том, все ли они разрешили заливку. Я поместил мой обработчик в отдельную сборку GitHooksCollector
, но это не так важно:
public class Collectors
{
private class PreCommitHooks
{
[ImportMany(typeof(IPreCommitHook))]
public IPreCommitHook[] Hooks { get; set; }
}
public static int RunPreCommitHooks(IList<string> args, string directory)
{
var catalog = new DirectoryCatalog(directory, "*Hooks.dll");
var container = new CompositionContainer(catalog);
var obj = new PreCommitHooks();
container.ComposeParts(obj);
bool success = true;
foreach(var hook in obj.Hooks)
{
success &= hook.Process(args);
}
return success ? 0 : 1;
}
}
Этот код так же использует NuGet-пакет System.ComponentModel.Composition
. Во-первых, мы говорим, что будем просматривать все сборки, чьё имя соответствует шаблону *Hooks.dll
, в папке directory
. Вы можете использовать здесь любой шаблон, какой вам нравится. Затем мы собираем все экспортированные реализации интерфейса IPreCommitHook
в объект PreCommitHooks
. И, наконец, мы запускаем все обработчики hook'а и собираем результат их выполнения.
Последней вещью, которую нам нужно сделать, является небольшое изменение файла pre-commit
:
#!/usr/bin/env dotnet-script
#r "nuget: System.Runtime.Loader, 4.3.0"
using System.IO;
using System.Runtime.Loader;
var hooksDirectory = Path.Combine(Environment.CurrentDirectory, "gitHookAssemblies");
var assemblyPath = Path.Combine(hooksDirectory, "GitHooksCollector.dll");
AssemblyLoadContext.Default.Resolving += (context, assemblyName) => {
var assemblyPath = Path.Combine(hooksDirectory, $"{assemblyName.Name}.dll");
if(File.Exists(assemblyPath))
{
return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
}
return null;
};
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
if(assembly == null)
{
Console.WriteLine($"Can't load assembly from '{assemblyPath}'.");
}
var collectorsType = assembly.GetType("GitHooksCollector.Collectors");
if(collectorsType == null)
{
Console.WriteLine("Can't find collector's type.");
}
var method = collectorsType.GetMethod("RunPreCommitHooks", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if(method == null)
{
Console.WriteLine("Can't find collector's method for pre-commit hooks.");
}
int exitCode = (int) method.Invoke(null, new object[] { Args, hooksDirectory });
Environment.Exit(exitCode);
И не забудьте поместить все задействованные сборки в папку gitHookAssemblies
.
Да, это было долгое вступление. Но теперь у нас есть вполне надёжное решение для создания обработчиков Git hook'ов на C#. Всё, что от нас требуется, это изменение содержимого папки gitHookAssemblies
. Её содержимое может быть помещено в систему контроля версий и, таким образом, распространено между всеми разработчиками.
В любом случае, нам пора вернуться к нашей первоначальной проблеме.
Web-сервис для резервирования констант
Мы хотели убедиться, что разработчики не смогут залить определённые изменения в случае, если они забыли зарезервировать за собой соответствующую константу на Web-сервисе. Давайте создадим простенький Web-сервис, чтобы можно было работать с ним. Я использую ASP.NET Core Web-сервис с Windows аутентификацией. Но на самом деле здесь возможны различные варианты.
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ListsService.Controllers
{
public sealed class ListItem<T>
{
public ListItem(T value, string owner)
{
Value = value;
Owner = owner;
}
public T Value { get; }
public string Owner { get; }
}
public static class Lists
{
public static List<ListItem<int>> SqlVersions = new List<ListItem<int>>
{
new ListItem<int>(1, @"DOMAIN\Iakimov")
};
public static Dictionary<int, List<ListItem<int>>> AllLists = new Dictionary<int, List<ListItem<int>>>
{
{1, SqlVersions}
};
}
[Authorize]
public class ListsController : Controller
{
[Route("/api/lists/{listId}/ownerOf/{itemId}")]
[HttpGet]
public IActionResult GetOwner(int listId, int itemId)
{
if (!Lists.AllLists.ContainsKey(listId))
return NotFound();
var item = Lists.AllLists[listId].FirstOrDefault(li => li.Value == itemId);
if(item == null)
return NotFound();
return Json(item.Owner);
}
}
}
Здесь, для целей тестирования, я использовал статический класс Lists
в качестве механизма хранения списков. Каждый список будет иметь целочисленный идентификатор. Каждый список будет содержать целочисленные значения и информацию о людях, за которыми эти значения зарезервированы. Метод GetOwner
класса ListController
позволяет получить идентификатор человека, за которым зарезервирован данный элемент списка.
Проверка SQL step-файлов
Теперь мы готовы проверить, можем ли мы залить новый step-файл или нет. Предположим для определённости, что мы храним step-файлы следующим образом. В корневой папке нашего проекта имеется каталог sql
. В нём каждый разработчик может создать папку verXXX
, где XXX
— некоторое число, которое должно быть предварительно зарезервировано на Web-сервисе. Внутри каталога verXXX
может быть один или несколько .sql
файлов, содержащих инструкции изменения базы данных. Мы не будем здесь обсуждать проблему обеспечения порядка выполнения этих .sql
файлов. Это не важно для нашего обсуждения. Мы просто хотим сделать следующее. Если разработчик пытается залить новый файл, содержащийся внутри папки sql/verXXX
, мы должны проверить, зарезервирована ли константа XXX
за этим разработчиком.
Вот как выглядит код соответствующего обработчика Git hook:
[Export(typeof(IPreCommitHook))]
public class SqlStepsHook : IPreCommitHook
{
private static readonly Regex _expr = new Regex("\\bver(\\d+)\\b");
public bool Process(IList<string> args)
{
using var repo = new Repository(Environment.CurrentDirectory);
var items = repo.RetrieveStatus()
.Where(i => !i.State.HasFlag(FileStatus.Ignored))
.Where(i => i.State.HasFlag(FileStatus.NewInIndex))
.Where(i => i.FilePath.StartsWith(@"sql"));
var versions = new HashSet<int>(
items
.Select(i => _expr.Match(i.FilePath))
.Where(m => m.Success)
.Select(m => m.Groups[1].Value)
.Select(d => int.Parse(d))
);
foreach(var version in versions)
{
if (!ListItemOwnerChecker.DoesCurrentUserOwnListItem(1, version))
return false;
}
return true;
}
}
Здесь мы используем класс Repository
из NuGet-пакета LibGit2Sharp
. Переменная items
будет содержать все новые файлы в индексе Git, которые расположены внутри папки sql
. Вы можете улучшить процедуру поиска таких файлов, если есть желание. В переменную versions
мы собираем различные константы XXX
из папок verXXX
. И, наконец, метод ListItemOwnerChecker.DoesCurrentUserOwnListItem
проверяет, зарегистрированы ли эти версии за текущим пользователем на Web-сервисе в списке 1.
Реализация ListItemOwnerChecker.DoesCurrentUserOwnListItem
довольно проста:
class ListItemOwnerChecker
{
public static string GetListItemOwner(int listId, int itemId)
{
var handler = new HttpClientHandler
{
UseDefaultCredentials = true
};
var client = new HttpClient(handler);
var response = client.GetAsync($"https://localhost:44389/api/lists/{listId}/ownerOf/{itemId}")
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
var owner = response.Content
.ReadAsStringAsync()
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
return JsonConvert.DeserializeObject<string>(owner);
}
public static bool DoesCurrentUserOwnListItem(int listId, int itemId)
{
var owner = GetListItemOwner(listId, itemId);
if (owner == null)
{
Console.WriteLine($"There is no item '{itemId}' in the list '{listId}' registered on the lists service.");
return false;
}
if (owner != WindowsIdentity.GetCurrent().Name)
{
Console.WriteLine($"Item '{itemId}' in the list '{listId}' registered by '{owner}' and you are '{WindowsIdentity.GetCurrent().Name}'.");
return false;
}
return true;
}
}
Здесь мы запрашиваем у Web-сервиса идентификатор пользователя, который зарегистрировал указанную константу (метод GetListItemOwner
). Затем полученный результат сравнивается с именем текущего пользователя Windows. Это только один из многих возможных путей реализации подобной функциональности. Например, вы можете использовать имя или e-mail пользователя из конфигурации Git.
Вот и всё. Просто соберите соответствующую сборку и поместите её в папку gitHookAssemblies
вместе со всеми её зависимостями. И всё заработает автоматически.
Проверка значений enum
Это здорово! Теперь никто не сможет залить изменения для базы данных, предварительно не зарезервировав за собой соответствующую константу на Web-сервисе. Но подобный метод может использоваться и в других местах, где требуется резервирование констант.
К примеру, где нибудь в коде проекта у вас есть enum. Каждый разработчик может добавлять в него новые члены с присвоенными целочисленными значениями:
enum Constants
{
Val1 = 1,
Val2 = 2,
Val3 = 3
}
Мы хотим избежать коллизии значений для членов этого перечисления. Поэтому мы требуем предварительного резервирования соответствующих констант на Web-сервисе. Насколько трудно реализовать проверку такого резервирования?
Вот как выглядит код для нового обработчика Git hook:
[Export(typeof(IPreCommitHook))]
public class ConstantValuesHook : IPreCommitHook
{
public bool Process(IList<string> args)
{
using var repo = new Repository(Environment.CurrentDirectory);
var constantsItem = repo.RetrieveStatus()
.Staged
.FirstOrDefault(i => i.FilePath == @"src/GitInteraction/Constants.cs");
if (constantsItem == null)
return true;
if (!constantsItem.State.HasFlag(FileStatus.NewInIndex)
&& !constantsItem.State.HasFlag(FileStatus.ModifiedInIndex))
return true;
var initialContent = GetInitialContent(repo, constantsItem);
var indexContent = GetIndexContent(repo, constantsItem);
var initialConstantValues = GetConstantValues(initialContent);
var indexConstantValues = GetConstantValues(indexContent);
indexConstantValues.ExceptWith(initialConstantValues);
if (indexConstantValues.Count == 0)
return true;
foreach (var version in indexConstantValues)
{
if (!ListItemOwnerChecker.DoesCurrentUserOwnListItem(2, version))
return false;
}
return true;
}
...
}
Сначала мы проверяем, был ли изменён файл, содержащий наше перечисление. Затем мы извлекаем содержимое этого файла из последней залитой версии и из индекса Git с использованием методов GetInitialContent
и GetIndexContent
. Вот их реализация:
private string GetInitialContent(Repository repo, StatusEntry item)
{
var blob = repo.Head.Tip[item.FilePath]?.Target as Blob;
if (blob == null)
return null;
using var content = new StreamReader(blob.GetContentStream(), Encoding.UTF8);
return content.ReadToEnd();
}
private string GetIndexContent(Repository repo, StatusEntry item)
{
var id = repo.Index[item.FilePath]?.Id;
if (id == null)
return null;
var itemBlob = repo.Lookup<Blob>(id);
if (itemBlob == null)
return null;
using var content = new StreamReader(itemBlob.GetContentStream(), Encoding.UTF8);
return content.ReadToEnd();
}
После этого мы извлекаем из обоих версий файла целочисленные значения членов перечисления. За это отвечает метод GetConstantValues
. Я использовал Roslyn
для реализации этой функциональности. Его можно задействовать с помощью NuGet-пакета Microsoft.CodeAnalysis.CSharp
.
private ISet<int> GetConstantValues(string fileContent)
{
if (string.IsNullOrWhiteSpace(fileContent))
return new HashSet<int>();
var tree = CSharpSyntaxTree.ParseText(fileContent);
var root = tree.GetCompilationUnitRoot();
var enumDeclaration = root
.DescendantNodes()
.OfType<EnumDeclarationSyntax>()
.FirstOrDefault(e => e.Identifier.Text == "Constants");
if(enumDeclaration == null)
return new HashSet<int>();
var result = new HashSet<int>();
foreach (var member in enumDeclaration.Members)
{
if(int.TryParse(member.EqualsValue.Value.ToString(), out var value))
{
result.Add(value);
}
}
return result;
}
При использовании Roslyn
я столкнулся со следующей проблемой. На тот момент, когда я писал этот код, последней версией пакета Microsoft.CodeAnalysis.CSharp
была 3.4.0
. Я поместил эту сборку в папку gitHookAssemblies
, но программа выдала ошибку, что она не может найти соответствующую версию сборки. Вот в чём причина такого поведения. Видите ли, dotnet-script
так же использует Roslyn
для своей работы. Это означает, что какая-то версия сборки Microsoft.CodeAnalysis.CSharp
уже была загружена в домен. В моём случае это была версия 3.3.1
. Когда в своём коде я стал использовать именно эту версию NuGet-пакета, проблема исчезла.
И, наконец, в методе Process
нашего обработчика hook`а, мы выбираем все новые значения и проверяем их владельцев на Web-сервисе.
Направления дальнейшего развития
Вот и всё. Наша система проверки резервирования констант построена. В конце я хотел бы кратко коснуться некоторых проблем, о которых ещё предстоит подумать.
Мы создали файл
pre-commit
, но ничего не сказали о том, как он попадёт в папки.git\hooks
на компьютерах всех разработчиков. Здесь можно использовать параметр--template
командыgit init
. Или же что-то подобное:
git config init.templatedir git_template_dir git init
Или можно использовать опцию
core.hooksPath
конфигурации Git, если у вас версия Git 2.9 или более поздняя:
git config core.hooksPath git_template_dir
Или же мы можем сделать это частью процесса сборки нашего приложения.
Тот же самый вопрос касается и инсталляции
dotnet-script
. Мы можем либо предустановить его на все машины разработчиков вместе с определённой версией .NET Core, либо так же сделать это частью процесса сборки приложения.
Лично я считаю наибольшей проблемой размещение сборок, на которые мы ссылаемся. Мы договорились, что будем помещать их в папку
gitHookAssemblies
, но я не уверен, что это поможет в любых ситуациях. Например, пакетLibGit2Sharp
поставляется с множеством нативных библиотек для различных операционных систем. Здесь я использовалgit2-7ce88e6.dll
, подходящую для Win-x64. Но если разные разработчики используют различные операционные системы, мы можем столкнуться с проблемой.
Мы почти ничего не сказали о реализации Web-сервиса. Здесь мы использовали Windows-аутентификацию, но существует и множество других возможностей. Так же Web-сервис должен предоставлять некоторый UI для резервирования новых констант и создания новых списков.
Возможно вы заметили, что использование асинхронных операций в наших обработчиках Git hook'ов было неудобным. Я думаю, что поддержку таких операций следует улучшить.
Заключение
В этой статье мы научились строить надёжную систему для создания обработчиков Git hook`ов на языках .NET. На этой основе мы создали несколько таких обработчиков, позволяющих проверять резервирование различных констант и предотвращать заливку кода в случае нарушений.
Я надеюсь, что эта информация будет полезна вам. Удачи!
P.S. Код для этой статьи вы можете найти на GitHub.