vyvchy
    Теми розділу

    09 · Git і CI/CD

    Системи контролю версій і модель Git

    Зміст

    Уяви теку з проєктом, де тести лежать поряд у файлах report_final.spec.ts, report_final_v2.spec.ts, report_final_РЕАЛЬНО_final.spec.ts. Хто останній їх чіпав? Що саме змінилося між другою й третьою версією? Як повернути стан «до того, як ми все зламали»? Без відповіді на ці питання команда з двох людей уже сповзає в хаос, а команда з десяти — просто не працює. Система контролю версій (version control system, VCS) — це інструмент, який відповідає на всі три питання автоматично: хто, коли, що і навіщо змінив, і як відкотитися.

    Для AQA це не «інструмент розробників, який мене не стосується». Твої автотести живуть у git-репозиторії — часто в тому самому, що й код застосунку. Ти щодня робиш коміти, розводиш гілки, розв'язуєш конфлікти, читаєш git blame, щоб зрозуміти, хто зламав локатор. CI-пайплайн запускається на кожен push. І на співбесіді на AQA питання про Git — майже гарантовані, бо без нього автоматизація не існує. Ця глава — модель світу: не команди (їх розберемо в главі «Базовий цикл Git»), а те, як Git думає. Зрозумієш модель — команди стануть очевидними; завчиш команди без моделі — зламаєшся на першому ж нестандартному випадку.

    Що таке контроль версій і навіщо він QA

    Контроль версій — це система, яка запам'ятовує стан набору файлів у часі й дозволяє повернутися до будь-якого збереженого стану. Кожен зафіксований стан називають комітом (commit) — це знімок проєкту плюс метадані: автор, дата, повідомлення й посилання на попередній коміт. Ланцюжок комітів утворює історію (history).

    Що це дає на практиці:

    • Історія й відповідальність. Видно, хто, коли й навіщо змінив кожен рядок. Тест почав падати після вчорашнього мержу — за хвилину знаходиш винний коміт.
    • Відкат. Зламали — повертаєшся до робочого стану без «пам'ятаєш, як воно виглядало?».
    • Паралельна робота. Кілька людей правлять один проєкт одночасно, кожен у своїй гілці (branch), а система зводить зміни докупи, а не затирає чужу роботу.
    • Експерименти без страху. Спробувати ризиковану переробку в окремій гілці й викинути її, якщо не спрацювало, — дешево.

    Для тестувальника додається свій кут: коміт — це одиниця, яку перевіряє CI; гілка — це те, що ревʼюять перед мержем; історія — це доказова база при розслідуванні регресій. У контексті процесів QA контроль версій — частина керування конфігурацією (configuration management): кожен тестовий артефакт має бути ідентифікований і версійований, щоб результат прогону можна було звʼязати з конкретною версією і коду, і тестів.

    Централізовані проти розподілених

    VCS історично бувають двох архітектур, і різницю між ними питають чи не найчастіше.

    Централізована (centralized VCS, CVCS) — Subversion (SVN), CVS, Perforce. Є один центральний сервер, який тримає всю історію. Розробники беруть із нього робочу копію (working copy) — зазвичай лише останній стан файлів, без історії. Щоб зробити коміт, подивитися лог чи створити гілку, потрібне зʼєднання із сервером. Сервер — єдина точка правди й водночас єдина точка відмови: він ліг — команда не комітить; диск на ньому вмер без бекапу — історія втрачена назавжди.

    Розподілена (distributed VCS, DVCS) — Git, Mercurial. Тут немає «головного» й «підлеглих» копій на рівні технології. Коли ти робиш git clone, ти отримуєш повну копію репозиторію разом з усією історією. Комітити, дивитися лог, розводити й зливати гілки, порівнювати версії можна повністю офлайн — усе це локальні операції з даними на твоєму диску. Сервер (його називають remote) потрібен лише для обміну змінами з іншими, і він — питання домовленості, а не архітектури: будь-який клон може стати новим центром.

    Розподілена (Git)

    Remote: копія для обміну

    Розробник A: повний репозиторій + історія

    Розробник B: повний репозиторій + історія

    Централізована (SVN, CVS)

    Сервер: вся історія

    Розробник A: робоча копія

    Розробник B: робоча копія

    Розподілена (Git)

    Remote: копія для обміну

    Розробник A: повний репозиторій + історія

    Розробник B: повний репозиторій + історія

    Централізована (SVN, CVS)

    Сервер: вся історія

    Розробник A: робоча копія

    Розробник B: робоча копія

    АспектЦентралізованаРозподілена (Git)
    Що є в тебе локальноРобоча копія (часто лише остання версія)Повний репозиторій з усією історією
    Коміт, лог, гілкиПотрібен серверЛокально, офлайн
    Точка відмовиСервер — критичнаБудь-який клон містить усе
    Швидкість операцій з історієюОбмежена мережеюМиттєва (локальний диск)
    ПрикладиSVN, CVS, PerforceGit, Mercurial

    Практичний наслідок для AQA: Git-операції над історією миттєві, бо не ходять у мережу. Коли git bisect за секунди перебирає сотні комітів у пошуках того, що зламав тест, — це працює саме тому, що вся історія лежить локально.

    Git думає знімками, а не діфами

    Це головна відмінність Git на рівні внутрішньої моделі, і саме вона відрізняє того, хто «вивчив команди», від того, хто розуміє інструмент.

    Більшість старих VCS зберігають дані як набір змін (діфів, deltas): базова версія файлу плюс список правок поверх неї. Щоб отримати актуальний стан, система бере базу й накочує всі діфи один за одним.

    Git влаштований інакше. Він думає про дані як про послідовність знімків (snapshots). При кожному коміті Git фактично фотографує, як виглядають усі файли в цей момент, і зберігає посилання на цей знімок. Це не означає, що Git дублює кожен файл у кожному коміті: якщо файл не змінився, Git не зберігає його копію, а кладе посилання на вже наявний ідентичний обʼєкт. Тобто знімок — це не «купа копій», а набір посилань на вміст, і незмінене перевикористовується.

    Технічно вміст файлу зберігається як обʼєкт, адресований хешем свого вмісту (за замовчуванням SHA-1). Однаковий вміст — однаковий хеш — зберігається один раз. Через це Git ловить будь-яке пошкодження: якщо байт у файлі змінився, а хеш ні — цілісність порушена, і Git це побачить.

    Чому це важить на практиці:

    • Коміт — це повний стан, а не «різниця». Тому переключитися на будь-який коміт — швидко: Git не «відкочує діфи», а розкладає готовий знімок.
    • Гілки дешеві. Гілка — це просто вказівник на коміт (детально — у главі «Гілки та злиття»). Створення гілки не копіює файли, тому їх заводять щедро.
    • Дифи — це подання, а не сховище. Коли ти дивишся git diff, Git обчислює різницю між двома знімками на льоту для твоєї зручності; зберігає він знімки.

    Три зони: робоча директорія, staging, репозиторій

    Найважливіша щоденна модель Git — це три зони, через які проходить кожна зміна. Плутанина тут — джерело половини «магічних» проблем новачка.

    1. Робоча директорія (working directory / working tree) — звичайні файли на диску, які ти відкриваєш у редакторі й правиш. Це єдина зона, яку видно у файловому менеджері.
    2. Staging area (індекс, index / staging area) — проміжна зона, «чернетка наступного коміту». Сюди ти явно складаєш ті зміни, які хочеш зафіксувати. Командою git add файл (точніше, його поточний стан) переноситься з робочої директорії в staging.
    3. Локальний репозиторій (.git) — прихована тека .git у корені проєкту, де лежить уся історія комітів. Командою git commit вміст staging перетворюється на новий постійний коміт.

    git add

    git commit

    git push

    git fetch / pull

    Робоча директорія
    файли, які ти редагуєш

    Staging / index
    чернетка коміту

    Локальний репозиторій
    .git, історія комітів

    Віддалений репозиторій
    GitHub / GitLab

    git add

    git commit

    git push

    git fetch / pull

    Робоча директорія
    файли, які ти редагуєш

    Staging / index
    чернетка коміту

    Локальний репозиторій
    .git, історія комітів

    Віддалений репозиторій
    GitHub / GitLab

    Навіщо взагалі окрема зона staging — адже можна було б комітити прямо з робочої директорії? Бо вона дає контроль над тим, що саме увійде в коміт. Ти правив три файли, але логічно завершена лише одна зміна — складаєш у staging тільки її й комітиш окремо, а решту лишаєш у роботі. Це основа охайної, читабельної історії, де кожен коміт — одна осмислена зміна, а не «зберіг усе, що назбиралось».

    Відповідно, файл у Git буває в одному з трьох станів: змінений (modified) — правлений у робочій директорії, але ще не в staging; підготовлений (staged) — доданий у staging під наступний коміт; зафіксований (committed) — уже в історії. Команда git status показує, який файл у якому стані, і читати її треба саме через цю модель. Повернути файл між зонами (прибрати зі staging, відкинути правки в робочій директорії) — тема глави «Скасування змін».

    Локальний і віддалений: Git ≠ GitHub

    Тепер розширимо модель на командну роботу — і одразу знімемо найпоширенішу плутанину.

    Git — це інструмент: консольна програма контролю версій, яку створив Лінус Торвальдс у 2005 році для розробки ядра Linux. Git працює повністю на твоїй машині й нікому нічого не винен: можна вести репозиторій роками, жодного разу не виходячи в інтернет.

    GitHub, GitLab, Bitbucket — це сервіси-хостинги, побудовані навколо Git. Вони тримають віддалену копію репозиторію й додають зверху те, чого в самому Git немає: вебінтерфейс, pull request-и й код-рев'ю, трекер задач, керування доступами, вбудований CI/CD. Git може існувати без них (локально або на власному сервері через SSH); вони без Git — безглузді.

    Аналогія: Git — це двигун, GitHub — одна з машин із цим двигуном. Можна знати двигун і не знати конкретну машину; знати машину без двигуна — не можна.

    Звідси — робота з віддаленим репозиторієм. Твій локальний репозиторій (.git) — повноцінний і самодостатній. Remote — це іменоване посилання на віддалену копію (за замовчуванням його називають origin). Обмін між ними — окремі, свідомі дії: git push відправляє твої локальні коміти на remote, git pull забирає чужі. Ключовий наслідок, який ловить кожного новачка: git commit нічого нікуди не відправляє. Коміт живе тільки на твоєму диску, поки ти не зробиш push. «Я ж закомітив» і «мої зміни на GitHub» — це два різні факти. Механіку push/pull/fetch детально розбираємо в главі «Базовий цикл Git», а pull request-и й стратегії гілкування — у главі «Pull Request».

    Для AQA ця межа принципова ще й тому, що CI/CD живе на боці сервісу: пайплайн запускає твої тести у відповідь на подію (push, відкриття PR) на GitHub чи GitLab — на події самого Git він не реагує, бо про твої локальні коміти ще нічого не знає.

    .gitignore: що не потрапляє в історію

    Не все, що лежить у теці проєкту, має бути в репозиторії. Згенеровані артефакти, залежності, локальні налаштування, а тим паче секрети — усе це або роздуває історію, або взагалі небезпечне. Щоб Git їх ігнорував, у проєкт кладуть файл .gitignore зі списком шаблонів.

    Типовий .gitignore для проєкту автотестів:

    # залежності — ставляться з package-lock.json, у репо не потрібні
    node_modules/
    
    # артефакти прогонів
    test-results/
    playwright-report/
    allure-results/
    
    # секрети та локальна конфігурація
    .env
    .env.local
    
    # службове ОС і редактора
    .DS_Store
    .idea/

    Найважливіша й найчастіше незрозуміла деталь: .gitignore діє лише на файли, які Git ще не відстежує (untracked). Якщо файл уже потрапив у історію (його колись закомітили), додавання його в .gitignore нічого не змінить — Git продовжить відстежувати зміни в ньому. Класична пастка: розробник комітить .env із паролем, аж потім згадує про .gitignore — а файл уже в історії. Прибрати з відстеження (лишивши на диску) треба явно:

    git rm --cached .env

    І окреме застереження, критичне для QA: якщо секрет уже опинився в історії комітів, самого git rm замало — він лишається в старих комітах, і будь-хто з доступом до репозиторію його дістане. Секрет треба вважати скомпрометованим і ротувати (згенерувати новий, старий відкликати). Гігієна секретів у репозиторії й пайплайні — окрема тема (розділ про секрети та конфігурацію в CI), але правило номер один просте: паролі, токени й ключі в git-історії не місце.

    git config: хто ти для Git

    Git потребує мінімального налаштування, і одне з них — обовʼязкове. Кожен коміт підписаний іменем і поштою автора, тож перш ніж комітити, їх треба задати:

    git config --global user.name "Olena Tester"
    git config --global user.email "olena@example.com"

    Конфігурація існує на трьох рівнях, і це варто розуміти, бо на співбесіді про це питають:

    • system (--system) — для всіх користувачів машини (файл рівня системи);
    • global (--global) — для твого користувача, усі твої репозиторії (файл ~/.gitconfig);
    • local (--local) — лише поточний репозиторій (файл .git/config).

    Правило пріоритету: чим вужчий рівень, тим сильніший. Local перекриває global, global перекриває system. Це зручно, коли, наприклад, у робочих проєктах треба комітити з корпоративною поштою, а в пет-проєктах — з особистою: ставиш різний user.email локально в конкретному репозиторії.

    Тут — ще одна пастка, яку плутають навіть досвідчені. user.email у git config — це не логін і не автентифікація. Це просто підпис, який Git вшиває в коміт як текст; поставити туди можна будь-що. А от право push-ити у віддалений репозиторій сервіс перевіряє окремо — через SSH-ключ або токен доступу. Тому «я поставив правильний email, а push відхиляють» — нормальна ситуація: email і доступ — це різні речі.

    Ще одне налаштування, яке реально псує життя тестам, — закінчення рядків (line endings). Windows традиційно завершує рядок парою символів CRLF, Unix і macOS — одним LF. Параметр core.autocrlf керує тим, як Git конвертує їх при коміті й вивантаженні. Розсинхрон тут дає «фантомні» зміни (весь файл раптом позначений як змінений, хоча в ньому «нічого не чіпали») і крихкі порівняння в тестах, чутливих до вмісту файлів. Для крос-платформних команд це узгоджують один раз на весь проєкт (часто через файл .gitattributes), щоб історія не залежала від того, хто на якій ОС працює.

    Типові помилки

    • «Я закомітив — значить, зміни в команди / на GitHub». Насправді commit — локальна дія; поки немає push, зміни бачиш лише ти. Колеги нічого не побачать, і CI не запуститься.
    • «git add зберігає зміни». Насправді add лише кладе поточний стан у staging — чернетку наступного коміту. Зберігає в історію тільки commit. Забув add після правки — закомітиш стару версію файлу.
    • «Git зберігає різницю між версіями». Насправді концептуальна модель Git — знімки цілих станів проєкту з дедуплікацією незмінених файлів, а не ланцюжок діфів. Дифи він обчислює на льоту для показу.
    • «Клон — це як робоча копія в SVN, лише останні файли». Насправді git clone тягне повний репозиторій з усією історією; майже всі операції потім локальні й офлайн.
    • «Додав файл у .gitignore — і його секрет зник». Насправді .gitignore діє лише на untracked-файли; уже закомічений файл лишається в історії, доки його не прибрати явно (git rm --cached), а секрет із минулих комітів треба ротувати.
    • «user.email у config — це мій логін до GitHub». Насправді це лише текстовий підпис автора в коміті; автентифікацію сервіс робить через SSH-ключ або токен, і це різні механізми.
    • «Git і GitHub — одне й те саме». Насправді Git — інструмент контролю версій, GitHub — один із хостинг-сервісів навколо нього; Git працює й без жодного з них.

    Підсумок

    • Git розподілений: кожен клон містить повну історію, і майже всі операції (коміт, лог, гілки, дифи) — локальні й миттєві, бо не ходять у мережу.
    • Git думає знімками цілих станів, а не діфами; незмінений вміст зберігається один раз і адресується хешем.
    • Кожна зміна проходить три зони: робоча директорія → (git add) → staging → (git commit) → локальний репозиторій. add і commit — різні кроки, і staging дає контроль над тим, що саме увійде в коміт.
    • Коміт живе локально, поки не push; Git ≠ GitHub — інструмент і сервіс-хостинг навколо нього — це різні речі.
    • .gitignore діє лише на untracked-файли, а user.name/user.email у config — це підпис автора, а не автентифікація.

    Що питають на співбесіді

    • «Чим розподілена система контролю версій відрізняється від централізованої?» Інтервʼюер перевіряє, чи розумієш ти головне: у Git кожен клон — повна копія з історією, робота офлайн і відсутність єдиної критичної точки. Слабка відповідь зводить усе до «Git новіший»; сильна — називає локальність операцій і наслідки (швидкість, надійність).
    • «Що таке staging area і навіщо вона потрібна?» Дивляться, чи не плутаєш ти add і commit. Хороша відповідь: staging — чернетка наступного коміту, вона дає зібрати в один коміт саме потрібні зміни, а не все підряд, і тримати історію охайною.
    • «Git — це те саме, що GitHub?» Питання-фільтр на базове розуміння. Правильно: Git — інструмент контролю версій, GitHub/GitLab/Bitbucket — сервіси-хостинги з вебінтерфейсом, PR, задачами й CI поверх Git; Git працює й без них.
    • «Git зберігає діфи чи знімки?» Перевіряють глибину моделі. Знімки цілих станів з дедуплікацією незмінених файлів; дифи — обчислюване подання.
    • «Випадково закомітив файл із паролем — що робиш?» Тут інтервʼюер дивиться на інстинкт безпеки. Ключове: .gitignore заднім числом не рятує, файл лишається в історії; секрет треба вважати скомпрометованим і ротувати, а не просто видалити наступним комітом.

    Джерела