3 минут чтения
7 апреля 2021 г.
Пишем скрипт для автоматизации коммитов GitHub
У GitHub есть API, позволяющий делать всё, что можно сделать руками: создавать репозитории, делать коммиты файлов и переключаться между ветками. Не все используют GUI при работе с системой контроля версий, и для коммита файлов приходится вводить не одну команду в терминал, а смена веток нередко приводит к запутанности действий. Сегодня мы поэкспериментируем с GitHub API и напишем скрипт, который сам собирает все файлы текущей директории раз в час и отправляет в отдельную ветку на GitHub при помощи get, post и put-запросов к методу /repos.
Получение access token
Для работы с GitHub API нужно получить токен, который выдаётся каждому пользователю в настройках. Для создания нового токена нажимаем на «Generate new token»:
В процессе работы с GitHub API токен может просрочиться и в ответ придёт ошибка «Bad credentials». Для решения проблемы можно создать новый токен или получить токен приложения
Следом нужно оставить название для токена и отметить области доступа ключу. Для наших целей достаточно дать доступ только к разделу repo.
После в зелёном окошке появится токен:
Отправляем запросы
Подключим нужные библиотеки и введём некоторые константные значения: логин, токен, название для репозитория и ветки, в которую в автоматическом режиме будут отправляться файлы:
import requests
import base64
import json
import os
username = ‘leftjoin’
repo = ‘leftjoin’
token = «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»
new_branch_name = ‘automatic’
Создадим новый репозиторий post-запросом. В поле data передаем название репозитория, в auth — логин и токен:
r = requests.post(‘https://api.github.com/user/repos’,
data=json.dumps({‘name’:repo}),
auth=(username, token),
headers={«Content-Type»: «application/json»})
Теперь загрузим файл README.md, в котором будет строка “Hello, world!”. Для этого строку нужно перекодировать в base64 – GitHub принимает данные в такой кодировке:
content = «Hello, world!»
b_content = content.encode(‘utf-8’)
base64_content = base64.b64encode(b_content)
base64_content_str = base64_content.decode(‘utf-8’)
Теперь создадим коммит в формате словаря: он состоит из пути (пока его оставим пустым), сообщения коммита и содержания:
f = {‘path’:»,
‘message’: ‘Automatic update’,
‘content’: base64_content_str
}
Отправим файл в репозиторий put-запросом, передав путь до README.md прямо в ссылке запроса:
f_resp = requests.put(f’https://api.github.com/repos/{
auth=(username, token),
headers={ «Content-Type»: «application/json» },
data=json.dumps(f))
Теперь в репозитории в ветке main есть один файл README.md, который содержит строку «Hello, world!»:
Работа с ветками
Так как наш скрипт подразумевает работу в фоновом режиме, не всегда будет возможность контролировать, какие версии файлов он отправляет. Чтобы не засорять основную ветку будем отправлять все файлы в специальную, из которой при надобности можно будет достать обновления. Логика такая: проверяем, есть ли в репозитории такая ветка и, если нет, создаём её. Затем получаем все файлы текущей директории, построчно считываем, перекодируем в base64, коммитим и отправляем в ветку для автоматических обновлений.
Начнём с функции, которая определяет, существует ли ветка с таким названием. Она отправит запрос на получение всех веток репозитория, и вернёт False, если ветки не существует:
def branch_exist(branch_name):
branches = requests.get(f’https://api.github.com/repos/{username}/{repo}/git/refs’).json()
try:
for branch in branches:
if branch[‘ref’] == ‘refs/heads/’ + branch_name:
return True
return False
except Exception:
return False
У каждой ветки, файлов и репозиториев на GitHub есть уникальный идентификатор, получаемый путём хэш-суммы алгоритмом SHA. Он необходим как для модификации файлов, так и для смены веток — опишем функцию, которая по названию ветки получает SHA для неё:
def get_branch_sha(branch_name):
try:
branches = requests.get(f’https://api.github.com/repos/{username}/{repo}/git/refs’).json()
for branch in branches:
sha = None
if branch[‘ref’] == ‘refs/heads/’ + branch_name:
sha = branch[‘object’][‘sha’]
return sha
except Exception:
return None
Следом опишем функцию создания новой ветки с названием из переменной new_branch_name:
def create_branch():
main_branch_sha = get_branch_sha(‘main’)
requests.post(f’https://api.github.com/repos/{username}/{repo}/git/refs’,
auth=(username, token),
data=json.dumps({
‘ref’:f’refs/heads/{new_branch_name}’,
‘sha’:main_branch_sha
})).json()
Для модификации файлов тоже необходимо знать его идентификатор:
def get_sha(path):
r = requests.get(f’https://api.github.com/repos/{username}/{repo}/contents/{path}’,
auth=(username, token),
data=json.dumps({
‘branch’: new_branch_name
}))
sha = r.json()[‘sha’]
return sha
Наконец, опишем ряд главных функций — первая по аналогии с примером из начала материала принимает содержимое файла и формирует коммит в отдельную ветку, а вторая отправляет файл в указанный репозиторий:
def make_file_and_commit(content, sha=None):
b_content = content.encode(‘utf-8’)
base64_content = base64.b64encode(b_content)
base64_content_str = base64_content.decode(‘utf-8’)
f = {‘path’:»,
‘message’: ‘Automatic update’,
‘content’: base64_content_str,
‘sha’:sha,
‘branch’: new_branch_name}
return f
def send_file(path, data):
f_resp = requests.put(f’https://api.github.com/repos/{username}/{repo}/contents/{path}’,
auth=(username, token),
headers={ «Content-Type»: «application/json» },
data=json.dumps(data))
print(f_resp.json())
return f_resp
И ещё две функции напоследок: первая нужна, потому что мы должны знать, модифицируем мы уже существующий файл или новый. В случае с новым нет необходимости получать SHA для файла — его просто не существует. Вторая функция считывает всё содержимое файла в строку.
def file_exist(path):
r = requests.get(f’https://api.github.com/repos/{username}/{repo}/contents/{path}’,
auth=(username, token))
return r.ok
def file_reader(path):
lines = «»
with open(path, ‘r’) as f:
for line in f:
lines += line
return lines
Соберём всё вместе в функцию main(). Проверяем, существует ли ветка для автоматических коммитов, затем получаем все файлы директории, проверяем их существование в репозитории и отправляем:
def main():
if not branch_exist(new_branch_name):
create_branch()
files = [os.path.join(dp, f) for dp, dn, fn in os.walk(os.path.expanduser(«.»)) for f in fn]
for path in files:
print(path)
sha = None
if file_exist(path):
sha = get_sha(path)
lines = file_reader(path)
f = make_file_and_commit(lines, sha)
r = send_file(path, f)
Перевести скрипт в автоматический режим может помочь библиотека schedule, которую мы уже использовали в материале «Анализ рынка вакансий аналитики и BI: дашборд в Tableau»
import schedule
schedule.every().hour.do(main)
while True:
schedule.run_pending()
Вот и всё: мы написали скрипт, который самостоятельно каждый час отправляет все файлы текущей директории в ветку на GitHub.
[ Рекомендации ]
Читайте также
4 минут чтения
16 сентября 2024
[ Связаться ]
Давайте раскроем потенциал вашего бизнеса вместе
Заполните форму на бесплатную консультацию