← До списку уроків
Заняття 03 з 13Git: код бота під контролем версій
курс 0% виконано0%
Заняття 03 · місяць 1

Git: код бота під контролем версій

90 хвилин

Навчитися зберігати кожну зміну бота в історії Git, за хвилину повертати зламаного бота до робочого стану, пробувати ризиковані ідеї в окремій гілці й тримати токен поза репозиторієм (так у git називають теку проєкту з історією змін)

Мій сайт
http://91.219.61.4/u/<логін>/
Мій кабінет
http://91.219.61.4/k/<логін>/
Вхід на сервер
ssh <логін>@91.219.61.4

Замість <логін> підставляєш свій логін виду bohdan.g9: ім’я, крапка, номер групи. Кутові дужки не пишуться. Кабінет — сторінка, де проти кожного критерію стоїть відмітка, зарахований він чи ні. У прикладах на цій сторінці скрізь написано bohdan.g9 — читай це як свій логін.

Що ти зробиш сьогодні

На занятті 02 твій бот ожив: він відповідає на /start і /help і працює на сервері, навіть коли твій комп’ютер вимкнено. Сьогодні ти почнеш його змінювати — і навчишся робити це так, щоб жодна зміна не могла зламати бота назавжди. Для цього існує Git — програма, яка пам’ятає кожен збережений стан коду. Збережений стан називається коміт; що це таке, докладно розібрано в теорії.

До кінця заняття бот отримає нову команду /fact і нове привітання, а в історії з’явиться щонайменше п’ять твоїх комітів зі зрозумілими повідомленнями. Ти двічі навмисно зламаєш бота — і двічі повернеш його до робочого стану: один раз — поки зміну ще не збережено комітом, другий — коли її вже збережено. Ризикований експеримент зробиш в окремій гілці proba (так називають окрему лінію змін, яка не чіпає робочого бота), а всю історію відправиш у дві запасні копії: ~/repo.git на сервері й GitHub.

Що можна буде показати: у Telegram — відповідь на /fact і нове привітання; у терміналі — git log --oneline зі списком твоїх комітів; на GitHub — ті самі коміти й гілку proba. І ще одне, що теж перевіряється: токен бота за весь цей час жодного разу не потрапить у Git.

Теорія

1. Навіщо боту історія змін і де вона живе

Ситуація, яка трапляється з кожним. Учора бот відповідав. Сьогодні ти додав нову команду, переписав привітання й поправив підказку /help — три правки в різних місцях файлу bot.py. Перезапустив службу, а бот мовчить. Яка з трьох правок його зламала? Яким був файл учора? Пам’ять на це не відповість.

Звичний спосіб захиститися — копії: bot2.py, bot_stare.py, bot_tochno_robochyi.py. За тиждень із них уже нічого не зрозуміло: яка з копій робоча, чим вони відрізняються, коли з’явилася помилка.

Git — програма, яка зберігає історію змін у теці проєкту. Вона запам’ятовує, які рядки в яких файлах змінилися, коли, хто це зробив і навіщо (це «навіщо» пишеш ти сам), і вміє повернути будь-який збережений стан. Два слова, які будуть далі в кожному розділі:

Тека ~/app у тебе вже є репозиторієм: git там увімкнули, коли створювали акаунт, і зробили перший коміт «Заготовка проєкту». На занятті 02 ти додав туди бота й відправив роботу на GitHub.

Історія твого бота живе в трьох місцях — і це навмисно:

Інші репозиторії, з якими твій обмінюється комітами, називаються віддаленими (англійською remote, вимовляється «рімоут»). У кожного з них у git є коротке ім’я: ~/repo.git зветься origin, GitHub — github. Надсилає коміти команда git push:

git push origin master

Читається так: «надішли в origin коміти гілки master». Надсилаються лише ті коміти, яких там ще немає. І важливе: push — це разова дія, а не постійний зв’язок. Що ти зберіг комітом після push, у копії не з’явиться, доки не виконаєш push знову.

Ігор виконав git push origin master, а після цього зробив ще два коміти з новою командою /fact. Наступного дня він випадково видалив теку ~/app цілком. Що можна відновити з ~/repo.git?

2. Коміт, індекс і три способи подивитися на зміни

Коміт — збережений стан усіх файлів проєкту, за якими стежить git, на той момент, коли ти виконав команду git commit. У кожному коміті записано вміст файлів, дату й час, ім’я автора, твоє повідомлення про те, що змінилося, і хеш.

Хеш — рядок із 40 літер і цифр, який git обчислює з умісту коміту, наприклад 58d966e1c530828b4f83a026f418afb88d707fe5. Це ідентифікатор коміту: за ним коміт знаходять і на нього посилаються. Майже скрізь git друкує лише перші сім символів — 58d966e, і цього досить, щоб не сплутати один коміт з іншим.

Кожен коміт пам’ятає попередній, тому коміти вишиковуються в історію. Її видно командою git log --oneline — по рядку на коміт:

94fe014 переписав привітання /start: бот пояснює, навіщо він
3c1f8a2 переписав README: для кого бот і як його запустити
58d966e додав команду /fact: бот розповідає цікавий факт
7eb7b1f Заняття 02: бот відповідає на /start і /help
c255902 Заготовка проєкту

Найновіший коміт — угорі. На нього вказує позначка HEAD — «ти зараз тут».

Між зміненим файлом і комітом є проміжна зупинка — індекс (англійською staging area, «місце підготовки»). Це список змін, які підуть у наступний коміт. Команда git add кладе зміну в індекс, команда git commit записує в історію все, що в ньому зібралося:

робоча копія  ──git add──▶  індекс  ──git commit──▶  історія

Навіщо ця зупинка. За вечір ти переписав привітання в bot.py і заодно доповнив README.md. Це дві різні справи, і в історії їм краще бути двома комітами: git add bot.py — коміт, потім git add README.md — ще один коміт. Через місяць ти знайдеш потрібну зміну за повідомленням, а не перебиратимеш змішане докупи.

Одна деталь, на якій часто помиляються: у коміт іде та версія файлу, яка була в момент git add. Якщо після git add ти ще раз змінив файл, нові правки лишаються поза комітом, доки не виконаєш git add знову.

Подивитися на зміни можна трьома командами. Запам’ятай, яка на яке питання відповідає:

Ось шматок виводу git diff, у якому видно додану команду /fact:

@@ -59,6 +60,8 @@ def handle_update(update):
                 "Напиши /help.")
     if text.startswith("/help"):
         return HELP
+    if text.startswith("/fact"):
+        return "Цікавий факт: на Венері доба довша за рік."
     if text.startswith("/"):
         return "Не знаю такої команди. Напиши /help."
     return f"Ти написав: {text}"

Рядок із @@ каже, де у файлі зміна: близько 60-го рядка, у функції handle_update. Цифри в цьому рядку можна не розбирати. Дивись на самі рядки: без знака — сусіди, щоб було легше зорієнтуватися, рядки з плюсом — нові. Якщо рядок змінено, diff показує його двічі: старий з мінусом, новий з плюсом.

Екран «завис» з двокрапкою внизу Довгий вивід git diff, git log чи git show git показує посторінково, як книжку: унизу екрана з’являється : або (END). Пробіл — наступна сторінка, q — вийти. Це не зависання.

Оля виконала git add bot.py. Потім помітила одруківку в тексті /help, виправила її в тому самому файлі, зберегла й одразу виконала git commit -m "додала команду /fact". Що потрапило в коміт?

3. Повідомлення коміту: для людини через місяць

Самому git байдуже, що написано в повідомленні. Воно для людини, яка читатиме історію, — найчастіше для тебе ж через місяць, коли бот почне поводитися дивно й треба буде знайти, звідки це взялося. Хороше повідомлення одним рядком відповідає на питання «що змінилося в боті?».

Так не пишутьЧомуТак пишуть
fixНе сказано, що зламалося і девиправив /help: бот відповідав «Не знаю такої команди»
зміниКоміт і так складається зі зміндодав команду /fact: бот розповідає цікавий факт
update bot.pyНазву файлу git покаже й сампереписав привітання /start своїми словами
123Через тиждень не означатиме нічогододав у README, як перевірити бота без Telegram
робота у вівторокДату й час git записує сампроба: бот рахує слова й символи в повідомленні

Правила прості: один рядок, до 70 символів; починай з дієслова — «додав», «виправив», «переписав», «прибрав»; пиши, що змінилося для бота, а не який файл ти відкривав. Якщо в повідомленні хочеться написати «і», найчастіше це дві різні зміни — і комітів має бути два.

Автоперевірка не рахує повідомлень, коротших за десять символів, і слів-заглушок на кшталт fix, update, wip, test. А викладач на розборі відкриває твій git log, вибирає навмання коміт і просить пояснити, що в ньому змінилося. Якщо за повідомленням ти сам не можеш цього сказати — повідомлення погане.

Повідомлення — одразу в команді Пиши його так: git commit -m "…". Без -m git відкриває текстовий редактор, щоб ти набрав повідомлення там, і на сервері це може виявитися незнайомий тобі редактор vi. Як із нього вийти — у розділі «Якщо щось не працює».

Як користуватися ШІ-помічником. ШІ добре пояснює повідомлення git: встав у чат текст помилки (лише його — без токена й без вмісту .env) і спитай: «Що це означає і що мені зробити? Я вчуся перший місяць». Але є два правила. Перше: повідомлення комітів пиши сам. ШІ не знає, що й навіщо ти змінив, а пояснювати на розборі доведеться тобі. Друге: ШІ інколи радить команди, які стирають історію або чужу роботу, — git push --force, git reset --hard, rm -rf .git. Не виконуй команду, яку не можеш пояснити. Спершу спитай: «Що саме зробить ця команда і що я можу втратити?»

І ніколи не вставляй у чат токен бота чи вміст .env: усе, що ти туди надіслав, зберігається на чужому сервері, і секретом це вже не є.

4. Відкат: повернути бота до робочого стану

«Відкотити» означає різне — залежно від того, що сталося. Ось чотири ситуації й команда для кожної.

Перша: змінив файл, ще не комітив, і бот зламався.

git restore bot.py

Файл повертається до стану з останнього коміту. Усі правки, які ти ще не зберіг комітом, у ньому зникають — без жодного питання.

Друга: не пам’ятаєш, що саме наробив. Спершу подивись, потім вирішуй:

git diff

Третя: поклав в індекс зайве — наприклад, git add прихопив не той файл. Прибрати його з індексу, не чіпаючи вмісту:

git restore --staged README.md

Четверта: коміт уже зроблено (можливо, навіть відправлено в ~/repo.git і на GitHub), а зміна виявилася поганою.

git revert --no-edit HEAD

git revert створює новий коміт, який робить протилежне до вказаного: що той додав — прибирає, що прибрав — повертає. HEAD — останній коміт; замість нього можна написати хеш будь-якого коміту з git log --oneline. Параметр --no-edit означає «візьми готове повідомлення Revert "…" і не відкривай редактора».

Чому не просто видалити поганий коміт з історії? Бо він уже може лежати в ~/repo.git і на GitHub, а видалити коміт тут — означає, що копії перестали б збігатися з тим, що в тебе: ми не можемо стерти його звідти. revert нічого не стирає: в історії видно і невдалу спробу, і її скасування. Тому він безпечний навіть після push.

Обережно з git restore Ця команда викидає незбережені зміни назавжди: git про них не знав, тож і повернути їх не зможе. Перед нею завжди дивись git diff — чи справді ти хочеш утратити те, що там написано.

Марта пів години правила bot.py і жодного разу не комітила. Тепер служба не запускається: у журналі SyntaxError. Останній коміт — учорашній, тоді бот працював. Яка команда поверне робочу версію?

5. Гілки: експеримент окремо від робочого бота

Гілка — окрема лінія комітів усередині того самого репозиторію. Основна гілка у твоєму репозиторії зветься master (у багатьох інших проєктах — main; це лише ім’я). У ній лежить бот, який працює. Як зветься твоя основна гілка, покаже команда git branch --show-current.

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

git switch -c proba         # створити гілку proba й перейти в неї
git switch master           # повернутися на основну гілку
git branch                  # список гілок, зірочка — поточна
git merge --no-edit proba   # перенести коміти з proba в поточну гілку

Перемикання гілки змінює файли в теці. Виконав git switch master — і bot.py знову такий, як у master. Це не втрата: коміти proba лежать на місці, повернешся в неї — повернеться і її версія файлів.

Звідси важливий наслідок для бота. Служба club-bot запускає run.py, а той підключає bot.py — це файли, які зараз лежать у ~/app, тобто код поточної гілки, і Python читає їх лише під час старту. Перемкнувся на proba й перезапустив службу — у Telegram відповідає експериментальний бот. Повернувся в master — перезапусти службу ще раз, інакше в її пам’яті так і лишиться експеримент.

Злиття (англійською merge) переносить коміти однієї гілки в іншу. Якщо в master після створення proba нових комітів не було, git просто пересуває master уперед, до останнього коміту proba, і пише Fast-forward — «перемотування вперед». Параметр --no-edit і тут рятує від редактора, якщо git захоче записати окремий коміт злиття.

Олег створив гілку proba, зробив у ній коміт «проба: бот відповідає великими літерами», потім виконав git switch master і python3 try_bot.py "привіт". Бот відповів звичайними літерами. Що сталося?

6. .gitignore і токен: що ніколи не кладуть у Git

Не все з теки ~/app має потрапляти в історію. Три групи файлів туди не кладуть:

.gitignore — файл зі списком того, що git навмисно не помічає. Кожен рядок у ньому — правило: ім’я файлу (.env), ім’я теки з косою рискою в кінці (__pycache__/) або шаблон із зірочкою (*.db означає «будь-яке ім’я, що закінчується на .db»). Рядки, що починаються з #, — коментарі для людини. Твій .gitignore прийшов із заготовкою, і всі ці правила в ньому вже є. Саме тому на занятті 02 git status не показав .env, а git add -A його не прихопив. Якщо спробувати додати .env навмисно, git відмовиться й порадить ключ -f («примусово»). Для секретів цей ключ заборонений.

Тепер найважливіше. Файл, який хоч раз потрапив у коміт, називається відстежуваним. Правила .gitignore діють лише на невідстежувані файли. Якщо .env уже потрапив у коміт, а правило дописали пізніше, git і далі стежитиме за файлом, а стара версія з токеном назавжди лишиться в тому коміті.

Чому такий токен вважають украденим, навіть якщо файл потім прибрали:

Хто має токен, той може від імені твого бота читати повідомлення, які йому пишуть однокласники, і відповідати їм що завгодно. Тому порядок дій один, і він не залежить від того, «чи хтось устиг побачити»:

  1. У Telegram відкрий @BotFather, надішли /revoke і вибери свого бота. BotFather видасть новий токен, а старий одразу перестане діяти.
  2. Впиши новий токен у ~/app/.env замість старого.
  3. Перезапусти службу: systemctl --user restart club-bot — тепер бот працює з новим токеном.
  4. Прибери .env з відстежуваних: git rm --cached .env і коміт. Файл лишиться на диску, а git перестане за ним стежити.

Набагато простіше не допускати цього взагалі: .gitignore пишуть до того, як з’являється секрет, а перед кожним комітом дивляться git status.

Андрій випадково додав .env у коміт з токеном бота й відправив коміт на GitHub. Наступного дня помітив, дописав .env у .gitignore і зробив новий коміт. Що тепер із токеном?

Слова цього уроку

СловоЩо означає
GitПрограма, яка зберігає історію змін у теці проєкту й уміє повернути будь-який збережений стан
репозиторійТека проєкту разом з усією історією; сама історія — у прихованій теці .git
робоча копіяФайли, які ти бачиш і редагуєш зараз
комітЗбережений стан файлів з автором, датою й повідомленням
хешІдентифікатор коміту з 40 символів; зазвичай показують перші сім
HEADПозначка «ти зараз тут» — останній коміт поточної гілки
індексСписок змін, підготовлених до наступного коміту; туди кладе git add
git statusЩо змінено і що вже лежить в індексі
git diffЯкі рядки змінено: з мінусом — як було, з плюсом — як стало
git logІсторія комітів; з --oneline — по рядку на коміт
git showОдин коміт разом з його змінами
git restoreПовертає файл до стану з останнього коміту; з --staged — прибирає зміну з індексу
git revertНовий коміт, який скасовує вказаний; історія при цьому не стирається
гілкаОкрема лінія комітів; основна в тебе — master
git switchПерейти на іншу гілку; з -c — створити гілку й перейти в неї
злиття, git mergeПеренесення комітів з іншої гілки в поточну
Fast-forwardЗлиття, коли гілку просто пересувають уперед, без окремого коміту
віддалений репозиторійІнший репозиторій, з яким обмінюються комітами: origin (~/repo.git) і github
bare-репозиторійРепозиторій без робочої копії, лише історія — як ~/repo.git
git pushНадіслати свої коміти у віддалений репозиторій
.gitignoreСписок того, що git навмисно не помічає
відстежуваний файлФайл, який уже був у коміті; правила .gitignore на нього не діють
git rm --cachedПерестати відстежувати файл, лишивши його на диску
токенПароль бота. Потрапив у коміт — вважай украденим
/revokeКоманда @BotFather: видає новий токен, а старий перестає діяти

Практика

Скрізь замість bohdan.g9 підставляй свій логін. Усі команди git виконуй у теці ~/app. Якщо в тебе немає Telegram, роби все те саме, а бота перевіряй командою python3 try_bot.py: вона викликає ту саму функцію handle_update, що й живий бот.

1 Зайти на сервер і переконатися, що бот живий

ssh bohdan.g9@91.219.61.4
cd ~/app
pwd
systemctl --user is-active club-bot
python3 try_bot.py "/start"

pwd має надрукувати /home/bohdan.g9/app — git працюватиме саме з цим репозиторієм. systemctl --user is-active club-bot відповідає active: служба з ботом працює. try_bot.py друкує привітання бота, наприклад Привіт, Тест! Я бот проєкту «…». Напиши /help. Ім’я «Тест» підставляє try_bot.py.

Якщо служба failed або try_bot.py друкує помилку, бот із заняття 02 зламаний, і спершу його треба полагодити: tail -n 30 ~/logs/club-bot.log покаже причину. Починати історію змін зі зламаного бота не варто.

2 Підписати свої коміти

Git записує в кожен коміт ім’я й пошту автора. Перевір, чи вони вже задані:

git config --global user.name
git config --global user.email

Якщо обидві команди щось надрукували — пропусти наступний блок. Якщо у відповідь порожньо, назви себе. Ім’я пиши без прізвища: історію бачить викладач, а на GitHub — кожен, кому ти покажеш репозиторій.

git config --global user.name "Богдан"
git config --global user.email "$(whoami)@club.local"

$(whoami) термінал замінить твоїм логіном, як на занятті 02. Пошту git нікуди не надсилає, це лише підпис; адреса club.local вигадана навмисно, а справжню пошту сюди не пиши — вона потрапила б у кожен коміт. --global означає «для всіх репозиторіїв твого акаунта», тому робиться це один раз. І ще одне налаштування — щоб git, коли йому знадобиться редактор, відкривав знайомий тобі nano:

git config --global core.editor nano
git config --global --list

Перша команда нічого не друкує — так і має бути. Друга показує все разом: рядки user.name=…, user.email=… і core.editor=nano.

3 Подивитися, що вже є в репозиторії

ls -a
git status
git log --oneline
git branch --show-current
git remote -v

Якщо git status показує змінені файли (modified), щось із заняття 02 лишилося без коміту. Подивись git diff і, якщо все гаразд, збережи комітом: git add -A, потім git commit -m "Заняття 02: бот відповідає на /start і /help".

4 Перевірити, що токен не потрапить у Git

cat .gitignore
git check-ignore -v .env __pycache__ app.db

У .gitignore є рядки .env, *.db, venv/ і __pycache__/. Друга команда для кожного імені друкує правило, яке його приховує:

.gitignore:12:.env	.env
.gitignore:4:__pycache__/	__pycache__
.gitignore:8:*.db	app.db

Читається так: файл із правилами, номер рядка в ньому, саме правило, а після проміжку — ім’я, яке воно приховує. Номери рядків у тебе можуть бути інші. Файлу app.db ще немає, але правило *.db уже на нього чекає.

Якщо для якогось імені рядка немає або в cat .gitignore бракує .env, *.db чи venv/ (так буває в тих, хто починав курс раніше й писав .gitignore сам), — зупинись і допиши відсутні правила в кінець файлу, кожне окремим рядком, наприклад echo "venv/" >> .gitignore (два знаки >> дописують рядок у кінець). Потім знову запусти git check-ignore -v і збережи комітом: git add .gitignore, git commit -m "доповнив .gitignore: .env, база й venv не потрапляють у Git". Автоперевірка L03.2 дивиться саме на ці три правила.

Тепер спробуй навмисно додати токен у Git. Це безпечно: git не дасть.

git add .env
The following paths are ignored by one of your .gitignore files:
.env
hint: Use -f if you really want to add them.

Git відмовився й нічого не додав (нижче може бути ще рядок-підказка, як вимкнути це повідомлення). Параметр -f з підказки для .env не використовуй ніколи. Ще дві перевірки:

git ls-files
git log --all --oneline -- .env

git ls-files друкує повний список файлів, за якими стежить git: bot.py, env.py, run.py, try_bot.py, .env.example, README.md та інші. Серед них не має бути .env і файлів .db. (.env.example — інший файл, токена в ньому немає, і в репозиторії йому саме місце: з нього видно, які налаштування потрібні боту.)

Друга команда шукає в усіх гілках коміти, у яких хоч раз був файл .env, і має не надрукувати нічого. Порожньо — токен ніколи не потрапляв у Git. Якщо з’явився хоч один рядок, переходь до розділу «Якщо щось не працює», останній пункт.

5 Додати боту команду /fact

nano bot.py

Знайди підказку HELP і допиши один рядок про нову команду — між рядком про /about і рядком про відлуння. Решту рядків не чіпай: кожен із них уже закінчується на \n (так у тексті підказки позначають перехід на новий рядок), тому змінюється лише один рядок:

HELP = ("Що я вмію:\n"
        "/start — привітання\n"
        "/help — ця підказка\n"
        "/about — хто мене зробив\n"
        "/fact — цікавий факт\n"
        "будь-який інший текст — повторю його")

Тепер у функції handle_update знайди перевірку /about (її ти додав на занятті 02) і одразу під її return встав два нові рядки — перед рядком if text.startswith("/"):. Факт вигадай свій, про тему твого проєкту; нижче приклад для бота про космос:

    if text.startswith("/about"):
        return f"Проєкт «{PRODUCT}», автор — {CFG['product']['author']}."
    if text.startswith("/fact"):
        return "Цікавий факт: на Венері доба довша за рік."
    if text.startswith("/"):
        return "Не знаю такої команди. Напиши /help."

Що тут відбувається. Кожна команда — це перевірка «якщо повідомлення починається з такого-то слова, поверни такий-то текст». Відступи — рівно 4 і 8 пробілів, як у сусідніх рядках, а текст відповіді — у лапках, які треба закрити.

Чому саме тут, а не в кінці функції? Python перевіряє умови згори вниз і виконує першу, яка підійшла. Рядок if text.startswith("/") ловить будь-яку команду, якої бот не знає. Якщо поставити /fact нижче за нього, до /fact черга не дійде ніколи.

Збережи (Ctrl+O, Enter) і вийди (Ctrl+X). Перевір бота й подивись, що змінилося:

python3 try_bot.py "/fact"
python3 try_bot.py "/help"
git diff

/fact відповідає твоїм фактом. /help показує шість рядків, серед них новий. git diff показує три рядки з плюсом і жодного з мінусом: один у HELP і два в handle_update. Старих рядків ти не міняв, тож їх diff не позначає.

Якщо замість відповіді бачиш SyntaxError, Python пише номер рядка й ставить знак ^ під місцем помилки: найчастіше там загублена лапка, дужка або двокрапка. Якщо два рядки підказки злилися в один (/about — хто мене зробив/fact — …), ти забув \n. Відповідь Не знаю такої команди на /fact означає, що команду вставлено нижче за перевірку /.

Усе працює — записуємо в історію й оновлюємо живого бота:

git add bot.py
git commit -m "додав команду /fact: бот розповідає цікавий факт про тему проєкту"
systemctl --user restart club-bot
systemctl --user is-active club-bot

git commit відповідає приблизно так: [master 58d966e] додав команду /fact: бот розповідає цікавий факт про тему проєкту і нижче 1 file changed, 3 insertions(+). Це гілка, короткий хеш (у тебе свій), твоє повідомлення і скільки рядків додано й прибрано. Служба — active. Напиши боту в Telegram /fact — він відповість так само, як try_bot.py.

6 Переписати README: що це за бот і як його запустити

README.md — головний опис проєкту. Його першим бачить кожен, хто відкриває твій репозиторій на GitHub. Зараз у ньому текст заготовки.

nano README.md

Зроби три речі:

  1. перший рядок # Мій сервіс заміни назвою свого бота;
  2. у розділі ## Що це прибери службовий текст між знаками підкреслення — він починається словами «Заняття 01: опиши одним абзацом» — і напиши свій абзац: для кого бот, що він уже вміє і як його знайти в Telegram;
  3. у розділі ## Як запустити допиши, що бот живе в службі club-bot, де лежить токен і як перевірити бота без Telegram. Команди, які там уже є, лиши.

Нижче зразок для чужого бота: він показує, з чого складаються розділи. У тебе інший бот і інший текст, тому пиши своє.

# Бот-помічник для походу

## Що це

Telegram-бот @bohdan_pohid_bot для однокласників, які вперше йдуть
у похід з ночівлею. Зараз він вітається (/start), показує свої
команди (/help), розповідає про проєкт (/about), ділиться
цікавим фактом (/fact) і повторює текст, який йому написали.

## Як запустити

Бот живе у службі club-bot на сервері (її запускає run.py), тому відповідає,
навіть коли мій комп'ютер вимкнено. Токен бота лежить у файлі .env,
якого в репозиторії немає навмисно: його роблять з .env.example
(cp .env.example .env && chmod 600 .env) і вписують токен від @BotFather.

    systemctl --user restart club-bot      # перезапуск після зміни коду
    tail -n 30 ~/logs/club-bot.log    # останні рядки журналу

Перевірити бота без Telegram:

    python3 try_bot.py "/start"

Заголовки лишай точно такими: два знаки решітки, пробіл, текст. Автоперевірка шукає саме ## Що це і ## Як запустити, а службового тексту заготовки там бути не має. Токена, паролів і свого прізвища в README не пиши: цей файл бачитимуть на GitHub. Збережи файл і збережи комітом:

git diff
git add README.md
git commit -m "переписав README: для кого бот і як його запустити"

git diff покаже рядки заготовки з мінусом і твій текст із плюсом. Коміт відповість рядком [master …] з твоїм повідомленням.

7 Дві зміни — два коміти

Вправа на індекс: ти зробиш дві різні зміни, а збережеш їх двома окремими комітами.

Перша — у bot.py. Перепиши привітання /start своїми словами: скажи, навіщо цей бот. {name} і {PRODUCT} лиши — це ім’я людини й назва проєкту. Наприклад:

    if text.startswith("/start"):
        return (f"Привіт, {name}! Я бот проєкту «{PRODUCT}». "
                "Я допомагаю зібратися в похід. Напиши /help.")

Друга — у README.md. Нижче, у розділі ## Структура, є таблиця файлів. Допиши рядки про файли бота одразу під її останнім рядком (з deploy.sh), без порожнього рядка між ними — інакше таблиця розірветься. Якщо такого розділу в тебе немає, допиши в кінець файлу рядок ## Структура, а під ним — ці рядки:

| `bot.py` | логіка бота: що він відповідає на кожне повідомлення |
| `env.py` | читає токен і налаштування з `.env` |
| `try_bot.py` | перевірка бота без Telegram |

Збережи обидва файли й подивись:

python3 try_bot.py "/start"
git status

Бот вітається по-новому. git status показує обидва файли в списку Changes not staged for commit. Покладемо в індекс лише один:

git add bot.py
git status
git diff --staged

Тепер bot.py стоїть угорі, під Changes to be committed, а README.md лишився нижче. git diff --staged показує тільки зміну привітання: саме вона піде в коміт. Записуємо одну зміну, потім другу:

git commit -m "переписав привітання /start: бот пояснює, навіщо він"
git add README.md
git commit -m "додав у README опис файлів бота"
git log --oneline

У git log --oneline зверху два нові рядки, кожен про своє. Перезапусти службу (systemctl --user restart club-bot) і напиши боту /start у Telegram.

8 Зламати бота й повернути, поки не зберіг комітом

Ламаємо навмисно — так, як ламається по-справжньому: одним пропущеним символом.

nano bot.py

Знайди рядок if text.startswith("/help"): і зітри двокрапку в кінці. Збережи й вийди. Перевір:

python3 try_bot.py "/help"

Замість відповіді — повідомлення про помилку. Його кінець такий:

  File "/home/bohdan.g9/app/bot.py", line 61
    if text.startswith("/help")
                               ^
SyntaxError: expected ':'

Python не зміг навіть прочитати файл: expected ':' — «очікував двокрапку» в рядку 61 (у тебе номер може бути інший). Тепер подивись, що стане з живим ботом:

systemctl --user restart club-bot
tail -n 15 ~/logs/club-bot.log

У журналі той самий SyntaxError (якщо його ще немає, зачекай кілька секунд і повтори другу команду). Служба падає ще під час запуску, а systemd за правилом Restart=always кожні три секунди пробує запустити її знову — і знову марно. Напиши боту в Telegram /help: він мовчить. Ось чим небезпечна одна загублена двокрапка. Повертаємо:

git status
git diff
git restore bot.py
git diff

git status показує modified: bot.py. Перший git diff — рядок із двокрапкою з мінусом і без неї з плюсом: оце й уся поломка. Після git restore другий git diff не друкує нічого: файл знову такий, як в останньому коміті. Перезапускаємо:

python3 try_bot.py "/help"
systemctl --user restart club-bot
systemctl --user is-active club-bot

Підказка знову на місці, служба active. А повідомлення, які ти писав зламаному ботові, за кілька секунд отримають відповідь: Telegram тримав їх, доки бот не повернувся. Запам’ятай порядок: журнал → git diff → git restore → перезапуск.

9 Поганий коміт і git revert

Буває гірше: зміну вже зберегли комітом, а потім з’ясувалося, що вона ламає бота. Відтворимо це. Уяви, що ти вирішив «навести порядок» і перенести перевірку невідомих команд на самий верх.

nano bot.py

У handle_update постав курсор на рядок if text.startswith("/"): і двічі поспіль натисни Ctrl+K — виріжуться він і рядок під ним (return "Не знаю такої команди…"). Стрілками підніми курсор на рядок if text.startswith("/start"):, на самий його початок (клавіша Home), і натисни Ctrl+U — вирізане вставиться над ним. Якщо курсор стоятиме посеред рядка, вставка розріже рядок навпіл. Збережи, вийди й збережи комітом, ніби все гаразд:

git diff
git add bot.py
git commit -m "перемістив перевірку невідомих команд вище"
python3 try_bot.py "/help"
python3 try_bot.py "/about"

git diff показав, що два рядки зникли внизу й з’явилися вгорі. А бот і на /help, і на /about відповідає Не знаю такої команди. Напиши /help. Усі команди починаються з /, тож перша ж перевірка тепер ловить їх усі. Коміт поганий — скасовуємо:

git log --oneline -3
git show HEAD
git revert --no-edit HEAD

git show HEAD показує останній коміт: автора, дату, повідомлення й ті самі переставлені рядки. git revert відповідає:

[master 654d9b6] Revert "перемістив перевірку невідомих команд вище"
 Date: …
 1 file changed, 2 insertions(+), 2 deletions(-)

У рядку Date: — дата й час нового коміту. Перевір, що бот знову розуміє команди, і подивись історію:

python3 try_bot.py "/help"
git log --oneline -3
systemctl --user restart club-bot

Підказка знову працює. У git log видно обидва коміти: поганий і його скасування. Нічого не стерто, і через місяць буде зрозуміло, що тут сталося. Службу між цими кроками ти не перезапускав, тож живий бот увесь час працював зі старим, правильним кодом; перезапуск наприкінці — щоб служба точно взяла поточну версію файлу.

10 Експеримент у гілці proba

git switch -c proba
git branch

Git відповідає Switched to a new branch 'proba', а git branch показує дві гілки, зірочка — проти proba. Тепер експеримент: хай бот на звичайний текст ще й рахує слова й символи. Відкрий nano bot.py і заміни останній рядок handle_update — return f"Ти написав: {text}" — на два:

    words = len(text.split())
    return f"Ти написав: {text}\nСлів: {words}, символів: {len(text)}."

text.split() розрізає текст на слова за пробілами, а len() рахує, скільки вийшло шматків (або скільки символів у рядку). Перевір і збережи комітом у гілку:

python3 try_bot.py "привіт, як справи"
git add bot.py
git commit -m "проба: бот рахує слова й символи в повідомленні"

Бот відповідає двома рядками: Ти написав: привіт, як справи і Слів: 3, символів: 17. Відповідь git на коміт починається з [proba …]: коміт записано в гілку proba. Хочеш побачити експеримент у Telegram — перезапусти службу: вона візьме файли поточної гілки. Тепер повертайся:

git switch master
python3 try_bot.py "привіт, як справи"
git log --oneline --graph --all -4

У master бот відповідає по-старому, одним рядком. Остання команда малює історію всіх гілок:

* 51b16fb (proba) проба: бот рахує слова й символи в повідомленні
* 654d9b6 (HEAD -> master) Revert "перемістив перевірку невідомих команд вище"
* 389010d перемістив перевірку невідомих команд вище
* 7abcaeb додав у README опис файлів бота

Позначки в дужках показують, де закінчується кожна гілка: proba на один коміт попереду master, а HEAD -> master означає, що ти зараз на master. Хеші в тебе свої, і можуть бути ще позначки origin/master чи github/master — так git показує, до якого коміту дійшла кожна копія. Тепер вирішуй сам:

В обох випадках гілку proba не видаляй: автоперевірка шукає в репозиторії другу гілку. І перезапусти службу, щоб живий бот працював з кодом master:

systemctl --user restart club-bot

11 Відправити історію в ~/repo.git

git branch --show-current
git push origin master
git push origin proba

Перша команда має надрукувати master: відправляємо з основної гілки. git push друкує кілька рядків підрахунку (Enumerating objects…, Writing objects…) — це git пакує коміти. Важливий останній рядок:

To /home/bohdan.g9/repo.git
   c255902..654d9b6  master -> master

Зліва — коміт, який був у ~/repo.git до команди, справа — який став після неї. Для proba буде * [new branch] proba -> proba: такої гілки там ще не було. Перевір, що коміти дійшли:

git log --oneline -1
git -C ~/repo.git log --oneline -1 master
git -C ~/repo.git branch

Параметр -C означає «виконай команду в іншій теці, не переходячи туди». Перші два рядки мають бути однакові — той самий хеш і те саме повідомлення: це й перевіряє критерій L03.4. Остання команда показує дві гілки, master і proba.

12 Перевірити себе перед кінцем заняття

git status
git log --oneline
git ls-files
git log --all --oneline -- .env
git branch

Зберегти роботу на GitHub

Третя копія — поза сервером. Віддалених репозиторіїв у тебе два, тому в git push назву пишемо явно: origin — це ~/repo.git, github — GitHub. Якщо GitHub у тебе немає (заняття 02), виконай лише коміт і git push origin master, а рядки з github пропусти: автоперевірка дивиться на ~/repo.git.

13 Зберегти залишки комітом й відправити

cd ~/app
git switch master
git add -A
git commit -m "Заняття 03: команда /fact, відкат поломок і гілка proba"
git push origin master
git push github master
git push github proba

Якщо git switch каже Already on 'master' — ти вже на основній гілці. Якщо git commit відповідає nothing to commit, working tree clean, усе вже лежить у комітах — це добре, переходь до push. Перший push оновлює ~/repo.git (потрібен, якщо щойно з’явився новий коміт), два наступні — GitHub. Останній рядок кожного — стрілка master -> master або proba -> proba; для proba на GitHub — * [new branch]. Відповідь Everything up-to-date означає, що відправляти нічого: ця копія вже має всі твої коміти.

Наприкінці наступних занять роби так само: коміт, потім git push origin master і git push github master — тоді обидві копії знають усю твою роботу.

14 Подивитися на результат на GitHub

Відкрий свій репозиторій club-app на github.com і онови сторінку. Має бути:

Якщо push відповів Permission denied (publickey), GitHub не впізнав ключ сервера. Виконай ssh -T git@github.com і дивись розділ «Якщо щось не працює».

Що перевіряє автоматика

Сьогодні автоперевірка не розмовляє з ботом — вона читає твій репозиторій ~/app. Рахує коміти з осмисленими повідомленнями (заготовка не входить, як і повідомлення, коротші за десять символів, чи слова-заглушки на кшталт fix і update), дивиться список відстежуваних файлів і правила .gitignore, рахує гілки, шукає твій останній коміт у ~/repo.git, читає розділи README.md і переглядає історію всіх гілок — чи не був там хоч раз файл .env. Якість повідомлень (L03.6) оцінює викладач: відкриє твій git log, вибере коміт і попросить пояснити, що в ньому змінилося.

Перевірка дивиться на гілку, на якій ти стоїш, і порівнює її останній коміт з ~/repo.git. Тому перед нею виконай git switch master і git push origin master. Запустити перевірку можна будь-коли кнопкою «Перевірити практичну частину» у розділі «Перевірка виконаних завдань» нижче на сторінці; крім того, вона сама проходить за розкладом.

КодЩо перевіряєтьсяЯкщо не зараховано
L03.1≥5 комітів з осмисленими повідомленнями
автоперевірка
Змістовних комітів менше за п'ять. Заготовка не рахується, як і повідомлення коротші за десять символів чи «fix», «update». Роби наступні коміти змістовними.
L03.2venv, база й .env не відстежуються git
автоперевірка
У git потрапило те, чого там бути не повинно: venv, файл бази (.db) або .env. Додай ці шляхи в .gitignore, а вже відстежувані прибери з індексу: git rm --cached .env
L03.3існує друга гілка
автоперевірка
У репозиторії лише одна гілка. Створи другу: git switch -c proba
L03.4~/repo.git містить ті самі коміти
автоперевірка
Останнього коміту немає в ~/repo.git — зміни не відправлено. Виконай git push origin master (назву твоєї гілки покаже git branch --show-current)
L03.5README має розділи «Що це» і «Як запустити»
автоперевірка
У ~/app/README.md мають бути розділи «## Що це» і «## Як запустити» з твоїм текстом (не менше 60 символів у кожному), а службовий рядок «Заняття 01: опиши одним абзацом…» треба видалити.
L03.6якість повідомлень комітів
викладач
Позначку ставить викладач на занятті або після нього.
L03.11токен ніколи не потрапляв у Git
автоперевірка
Файл .env колись потрапив у коміт, і токен лишився в історії назавжди. Прибери .env з індексу (git rm --cached .env), а токен вважай украденим: @BotFather, /revoke, новий токен у .env. Якщо коміт уже відправлено в ~/repo.git чи на GitHub, історію чистить викладач — самому цей пункт не виправити.

Твої відповіді на питання уроку

Кожне питання оцінюється відразу в браузері — це лишається так само. Ця кнопка лише надсилає підсумок учителю, щоб відповіді не зникли разом із закритою вкладкою.

Перевірка виконаних завдань

Список критеріїв і що саме вони означають — у таблиці вище, «Що перевіряє автоматика». Тут — та сама перевірка, але прямо зараз, без очікування на найближчий прохід за розкладом.

Домашнє завдання

  1. Команда /time. Додай боту команду, яка каже, котра година, — так само, як /fact: перевірка в handle_update вище за if text.startswith("/") і рядок у HELP. datetime у bot.py уже імпортовано (на початку файлу є рядок from datetime import datetime), тож відповідь може бути такою: return f"Зараз {datetime.now():%H:%M}." Перевір через try_bot.py і порівняй зі своїм годинником. Не збігається? Виконай на сервері date і подивись, який часовий пояс він друкує: годинник сервера налаштований не на київський час. Виправ відповідь так, щоб час був київський:
        if text.startswith("/time"):
            from zoneinfo import ZoneInfo
            kyiv = datetime.now(ZoneInfo("Europe/Kyiv"))
            return f"У Києві зараз {kyiv:%H:%M}."
    Це два окремі коміти: «додав команду /time» і «виправив /time: час за Києвом, а не за поясом сервера».
  2. README для людини, яка бачить його вперше. Дай прочитати свій README.md комусь із рідних або однокласнику й нічого не пояснюй уголос. Запиши, де людина зупинилася й що перепитала, і виправ саме ці місця. Кожне виправлення — окремим комітом зі змістовним повідомленням.
  3. Поясни свої рядки. Виконай git log -p -3 — три останні коміти разом зі змінами — і перечитай кожен рядок із плюсом: на наступному занятті викладач покаже будь-який із них і попросить пояснити. Спробуй також git log -S "/fact" --oneline: ця команда знаходить коміт, у якому з’явилося слово /fact.
  4. Ще один експеримент. Створи гілку proba-pryvit і навчи в ній бота окремо відповідати на слово «привіт» (перевірка if text.lower() == "привіт": перед останнім рядком функції). Перевір, вирішуй, чи переносити в master, і відправ обидві гілки в обидві копії.
  5. Наприкінці. git status — working tree clean; git push origin master і git push github master; git log --oneline -1 і git -C ~/repo.git log --oneline -1 master друкують той самий коміт.

Якщо щось не працює

Відкрився незнайомий редактор або екран «завис»

Якщо внизу екрана двокрапка : або (END), це не редактор, а посторінковий перегляд довгого виводу git log, git diff чи git show. Натисни q.

Якщо екран заповнили рядки зі знаком ~ ліворуч, а літери друкуються дивно, — це редактор vi. Git відкриває його, коли ти забув -m у git commit або --no-edit у git revert чи git merge, а редактор у налаштуваннях не вказано. Вихід:

Щоб це не повторювалося, один раз виконай git config --global core.editor nano. Відтепер git відкриватиме nano: повідомлення пишеш у першому рядку, потім Ctrl+O, Enter, Ctrl+X.

git commit пише «Please tell me who you are»

Git не знає, яким іменем підписати коміт, і поруч друкує Author identity unknown. Коміт при цьому не створено. Назви себе (крок 2) і повтори команду:

git config --global user.name "Богдан"
git config --global user.email "$(whoami)@club.local"
git config --global --list

git switch не перемикає гілку

Відповідь error: Your local changes to the following files would be overwritten by checkout означає: у тебе є зміни, не збережені комітом у файлі, який у двох гілках різний, і перемикання їх знищило б. Git відмовився й нічого не зачепив. Подивись, що це за зміни:

git status
git diff

Зміни потрібні — збережи комітом їх у поточну гілку (git add, git commit -m "…"). Не потрібні — git restore назва_файлу. Після цього git switch спрацює.

Бот мовчить або відповідає по-старому

Пройди по черзі:

git branch --show-current
git status
python3 try_bot.py "/start"
systemctl --user restart club-bot
systemctl --user status club-bot
tail -n 30 ~/logs/club-bot.log

Якщо systemctl --user status club-bot показує inactive поза заняттям — служби груп увімкнені за розкладом (заняття й вечір з 17:00 до 23:00). Telegram тримає повідомлення до доби: бот відповість, щойно служба підніметься.

git push не проходить

git pull --rebase github master
git push github master

Параметр --rebase тут обов’язковий: без нього git зупиниться з повідомленням Need to specify how to reconcile divergent branches і нічого не зробить. З ним git спершу забирає коміти з GitHub, а потім кладе твої поверх них.

git revert зупинився з CONFLICT

Так буває, коли ти скасовуєш не останній коміт, а давніший, і ті самі рядки відтоді змінювалися ще раз: git не може сам вирішити, яку версію лишити. Поверни все як було до команди:

git revert --abort
git status

git status має знову сказати working tree clean. Скасовуй коміти по черзі, починаючи з найновішого, — або поклич викладача й розберіться разом.

.env потрапив у коміт

Ознаки: git ls-files показує .env, git log --all --oneline -- .env друкує рядки або критерій L03.11 червоний. Спершу — токен, бо він уже вважається вкраденим:

  1. у @BotFather — /revoke, вибери бота, отримай новий токен;
  2. впиши його в ~/app/.env замість старого й перезапусти службу: systemctl --user restart club-bot.

Потім — репозиторій. Перестань відстежувати файл і перевір, що правило на місці:

git rm --cached .env
git check-ignore -v .env

Якщо .env потрапив лише в один коміт — git log --all --oneline -- .env друкує один рядок, і це твій останній коміт, — і цей коміт ще нікуди не відправлено (його немає ні в git -C ~/repo.git log --oneline -1 master, ні на GitHub), перепиши його без .env: git commit --amend --no-edit. Після цього git log --all --oneline -- .env не друкує нічого, і L03.11 стане зеленим. Якщо коміт уже відправлено, зроби звичайний коміт git commit -m "прибрав .env з репозиторію": git перестане стежити за файлом, але в копіях старий коміт лишиться — історію в трьох місцях чистять разом з викладачем. Захищає тебе в будь-якому разі новий токен, а не зелена позначка.

Якщо git rm --cached .env відповідає did not match any files, зараз git за .env не стежить — файл був лише в старому коміті.