Cake vs MSBuild

cake

Po ostatnich problemach z Visual Studio ( VISUAL STUDIO : BUILD VS PUBLISH ) postanowiłem wziąć sprawy w swoje ręce i przygotować własne narzędzie do tworzenia paczek wdrożeniowych z poziomu Visual Studio.

Gdy tylko zabrałem się do pracy w mojej głowie pojawiło się kilka pomysłów:

– okiełznanie wszystkiego poprzez MSBuild
– oskryptowanie wszystkiego w PowerShellu
– wykorzystanie Roslyn

Jednak robiąc rozeznanie natrafiłem na idealne z mojego punktu widzenia narzędzie, jakim jest Cake i właśnie to narzędzie stało się sercem mojego nowego mechanizmu tworzenia paczek wdrożeniowych z poziomu Visual Studio.

Cake to międzyplatformowe narzędzie przeznaczone do automatyzacji procesu budowania i wdrażania aplikacji, które oferuje nam szeroki zestaw funkcjonalności takich, jak m.in. kompilowanie kodu, operacje na plikach i folderach, uruchamianie testów jednostkowych, kompresja plików oraz przygotowywanie paczek NuGet.

Jak zatem wykorzystać Cake do stworzenia narzędzia generującego paczki wdrożeniowe wprost z Visual Studio ?

Krok 1 – to pobranie Cake

Skrypty Cake można pobrać ze strony głównej projektu: Cake getting started. Po sklonowaniu repozytorium lub rozpakowaniu pobranego zipa otrzymujemy w pakiecie:

– główne skrypty Cake ( build.ps1, build.sh )
– przykładową aplikację ( folder src )
– przykład skryptu Cake ( build.cake )

Krok 2 – przygotowania pliku build.cake

Bazując na przykładowym pliku build.cake możemy przystąpić do definiowania własnego pliku dostosowanego do naszych potrzeb i naszej aplikacji. W moim przypadku głównym problemem było przenoszenie folderu MockData ( pochodzącego z oddzielnego projektu ) z folderu bin do App_Data, który znajduje się poziom wyżej niż katalog bin.

W przypadku standardowego podejścia Visual Studio + MSBuild konieczne było po pierwsze zdefiniowanie odpowiedniego post build eventu a po drugie dodanie odpowiedniej sekcji do pliku definiującego profil publikuący naszą aplikację. Takie samo zachowanie trzeba było więc zdefiniować w dwóch różnych miejscach.

Jak zrobić to znacznie prościej za pośrednictwem Cake ?

Pierwsze co musimy zdefiniować w naszym skrypcie to target i konfiguracja, która zostanie wykorzystana podczas budowania naszej aplikacji:

var target = Argument("target", "Default");
var configuration = Argument("configuration", "Release");

Następnie definiujemy katalog wyjściowy:

var buildDir = Directory("./MyTestCakeProject/bin");

Po czym przechodzimy do definiowania zadań naszego skryptu:

Pierwszy task to czyszcznie folderu wyjściowego:

Task("Clean")
.Does(() =>
{
CleanDirectory(buildDir);
});

Drugi task to aktualizacja paczek nugetowych:

Task("Restore-NuGet-Packages")
.IsDependentOn("Clean")
.Does(() =>
{
NuGetRestore("./MyTestCakeProject.sln");
});

Trzeci task odpowiedzialny jest za budowanie naszej aplikacji:

Task("Build")
.IsDependentOn("Restore-NuGet-Packages")
.Does(() =>
{
if(IsRunningOnWindows())
{
// Use MSBuild
MSBuild("./MyTestCakeProject.sln", settings =>
settings.SetConfiguration(configuration));
}
else
{
// Use XBuild
XBuild("./MyTestCakeProject.sln", settings =>
settings.SetConfiguration(configuration));
}
});

Następny task to kluczowa część naszego skryptu czyli tworzenie naszej paczki wdrożeniowej.

Task("CopyFakeFiles")
.IsDependentOn("Build")
.Does(() =>
{
if(!DirectoryExists("./MyTestCakeProject/App_Data/FakeFiles"))
{
CreateDirectory("./MyTestCakeProject/App_Data/FakeFiles");
}
else
{
CleanDirectory("./MyTestCakeProject/App_Data/FakeFiles");
}

MoveFiles("./MyTestCakeProject/bin/FakeFiles/*", "./MyTestCakeProject/App_Data/FakeFiles");
});

Task("CreatePackage")Task("CreatePackage")    .IsDependentOn("CopyFakeFiles")    .Does(() =>{ if(!DirectoryExists("./MyTestCakeProject/Deploy")) { CreateDirectory("./MyTestCakeProject/Deploy"); }    else { CleanDirectory("./MyTestCakeProject/Deploy"); } CopyDirectory("./MyTestCakeProject/App_Data", "./MyTestCakeProject/Deploy/App_Data"); CopyDirectory("./MyTestCakeProject/Areas", "./MyTestCakeProject/Deploy/Areas"); CopyDirectory("./MyTestCakeProject/bin", "./MyTestCakeProject/Deploy/bin"); CopyDirectory("./MyTestCakeProject/Content", "./MyTestCakeProject/Deploy/Content");    CopyDirectory("./MyTestCakeProject/fonts", "./MyTestCakeProject/Deploy/fonts"); CopyDirectory("./MyTestCakeProject/Scripts", "./MyTestCakeProject/Deploy/Scripts"); CopyDirectory("./MyTestCakeProject/Views", "./MyTestCakeProject/Deploy/Views"); CopyFile("./MyTestCakeProject/favicon.ico", "./MyTestCakeProject/Deploy/favicon.ico"); CopyFile("./MyTestCakeProject/Global.asax", "./MyTestCakeProject/Deploy/Global.asax"); CopyFile("./MyTestCakeProject/packages.config", "./MyTestCakeProject/Deploy/packages.config"); CopyFile("./MyTestCakeProject/Web.config", "./MyTestCakeProject/Deploy/Web.config"); Zip("./MyTestCakeProject/Deploy", "./MyTestCakeProject/Deploy.zip");});

Ostatni task to task, który zamyka nam w całość wszystkie poprzednio zdefiniowane zadania

Task("Default")
.IsDependentOn("CreatePackage");

Po zdefiniowaniu wszystkich niezbędnych zadań nie pozostaje nam nic innego jak uruchomić nasz target

RunTarget(target);

Krok 3 – dodanie plików Cake do Visual Studio

Wystarczy teraz, że nowo utworzone pliki Cake dodamy do naszej solucji w Visual Studio, tym samym kończąc pracę nad naszym nowym narzędziem do tworzenia
paczek wdrożeniowych.

Krok 4 – wykonywanie skrytpu Cake z poziomu Visual Studio

Po dodaniu plików Cake do solucji najlepszym rozwiązaniem byłaby możliwość ich uruchamiania bezpośrednio z poziomu Visual Studio.
Jak to zrobić ? Przeczytaj w moim wcześniejszym poście.

Hurra !!! Nasze własne narzędzie do tworzenia paczek wdrożeniowych z poziomu Visual Studio ukończone !!!

Podsumowując

1) Skrypt build.cake rozwiązuje problemy opisane w poście VISUAL STUDIO : BUILD VS PUBLISH
2) Skrypt build.cake dla programistów .NET jest zdecydowanie prostszy w tworzeniu i modyfikacji niż MSBuild
3) Cake jest narzędziem multiplatformowym ( działa zarówno na Windows, Linux jak i macOS)
4) Cake wspiera większość narzędzi wykorzystywanych podczas budownia naszych aplikacji takich jak. np. MSBuild, MSTest, xUnit, Nuget itd.

 

Advertisements

Wykonywanie skryptu PowerShell z poziomu Visual Studio

powershell

Domyślnie Visual Studio nie pozwala wykonywać skryptów PowerShell a w menu kontekstowym dostępna jest jedynie opcja Open with PowerShell ISE.

Jak zatem umożliwić wykonywanie skryptów PowerShell z poziomu Visual Studio ?

Odpowiedzią na to pytanie jest post: http://nickmeldrum.com/blog/how-to-run-powershell-scripts-from-solution-explorer-in-visual-studio-2010

Jednak rozwiązania opisane w załączonym poście nie zadziałają od ręki… Dlaczego ?  Dlatego, że otrzymamy następujący komunikat:

File …\script.ps1 cannot be loaded because the execution of scripts is disabled on this system. Please see “get-help about_signing” for more details.
+ CategoryInfo : NotSpecified: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : RuntimeException

Konieczne jest wprowadzenie małej modyfikacji.

W polu Arguments bezpośrednio przed -file należy dodać następującą linijkę :

-ExecutionPolicy ByPass

vs_settings_ps

Linijka ta pozwala na wykonanie skryptu bez sprawdzania “execution policy” dla wskazanego pliku.

Po dodaniu odpowiedniej komendy wraz z opisaną tu poprawką i skonfigurowaniu Visual Studio zgodnie ze wskazówkami z podlinkowanego postu będziemy mogli w prosty sposób poprzez menu kontekstowe uruchamiać skrypty PowerShell bezpośrednio z poziomu Visual Studio.

vs_run_ps

Visual Studio : Build vs Publish

Visual_Studio_2013_Logo.svg

Visual Studio to niesamowite narzędzie, które pozwala w wygodny sposób tworzyć, uruchamiać oraz debugować tworzone przez nas aplikacje. Duża intuicyjność i zazwyczaj bezbłędne działanie powodują, że rzadko zagłębiamy się w to, co faktycznie dzieje się pod maską tego pięknego programu.

Dopóki wszystko działa dobrze jesteśmy zadowoleni a Visual Studio nazywamy swoim programistycznym rajem. Jednak często okazuje się, że granica między rajem a piekłem jest niezwykle cienka…

Sam ostatnio wpadłem w diabelską pułapkę jednej z opcji, która zazwyczaj ułatwia życie automatyzując proces tworzenia paczki wdrożeniowej czyli opcji : Publish. Do pewnego momentu ( jak większość funkcji Visual Studio ) działała ona wyśmienicie. Dwa kliki myszką i bach paczka wygenerowana, gotowa do wrzucenia na środowisko. Jednak pewnego dnia ta jakże banalna w użyciu funkcja po prostu przestała działać… Dlaczego… ?

Tworzoną przeze mnie aplikacją był serwis REST, który powstawał przy wykorzystaniu Microsoft ASP.NET MVC Web API.

W serwisie tym wykorzystywałem frameworkowy folder App_Data. Folder ten zawierał statyczne pliki, które mogły być pobierane z aplikacji. Trafiały do niego również pliki przesyłane przez użytkowników.

Ponieważ część funkcjonalności mojej aplikacji nie była jeszcze zaimplementowana w pełni, to w folderze tym chciałem umieścić również inne dokumenty. Dokumenty, w których miały się znaleźć dane, które docelowo moja aplikacja miała zwracać użytkownikom z innych źródeł.

Ponieważ folder App_Data wykorzystywany był jako kontener do plików przesyłanych przez użytkowników nie mógł znajdować się w katalogu bin mojej aplikacji, gdyż każdorazowa zmiana jego zawartości powodowałaby restart mojej aplikacji. Dlatego też folder ten przy każdym wdrożeniu na dane środowisko przenoszony był poziom wyżej (poza katalog bin).

Żeby oddzielić prawdziwe dane zwracane przez aplikację od tych, które zostały zamockowane, stworzyłem specjalnie przeznaczony do tego celu projekt o wymownej nazwie MockData a w nim zawarłem wszelkie dokumenty mockujące poszczególne części mojej aplikacji.

Ponieważ pliki te znajdowały się w innym projekcie, ( do którego referencję posiadała moja aplikacja ) to podczas procesu kompilacji solucji przenoszone były do folderu bin mojej aplikacji i nie trafiały do docelowego folderu App_Data znajdującego się poziom wyżej.

Aby za każdym razem nie przenosić ich ręcznie we właściwościach aplikacji zdefiniowałem następujący post build event, którego zadaniem było przenoszenie zamockowanych plików z folderu bin jeden poziom wyżej do folderu App_Data.

publish_error

Lokalnie wszystko działało wyśmienicie, po kompilacji solucji dokumenty przenoszone były z katalogu bin do App_Data. Niestety problem pojawił się wówczas, gdy postanowiłem wdrożyć aplikację na środowisko testowe…

Tak samo jak dotychczas wykorzystałem do tego celu wbudowane w Visual Studio narzędzie Publish.

Raz dwa wygenerowałem paczkę i umieściłem ją na serwerze. Ku mojemu zaskoczeniu okazało się, że aplikacja podczas próby pobrania plików z App_Data sypie błędami, ponieważ nie może odnaleźć wskazanych plików. Plików, które skopiowane miały być z projektu MockData….

Pierwsze co przyszło mi na myśl to sprawdzenie co generuje Visual Studio podczas kompilacji mojej solucji. Okazało się, że sam build działa tak jak tego oczekiwałem. Przecież aplikacja lokalnie działała bez zarzutu…

Przyszła więc kolej na przyjrzenie się temu co dziej się podczas tworzenia paczki poprzez wbudowaną opcję Publish Visual Studio.

Po kilku próbach okazało się, że funkcja Publish nie bierze pod uwagę plików przekopiowanych przez post build event !

Dlaczego ??? Wystarczy spojrzeć na to co generuje Visual Studio w oknie Output po kliknięciu na Publish.

output_after_publish

Podczas publikowania aplikacji Visual Studio w pierwszej kolejności kompiluje ją wraz z jej zależnościami. Następnie co nie powinno nikogo dziwić uruchamiany jest post build event, który kopiuje pliki przez nas wskazane z projektu MockData do App_Data. Po procesie kompilacji następuje tworzenie paczki wdrożeniowej. Jednak kopiowane są do niej tylko te pliki, które brane były pod uwagę w procesie kompilacji. Pliki skopiowane przez post build event dla mechanizmu tworzącego paczkę wdrożeniową pozostają niewidoczne.

Co zatem można zrobić żeby naprawić proces tworzenia paczki wdrożeniowej ?

Jedną z opcji jest edycja xml’a definiującego profil publikujący aplikację.

publish_profile_xml

Aby podczas publikowania aplikacji Visual Studio wzięło pod uwagę również pliki kopiowane przez post build event należy do xml opisującego profil publikujący aplikację dodać sekcję PipelineCollectionFilesPhaseDependsOn.

<?xml version="1.0" encoding="utf-8"?>
<!--
This file is used by the publish/package process of your Web project. You can customize the behavior of this process
by editing this MSBuild file. In order to learn more about this please visit http://go.microsoft.com/fwlink/?LinkID=208121. 
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <WebPublishMethod>FileSystem</WebPublishMethod>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <SiteUrlToLaunchAfterPublish />
    <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <publishUrl>C:\Users\Marek Seweryn\Documents\Visual Studio 2013\Projects\WebApplication2</publishUrl>
    <DeleteExistingFiles>True</DeleteExistingFiles>
    <PipelineCollectFilesPhaseDependsOn>
      CustomCollectFiles;
    </PipelineCollectFilesPhaseDependsOn>
  </PropertyGroup>
  <Target Name="CustomCollectFiles">
    <Message Text="++++++Inside of CustomCollectFiles" Importance="high" />
    <Message Text="$(MSBuildThisFileDirectory)" Importance="high" />
    <ItemGroup>
      <_CustomFiles Include="$(MSBuildThisFileDirectory)..\..\..\MockData\MockData\**\*" />
      <FilesForPackagingFromProject Include="%(_CustomFiles.Identity)">
        <DestinationRelativePath>App_Data\MockData\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
      </FilesForPackagingFromProject>
    </ItemGroup>
  </Target>
</Project>

Dodając to wskazujemy Visual Studio aby do generowanej paczki dodał również pliki skopiowane z naszego projektu.

Po odpaleniu Publish pliki zostały skopiowane !!! Hurrra !!!

Po raz kolejny widać, że Microsoftowe Wizardy nie we wszystkich przypadkach się sprawdzają…. Niestety ale często musimy dopisywać trochę magii żeby uzyskać zamierzony efekt.

Pocieszeniem jest fakt, że paczkę wdrożeniową możemy utworzyć w dużo prostszy sposób wykorzystując do tego narzędzie takie jak : Cake.

Jak to zrobić opiszę w następnym poście !!!

Jak nie Zip-ować katalogów w C# ?!

archive

Często w pracy programisty zdarza się, że tracimy mnóstwo czasu na rzeczy, które na pierwszy rzut oka wydają się banalne do zaimplementowania.

Czasem przyczyną naszej wielogodzinnej męczarni jest brakujący średnik w kodzie a czasem niespodzianki, którymi zaskakuje nas wykorzystywany przez nas język, biblioteka czy framework.

Nie zdarza się to co prawda często ( no chyba, że pokusiliśmy się na użycie biblioteki czy frameworka w wersji beta) jednak, gdy już nas to dopadnie nierzadko stajemy na krawędzi programistycznego załamania nerwowego.

Mnie osobiście ostatnio do szewskiej pasji doprowadziło banalne z nazwy zip-owanie plików.

Zip-owanie plików to nic innego, jak kompresja i archiwizowanie wielu różnych plików czy też katalogów w postaci pojedynczego pliku z rozszerzeniem .zip.

Archiwa zip możemy tworzyć bezpośrednio w systemie Windows jak również poprzez specjalne programy do archiwizowania plików.

Jednak z programistycznego punktu widzenia najważniejsze jest to, że możemy je również tworzyć z poziomu kodu w języku C#.

Sama procedura tworzenia archiwum zip jest wręcz trywialna:

a) Wskazujemy pliki, które chcemy umieścić w archiwum

b) Odczytujemy zawartości wskazanych plików

c) Odczytane dane z plików wraz z nazwami plików przekazujemy do obiektu archiwum

d) Zapisujemy obiekt archiwum zip na dysku

Zatem jak powyższa procedura będzie wyglądać w postaci kodu w języku C# ?

Poniższy fragment kodu jest właśnie implementacją tejże procedury:

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;

namespace ZipExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var zipService = new ZipService();
            var filesToZip = FilesToBeZippedProvider.GetFilesToZip();
            var zippedFiles = zipService.ZipFiles(filesToZip).Result;
            File.WriteAllBytes("test.zip", zippedFiles.Content);
        }
    }
}

public class FilesToBeZippedProvider
{
    public static List GetFilesToZip()
    {
        var filesToZip = new List();
            var inputFiles = Directory.GetFiles("InputFiles");

            foreach (var file in inputFiles)
            {
                Console.WriteLine(file);
                Console.WriteLine(System.IO.File.ReadAllBytes(file).Length);
                filesToZip.Add(new TestFile()
                {
                    FileName = file,
                    Content = System.IO.File.ReadAllBytes(file)
                });
            }
            return filesToZip;
}
}

public class ZipService
{
    public async Task ZipFiles(IEnumerable files)
    {
        using (var memoryStream = new MemoryStream())
        {
            using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create))
            {
                foreach (var item in files)
                {
                    ZipArchiveEntry readmeEntry = archive.CreateEntry(item.FileName);
                    using (var stream = readmeEntry.Open())
                    {
                        await stream.WriteAsync(item.Content, 0, item.Content.Length);
                    }
                }
            }
            return new TestFile()
            {
                Content = memoryStream.ToArray(),
                FileName = "tst",
            };
        }
    }
}

public class TestFile
{
    public int Id { get; set; }
    public string FileName { get; set; }

    public byte[] Content { get; set; }
}
Wszystko na pierwszy rzut oka wygląda jak najbardziej OK. Przecież kod jest napisany zgodnie z przykładami, jakie możemy znaleźć na msdn
Ok zerknijmy zatem co fizycznie zawiera utworzone przez nasz kod archiwum ?
ZipNoOk1ZipNoOk2

Niestety nie wygląda ono, tak jakbyśmy się tego spodziewali. Winną tego stanu rzeczy jest jedna linijka w kodzie….Potrafisz ją znaleźć ???

Tak chodzi tutaj o przekazywanie nazwy plików do obiektu archiwum.

Jeżeli nazwy plików przekażemy w postaci całej ścieżki, która prowadzi do pliku to wówczas zostanie to odwzorowane również w naszym archiwum. Problemu nie ma wówczas, gdy faktycznie jest to naszym zamiarem ale jak sobie z tym poradzić, gdy jednak pliki, które pochodzą z wielu różnych katalogów chcemy aby bezpośrednio znalazły się w tworzonym przez nas archiwum ?

Wystarczy, że do obiektu archiwum przekażemy wyłącznie nazwę naszego pliku.

public class FilesToBeZippedProvider
{
    public static List GetFilesToZip()
    {
        var filesToZip = new List();
            var inputFiles = Directory.GetFiles("InputFiles");

            foreach (var file in inputFiles)
            {
                Console.WriteLine(file);
                Console.WriteLine(System.IO.File.ReadAllBytes(file).Length);
                filesToZip.Add(new TestFile()
                {
                    FileName = Path.GetFileName(file),
                    Content = System.IO.File.ReadAllBytes(file)
                });
            }
            return filesToZip;
    }
}

Wprowadzając tę mikro poprawkę uzyskamy, takie archiwum na jakim nam zależało czyli w przypadku mojego prostego przykładu archiwum o postaci:

ZipOk

Powyższa jedna linijka kodu zabrała mi około dwóch godzin z mego programistycznego życia, które zamiast na implementowaniu dalszej części funkcjonalności spędziłem na analizowaniu i debugowaniu innych części systemu, w których to niesłusznie doszukiwałem się błędów…

Git a ewidencja czasu pracy

productivity-1995786_1920

Drogi programisto, droga programistko…

Czy kiedykolwiek musiałeś/musiałaś spowiadać się z tego co konkretnie wyszło spod Twoich palców w ostatnim dniu, tygodniu, miesiącu czy roku ?

Zazwyczaj nie… i dobrze, gdyż zwykle miarą Twojej produktywności jest ilość zadań, które udało Ci się zrealizować w ostatnim sprincie. To właśnie te zadania widzi Twój PM i tylko to przekłada się później na to co widzi klient.

Niestety problem pojawia się wówczas, gdy np. chcesz uzyskać wynagrodzenie powiększone o koszty uzyskania przychodu z tytułu praw autorskich lub jesteś osobą prowadzącą jednoosobową działalność gospodarczą, polegającą na realizacji zleceń w projektach informatycznych.

Wówczas nie tylko Twój team będzie chciał zajrzeć w Twój kod, żeby popastwić się nad Tobą i wytknąć najgłupsze błędy podczas code reeview. Ponadto nie tylko Twój szef / PM czy kierownik, będzie chciał zweryfikować ile faktycznie czasu zajęło Ci zrealizowanie danego zadania. (patrz Ewidencja czasu pracy dla samozatrudnionych).

Wiedz, że w takim przypadku Twój kod może być również weryfikowany przez organy zewnętrzne. I to wcale nie po to, żeby pokazać jak dbać o jego jakość i wydajność… O nie… Również to ile czasu spędziłeś nad danym zadaniem nie posłuży jedynie do wyrysowania pięknego wykresu spalania zadań z backlogu podczas danego sprintu…

Jedynym celem tych organów będzie bowiem udowodnienie Ci, że pieniążki, które otrzymałeś / otrzymałaś wcale Ci się nie należały, i że zapewne Twoim głównym celem podczas tworzenia tego kodu była chęć oszukania państwa na dobre kilka stówek miesięcznie…

Jak więc udowodnić, że te pieniążki jednak Ci się należały ?

Właśnie do tego celu przyda Ci się Twoja własna ewidencja czasu pracy.

Jak ją prowadzić ? Najprościej poprzez comiesięczne generowanie pliku – zestawienia wszystkich utworzonych, edytowanych oraz dodanych linijek kodu w projekcie.

Jeżeli tworzysz swoje projekty wykorzystując GITa, jako system kontroli wersji, to wiedz, że stworzenie pliku zawierającego wszystkie Twoje zmiany z danego okresu czasu jest wręcz banalne, i można dokonać tego jedną linijką polecenia wpisując.

git log –patch –after=”2017-01-01″ –before=”2017-01-31″ –author=”login” –all > ../myWork.txt

Po wykonaniu powyższej komendy otrzymamy pliczek, w którym znajdą się wszystkie nasze zakomitowane zmiany ze wskazanego przez nas przedziału czasu.

Czy to wystarczy ? Tak naprawdę nie wiem… I nie wiem czy ktokolwiek w 100% jest w stanie to stwierdzić.

Niestety jak większość tego typu przypadków w naszym prawie, także i to zależeć może od interpretacji odpowiednich organów…

A może zna ktoś lepsze sposoby na ewidencjonowanie programistycznej harówki ??

Jeżeli tak to koniecznie dajcie znać w komentarzach !!!