Korzystając z systemu kontroli wersji Git, zdarza się od czasu do czasu ujrzeć warning o następującej treści „LF will be replaced by CRLF in <file>. The file will have its original line endings in your working directory”. Zmiana znaku lub sekwencji znaków oznaczających zakończenie linii tekstu może spowodować błędną interpretacje zawartości przy odczycie danych. Zakończę w tym miejscu wstęp i od razu przejdę do analizy problemu i przedstawię możliwe rozwiązania.

Problem

Najlepiej wyjaśnić sposób poradzenia sobie z problem na jakimś przykładzie. Wyobraźmy sobie, że w ramach prac programistycznych zależy nam na uruchomieniu zainicjalizowanej bazy danych na SQL Server 2017 w kontenerze Docker. W celu wykonania zadania utworzę następujące pliki:

  • docker-compose.yml (zawiera definicje kontenera SQL Server);
version: "3.7"

services:
  mssqlServer:
    image: microsoft/mssql-server-linux:2017-latest
    container_name: "mssqlServer"
    environment:
      SA_PASSWORD: Qwerty1!
      ACCEPT_EULA: Y
    ports:
      - "1433:1433"
    restart: always
    working_dir: /Scripts
    volumes:
      - ./Scripts:/Scripts
    command: bash -c "sh createDB.sh & /opt/mssql/bin/sqlservr"
  • createDB.sh (skrypt uruchamiający za pomocą narzędzia sqlcmd polecenia T-SQL z pliku createDatabase.sql);
#!/bin/ssh

sleep 20s
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P ${SA_PASSWORD} -i createDatabase.sql
  • createDatabase.sql (plik zawiera polecenia T-SQL do utworzenia bazy danych o nazwie Mundial).
-- Create a new database called 'Mundial'
-- Connect to the 'master' database to run this snippet
USE master
GO
-- Create the new database if it does not exist already
IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = N'Mundial')
CREATE DATABASE Mundial
GO

USE Mundial
GO

-- Create a new table called 'Players' in schema 'dbo'
-- Drop the table if it already exists
IF OBJECT_ID('dbo.Players', 'U') IS NOT NULL
DROP TABLE dbo.Players
GO
-- Create the table in the specified schema
CREATE TABLE dbo.Players
(
    PlayersId INT NOT NULL IDENTITY(1,1) PRIMARY KEY, -- primary key column
    FirstName [NVARCHAR](50) NOT NULL,
    LastName [NVARCHAR](50) NOT NULL,
    Nationality [NVARCHAR](50) NOT NULL,
    Height tinyint NOT NULL,
    Age tinyint NOT NULL,
    Position [NVARCHAR](50) NOT NULL,
    CurrentClub [NVARCHAR](50) NOT NULL,
    MarketValue real NOT NULL
);
GO

-- Insert rows into table 'dbo.Players'
INSERT INTO dbo.Players
( -- columns to insert data into
 [FirstName], [LastName], [Nationality], [Height], [Age], [Position], [CurrentClub], [MarketValue]
)
VALUES
( 'Jan', 'Kowalski', 'Poland', 185, 21, 'Left Wing', 'DRY Opole' , 1.2),
( 'Marian', 'Lewandowski', 'Poland', 160, 29, 'Centre-Forward', 'SOLID Warszawa' , 100.0),
( 'Adam', 'Peszkin', 'Poland', 185, 32, 'Left Wing', 'KISS Gdynia' , 2.3),
( 'Olaf', 'Majka', 'Poland', 210, 17, 'Keeper', 'YAGNI Gliwice' ,32.2)
GO

W powyższym wpisie pomijam kwestie instalacji narzędzi Docker i Docker Compose. Osoby zainteresowane tematem uruchomienia kontenera SQL Server 2017 w 3 krokach, zapraszam do wcześniejszego mojego wpisu Praca z SQL Server w Visual Studio Code.

Wszystko zostało elegancko zakodowane, sprawdzone i kontener uruchomił się prawidłowo. Tym samym czas dorzucić zmiany do repozytorium Git. Wykonujemy polecenie git add w celu dodania zmian z working copy do indexu. W oknie Git Bash wyświetliły się poniższe ostrzeżenia, ale jako zawodowi programiści nie będziemy się tym przejmować. Czy my możemy popełnić błąd obsługując gita 🙂 W kolejnych krokach wykonujemy git pull, checkout, rebase, merge i push. Praca zakończona, czas na zasłużony odpoczynek.

git add scripts (LF will be replaced by CRLF)

Na kolejny dzień wracamy do naszego repo, przełączamy się na branch develop i próbujemy zbudować kontener wykonując w głównym folderze repozytorium komendę docker-compose up -d.

invalid time interval (LF will be replaced by CRLF)

Po podłączeniu się do SQL Server nie widać bazy Mundial, ale jak to? Zobaczmy co tam Git Bash pokazał, kontener utworzony (done), status kontenera Up, czyli wszystko prawidłowo. Podejrzyjmy zatem logi dla kontenera mssqlServer. Co tu się dzieje mamy komunikaty „not found”, „invalid time interval” oraz „invalid filename”. Ale jak to przecież wczoraj wszystko działało. W historii gita nie ma od tego momentu żadnych dodatkowych zmian. Nie trać czasu, restart nie pomoże na taką magię.

Rozwiązanie

Zanim zaprezentuję możliwe rozwiązania, zdefiniujemy przyczynę. Po tytule wpisu można domyślić się, że chodziło o konwersje plików dokonanych przez gita. Skrypty wyklepałem w nano, gdzie znak końca linii był LF tak jak w Linuxie. Nasz kontener korzysta z obrazu zawierającego SQL Server 2017 na Ubuntu.

docker container ubuntu

Teraz przypomnijmy sobie ostrzeżenia wyświetlone przez system kontroli wersji. Zgodnie z ich treścią dla skryptów znak końca bieżącej linii LF zostanie zastąpiony przez dwa znaki CRLF reprezentujące koniec linii w plikach np. w systemie Windows w trakcie operacji git checkout. W sytuacji uruchomienia skrypt createDB.sh pod Ubuntu został źle zinterpretowany ze względu na dwa znaki CRLF. W tym przypadku oczekiwany był znaku końca linii LF używany np. w systemie Ubuntu. Przyczyna zdiagnozowana, ale jak to teraz naprawić.

Globalne ustawienia dla końca linii

Pierwszym sposobem jest normalizacja zakończenia linii na poziomie systemowym. W tym celu należy użyć polecenia git config –global core.autocrlf , które zmieni sposób obsługi zakończenia linii w git.

git config --global core.autocrlf input
git config --global core.autocrlf true
git config --global core.autocrlf false

Zgodnie z powyższymi poleceniami dla core.autocrlf możemy ustawić trzy wartości:

  • true – powoduje zmianę CRLF na LF podczas commita do repozytorium. W przypadku operacji pobrania kodu z repozytorium lub wykonania instrukcji git checkout zostanie dokonana zmiana LF na CRLF. W repozytorium znaki końca linii przechowywane są jako LF.
  • false – nie powoduje zmiany CRLF na LF i odwrotnie. Znaki LF i CRLF będą zapisywane do repozytorium Git.
  • input – powoduje zmianę CRLF na LF podczas commita do repo. W przypadku operacji pobrania kodu z repozytorium lub wykonania instrukcji git checkout nie jest wykonywana konwersja. W repozytorium znaki końca linii przechowywane są jako LF.

Pierwsza wartość true zalecana dla systemu Windows nie sprawdzi się w moim aktualnym przypadku. Wartość input rekomendowana dla systemów Linux/Mac zdałaby tutaj rozwiązanie, ale w przypadku innych projektów na komputerze mogła być problematyczna. Ostatnią wartość false odrzucam, ze względu na możliwy bałagan z CRLF i LF w repozytorium. W takim przypadku najlepszą opcją będzie zarządzanie końcem linii na poziomie repozytorium.

Zarządzanie końcem linii na poziomie repozytorium

W powyższym podejściu zarządzamy końcem linii, konfigurując plik .gitattributes dla konkretnego repozytorium. Konfiguracja w pliku .gitattributes zastępuje globalną konfiguracje dla wskazanego repozytorium. Plik .gitattributes tworzymy w głównym folderze repozytorium. Konfiguracja w pliku .gitattributes polega na powiązaniu wzorca (np. plików o wskazanym rozszerzeniu) z atrybutami (np. konfiguracją końca linii). Dodajmy plik .gitattributes o poniższej zawartości do naszego repozytorium. Oczekujemy, że w naszym lokalnym folderze dla plików o rozszerzeniu .sh i .sql zostanie ustawiony znak końca linii LF.

* text=auto

*.sh text eol=lf
*.sql text eol=lf

Teraz dodamy znormalizowane pliki, poprzez wykonanie poniższego polecenia.

git add --renormalize . 

Powyższe polecenie nie spowoduje aktualizacji znaku/znaków linii zakończenia w plikach z working copy. W ramach aktualizacji w working copy należy wykonać poniższe instrukcje. Tutaj trzeba mieć na uwadze usunięcie plików nieśledzonych przez gita.

git rm --cached -r .
git reset --hard

W programie Notepad ++ możemy podejrzeć czy dla pliku createDB.sh znak końca linii to LF. Sprawdzimy teraz, czy uda się postawić bazę danych ze skryptu. Wykonujemy ponownie komendę docker-compose up -d w głównym folderze repozytorium.

LINQPad 5 connected SQL Server

W ostatnim kroku wykorzystując LINQPad 5 połączyłem się z SQL Server i pobrałem zawartość tabeli Players z bazy Mundial. Po zakończeniu procesu normalizacji plików ponownie udało się prawidłowo utworzyć kontener SQL Server w narzędziu Docker.