Miłosz Orzeł

.net, js, html, arduino, java... no rants or clickbaits.

TortoiseSVN pre-commit hook w C# – uchroń się przed głupim błędem

Chyba każdemu podczas tworzenia albo debugowania programu zdarza się wprowadzać do kodu tymczasowe zmiany, które ułatwiają bieżące zadanie ale nie powinny trafić do repozytorium. Chyba każdemu zdarzyło się też wbić taki kod do kolejnej rewizji. Jeśli masz szczęście błąd szybko się ujawni i skończy się na odrobinie wstydu, jeśli nie...

Gdyby tylko można było jakoś oznaczyć „nie-commitowalny” kod...

Można i to całkiem prosto! 

TortoiseSVN umożliwia ustawienie tzw. pre-commit hook. Jest to program (lub skrypt) uruchamiany w chwili gdy użytkownik naciśnie przycisk „OK”w oknie „SVN Commit”. Hook może np. zbadać zawartość modyfikowanych plików i zablokować commit jeśli zajdzie taka potrzeba. Hooki Tortoise’a różnią się tym od hooków Subversion, że są odpalane lokalnie a nie na serwerze, na którym przechowywane jest repozytorium. Działasz więc na swojej maszynie, nie musisz obawiać się oto czy Twój hook zostanie zaakceptowany przez admina i czy zadziała na serwerze (serwer może np. nie obsługiwać .NET), nie wpływasz też na innych użytkowników repozytorium. Poza tym hooki client-side działają szybciej...

Szczegółowy opis hooków znajduje się w rozdziale „4.30.8. Client Side Hook Scripts” pliku pomocy Tortoise.

TSVN obsługuje 7 rodzai hooków: start-commit, pre-commit, post-commit, start-update, pre-update, post-update oraz pre-connect.  My zajmiemy się obsługą akcji pre-commit. Istotą hooka będzie wykrycie czy wśród dodawanych lub aktualizowanych plików nie znajduje się taki, który zawiera znacznik kodu tymczasowego. Za taki znacznik uznamy tekst „NOT_FOR_REPO”, który można umieścić w komentarzu nad kodem tymczasowym. 

Oto cały kod hooka – prosta aplikacja konsolowa, która może uratować Ci dupę :)

using System;
using System.IO;
using System.Text.RegularExpressions;

namespace NotForRepoPreCommitHook
{
    class Program
    {
        const string NotForRepoMarker = "NOT_FOR_REPO";

        static void Main(string[] args)
        {              
            string[] affectedPaths = File.ReadAllLines(args[0]);

            Regex fileExtensionPattern = new Regex(@"^.*\.(cs|js|xml|config)$", RegexOptions.IgnoreCase);

            foreach (string path in affectedPaths)
            {
                if (fileExtensionPattern.IsMatch(path) && File.Exists(path))
                {
                    if (ContainsNotForRepoMarker(path))
                    {
                        string errorMessage = string.Format("{0} marker found in {1}", NotForRepoMarker, path);
                        Console.Error.WriteLine(errorMessage);    
                        Environment.Exit(1);  
                    }
                }
            }             
        }

        static bool ContainsNotForRepoMarker(string path)
        {
            StreamReader reader = File.OpenText(path);

            try
            {
                string line = reader.ReadLine();

                while (line != null)
                {
                    if (line.Contains(NotForRepoMarker))
                    {
                        return true;
                    }

                    line = reader.ReadLine();
                }
            }
            finally
            {
                reader.Close();
            }  

            return false;
        }
    }
}

TSVN wywołuje hook typu pre-commit z czterema parametrami. Nas interesuje jednak tylko pierwszy parametr, który zawiera ścieżkę do pliku *.tmp. W pliku tym znajdują się ścieżki do obsługiwanych przy danym commicie plików. Każda linia to jedna ścieżka. Po załadowaniu listy plików, filtrujemy je po rozszerzeniu (przydatne jeśli nie chcesz sprawdzać plików wszystkich typów). Ważne jest też sprawdzenie czy plik istnieje – lista w pliku *.tmp  zawiera także ścieżki do usuwanych plików! Samo wykrycie znacznika określonego przez stałą NotForRepoMarker znajduje się w metodzie ContainsNotForRepoMarker. Mimo swojej prostoty zapewnia niezłą wydajność. Na moim (średniej klasy) laptopie, przeglądniecie 100 MB pliku nie trwa nawet sekundy. Jeśli znacznik zostaje wykryty program przerwa pracę z kodem błędu (wartość różna od 0). Przed zakończeniem do standardowego wyjścia błędu (Console.Error) dodawane jest info o tym, który plik posiada znacznik kodu tymczasowego. Tekst ten trafi do okna Tortoise.

Prawa, że kod jest prosty? Na dodatek instalacja hooka jest banalna!

By podłączyć hook wybierz opcje „Settings” z menu kontekstowego TortoiseSVN. Następnie wybierz element „Hook Scripts” i naciśnij przycisk „Add...”. Pojawi się takie okno:

Okno konfiguracji hooków TSVN

Hook Type” ustaw na „Pre-Commit Hook”. W polu „Working Copy Path” ustaw ścieżkę do katalogu z lokalną kopią repo, którego ma dotyczyć hook (różne foldery mogą mieć różne hooki). W polu „Command Line To Execute” ustaw ścieżkę do aplikacji implementującej hooka. Zaznacz opcje „Wait for the script to finish” i „Hide the script while running” (nie chcesz przecież by okno konsoli było na chwilę widoczne). Naciśnij „OK” i voila, hook zainstalowany!

Spróbuj teraz oznaczyć jakiś fragment kodu komentarzem „NOT_FOR_REPO” i wykonać commit. Powinieneś zobaczyć coś takiego:

Zablokowanie operacji przez pre-commit hook

Zwróć uwagę na przycisk „Retry without hooks” – pozwala on na wykonanie operacji commit przy zignorowaniu hooków.

Mamy już hook chroniący przed wgraniem testowego kodu. Można by też pokusić się o hook, który wymusza wpisanie komentarza, blokuje wgranie pliku typu *.log itp. Twoje prywatne hooki – Ty decydujesz! A jeśli któreś z nich przydadzą się całemu zespołowi można zawsze przerobić je na hooki Subversion działające na serwerze.

Testowane na TortoiseSVN 1.7.8/Subversion 1.7.6.1

Aktualizacja 24.03.2014: Wyróżnienie informacji o opcji "Wait for the script to finish" bez niej hook nie zablokuje commita!

Aktualizacja 17.09.2013 (dodatkowe info): Hook może zostać ustawiony na folderze nadrzędnym zawierającym checkouty różnych repozytoriów. Jeśli jesteś gotów poświecić nieco wydajności dla dodatkowego bezpieczeństwa to możesz zrezygnować z filtrowania plików przed sprawdzeniem znacznika NotForRepoMarker.