Большая часть людей, работающая в IT сфере трекает время — заполняет таблицы/формы/джиру. Я это делаю в двух компаниях, соответственно на двух ресурсов. Одна из компаний при этом требует, чтобы я это делал каждый день. Порой бывает трудно себя заставить в конце рабочего дня открывать эти ресурсы и заполнять 90 процентов однотипных полей, а потом еще придумывать чем ты занимался.
Понятно, что все трекают время по-разному, кто-то трекает 1 таск, с описанием того что он делал, кто-то списывает на разные. Данная статья — не практический, сколько мотивационный пример, показывающий, что автоматизировать можно все что угодно.
Исходные данные
- 1 google форма вида:
- 1 jira с плагином Tempo
- «какая-то работа которую я делал, ее нужно описать»
- bash, python и много энтузиазма
Идея
- Для того чтобы не придумывать каждый раз чем я занимался, я решил что всю информацию можно брать из моих коммитов, раз я все равно их делаю и пишу их описание. Для этого
- Организовать правильно структуру директорий моих репозиториев, чтобы можно было структурировать текст поля «что я делал сегодня»
- Написать парсер, который будет делать структуру сообщения, собирать коммит-месседжи за сегодняшний день, и заполнять итоговое сообщения, убирая всяких мусор, дублирующие коммиты и прочее
- Отправить готовые данные в форму и джиру
- Just for fun — интегрировать скрипт в macOS, чтобы можно было его запускать голосом, например, или по расписанию
Реализация парсера
Структура репозиториев
Для удобства, в $HOME/Documents/Git я сделал следующую структуру:
- Проект 1
- репозиторий 1
- репозиторий 2
- Проект 2
- репозиторий 1
и так далее.
Переменные
#!/usr/bin/env bash
# Переменные
date=$1
workdir_root="$HOME/scripts/TimeTracker"
git_dir="$HOME/Documents/Git"
if [[ "$date" == 'today' ]]; then
day=$(date +'%Y-%m-%d')
elif [[ "$date" == 'yesterday' ]]; then
day=$(date -v -1d +'%Y-%m-%d')
fi
Задаем переменные — где у нас лежит директория с гит репозиториями, за какую дату сдавать отчет — 2 опции, за вчера и сегодня. У меня трекается время всегда за сегодня, но на случай, если компьютер выключен или была еще какая-то причина — на следующий день, скрипт запустится с параметром — затрекать за вчера, чтобы если я день пропустил — он его отправил.
Сам скрипт у меня лежит в workdir_root, там же будут храниться текстовые файлы с описанием коммитов:
Функции
# Описание: В зависимости он передаваемой в функцию $date, она возвращает только заголовки коммитов,
# принадлежащих текущему пользователю либо за сегодня, либо за вчера. Запускается в
# текущей директории.
# Использование: GetCommits today|yesterday
function GetCommits() {
local date
date=$1
if [[ "$date" == 'today' ]]; then
git log --pretty=format:"%s" --committer="$(git config user.name)" --after=9am | sort -u
elif [[ "$date" == 'yesterday' ]]; then
git log --pretty=format:"%s" --committer="$(git config user.name)" --after=yesterday.9am --before=9am | sort -u
fi
}
# Описание: удаляет файлы внутри указанной директории или указанный файл и проверяет наличие темповых директорий
# Использование: ClearBeforeStart path_to_dir/file
function ClearBeforeStart() {
local object
for object in "$@"; do
if [[ -d $object ]]; then
(rm -rf "${object:?}*/") 2>/dev/null
else
(rm -rf "${object}") 2>/dev/null
fi
done
mkdir -p "$workdir_root"/tmp/old
mkdir -p "$workdir_root"/tmp/merged
mkdir -p "$workdir_root"/tmp/summary
}
# Описание: Функция проходит по директориям в $git_dir и для каждого репозитория
# записывает файл вида $project_name-$project_repo.txt в директорию $workdir_root/tmp
# с содержимым функции GetCommits, в зависимости от указанной $date, передаваемой в эту функцию
# Использование: ProcessGitRepos today|yesterday
function ProcessGitRepos() {
local project_dirs dir project_name project_repo
project_dirs=$(find "$git_dir" -type d ! -path "*/Private/*" ! -path "*.idea" ! -path "*.git" -maxdepth 2 -mindepth 2)
for dir in $project_dirs; do
if [[ -d $dir/.git ]]; then
cd "$dir" || exit
project_name="$(echo "$dir" | cut -d '/' -f 6)"
project_repo="$(echo "$dir" | cut -d '/' -f 7)"
GetCommits "$date" > "$workdir_root/tmp/old/$project_name-$project_repo.txt"
fi
done}
# Описание: Если в указанном файле есть строчки, начинающиеся с одного и того же слова, функция склеивает
# их в одну по шаблону СЛОВО строка1, строка2 и т.д; Новый текст сохраняет в $workdir_root/tmp/merged
# Пример:
# Было: fix config templates
# fix secrets
# Стало: fix config templates, secrets
# Использование: MergeLines file_path
function MergeLines() {
local file line line_start_new line_start_old
file=$1
line_start_old=""
while IFS= read -r line
do
line_start_new=$(echo "$line" | cut -d ' ' -f 1)
if [[ $line_start_new == "$line_start_old" ]]; then
echo -ne ",${line/$line_start_new/}" >> "${file/old/merged}"
else
echo -ne "\n $line" >> "${file/old/merged}"
fi
line_start_old=$line_start_new
done < <(grep -v '^ *#' < "$file")
}
# Описание: Главная функция по обработке файлов с описанием коммитов. Вызывает функцию MergeLines,
# удаляет строки с заголовками Pull Request, Merge remote-tracking, строки fix без описания.
# Результат - файлы созданные функцией MergeLines: в $workdir_root/tmp/merged
# Использование: ProcessCommits
function ProcessCommits() {
local file project_name project_repo
for file in "$workdir_root/tmp/old/*" ; do
gsed -i '/^fix$/d' "$file"
gsed -i '/Merge remote-tracking/d' "$file"
gsed -i '/Pull Request/d' "$file"
MergeLines "$file"
if [[ -f "${file/old/merged}" ]]; then
echo '' >> "${file/old/merged}"
project_name="$(echo "$file" | cut -d '/' -f 8 | cut -d '-' -f 1)"
project_repo=$(echo "$file" | cut -d '/' -f 8 | cut -d '-' -f 2 | cut -d '.' -f 1)
gsed -i "1 s/.*/$project_name: $project_repo/" "${file/old/merged}"
fi
done
}
# Описание: склеивает все файлы из $workdir_root/tmp/merged в 1 файл $workdir_root/tmp/summary
# Использование: MergeFiles
function MergeFiles() {
local file
if [[ -z $(ls -A "$workdir_root"/tmp/merged/) ]]; then
echo "Работа над ******, поддержка" >> "$workdir_root"/tmp/summary/"$day".txt
else
for file in "$workdir_root/tmp/merged/*" ; do
cat "$file" >> "$workdir_root"/tmp/summary/"$day".txt
done
fi ClearBeforeStart "$workdir_root"/tmp/merged "$workdir_root"/tmp/old
}
# Описание: Проверяет, создан ли "$workdir_root"/tmp/summary/"$day".txt, используется при повторном запуске чтобы определить, создавали мы уже лог или нет
#
# Использование: CheckPushToTracker
function CheckPushToTracker() {
if [[ -f "$workdir_root"/tmp/summary/"$day".txt ]]; then
echo "$date"_exist
true
else
false
fi
}
Запуск парсера
После функций добавляем:
if ! CheckPushToTracker; then
ProcessGitRepos "$date"
ProcessCommits
MergeFiles
echo "SUCCESS_$date"
fi
При запуске скрипт ищет текстовый файл с сформированным сообщением за сегодня или вчера. Если такой файл не найден, он проходит по всем директориям с репозиториями, если там я делал коммит — создает для него текстовый файл с описанием названия проекта, репозитория, описанием коммитов. Потом он в этом описании чистит дублирующие коммиты, делает совмещает строки с одинаковым первым словом, например:
Было: fix config, templates
fix secrets
Стало: fix config, templates, secrets
А в самом конце уже склеивает все файлы (созданные для каждого репозитория) — в один, главный файл, как результат сегодняшнего дня.
А если скрипт за сегодня вообще не найдет коммитов, то запишет в этот файл стандартное сообщение, которое можно найти в функции MergeFiles.
Реализация отправки данных
Я бы с удовольствием продолжил в том же духе и продолжил на баше, но столкнулся c проблемой — мой curl запрос никак не хотел на итоговой форме выделять radiobutton.
В дополнении к этому я решил на первое время не делать это на 100 процентов автоматически, а чтобы скрипт показывал окошко с результатом сообщения, чтобы я мог сверить, устраивает ли меня все, может я захочу что-нибудь дописать и тогда уже отправить.
Для этого я решил использовать Python c библиотекой tkinter — для окошка.
Переменные и шапка
from datetime import date as d, timedelta
from datetime import datetime
import requests
import sys
from tkinter import *
from jira import JIRA
# Определяем дату в зависимости от переданного параметра
day = ''
if sys.argv[1] == 'today':
day = d.today()
elif sys.argv[1] == 'yesterday':
day = d.today() - timedelta(days=1)
filename = f'/Users/sandzhaj/scripts/TimeTracker/tmp/summary/{day}.txt'
# Данные формы
form_id = 'айди гугл формы'
email = {'id': 'emailAddress', 'val': 'моя почта'}
name = {'id': 'entry.208546599', 'val': 'Марк Минаков'}
date_form = {'id': 'entry.1615662905', 'val': day}
hours = {'id': 'entry.1594873309', 'val': 8}
with open(filename) as file:
tasks = {'id': 'entry.1191337264', 'val': file.read()}
GoogleURL = f'https://docs.google.com/forms/d/e/{form_id}'
urlResponse = GoogleURL + '/formResponse'
urlReferer = GoogleURL + '/viewform'
user_agent = {'Referer': urlReferer,
'User-Agent': "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36"}
Взять ID полей можно на странице формы в браузере в консоли разработчика. Проще всего заполнить форму и найти потом это по коду:
Создаем окошко и отправляем данные
# Создаем главное окно и размещаем его по центру экрана
root = Tk()
w = 450 # width for the Tk root
h = 275 # height for the Tk root
# get screen width and height
ws = root.winfo_screenwidth() # width of the screen
hs = root.winfo_screenheight() # height of the screen
# calculate x and y coordinates for the Tk root window
x = (ws/2) - (w/2)
y = (hs/2) - (h/2)
root.geometry('%dx%d+%d+%d' % (w, h, x, y))
def save_and_push():
"""Функция перезаписывает содержимое файла, отправляет форму. Запускается по кнопке"""
# Перезаписываем файл с задачами. Нужно на случай, если вносили изменения
with open(filename, "w") as output_file:
text = textEditor.get(0.1, END)
output_file.write(text)
tasks['val'] = text
# Задаем данные для запроса в гугл форму
form_data = {
email['id']: email['val'],
name['id']: name['val'],
date_form['id']: date_form['val'],
hours['id']: hours['val'],
tasks['id']: tasks['val'],
}
# Отправляем данные в форму, немного увеличиваем окно и добавляем строчку о том, успешно или нет отправлены данные
r = requests.post(urlResponse, data=form_data, headers=user_agent)
root.geometry('450x300')
if r.status_code == 200:
Label(root, text='Forms: Успешно', fg='green').pack()
print(r.text)
else:
Label(root, text=f'Forms: Ошибка {r.status_code}', fg='red').pack()
print(r.text)
# Данные Jira
from_date_str = str(day) + "T09:00:00.000+0300"
from_date = datetime.strptime(from_date_str, '%Y-%m-%dT%H:%M:%S.000%z')
jira = JIRA(server="https://jira.myserver.ru", token_auth='МОЙ_ТОКЕН')
r_jira = jira.add_worklog(issue="НОМЕР_ЗАДАЧИ", timeSpent="8h", comment=tasks['val'], started=from_date)
# Опять увеличиваем окошко и добавляем строчку об успешности
root.geometry('450x325')
if r_jira:
Label(root, text='Lanit: Успешно', fg='green').pack()
else:
Label(root, text=f'Lanit: Ошибка', fg='red').pack()
# Описываем содержимое окошка: добавляем текстовое поле, с содержимым списка наших задач, добавляем кнопку "Сохранить и отправить", которая запустит функцию выше
textEditor = Text(root, width=43, height=10, wrap=WORD, font=("Helvetica", 15), border=10)
textEditor.pack()
textEditor.insert(END, tasks['val'])
button_save = Button(root, text="Cохранить и отправить", command=save_and_push)
button_save.pack(pady=15)
# Заголовок окна
root.title(f"TrackerTime {date_form['val']}")
# Чтобы окно не закрывалось после появления
root.mainloop()
Интеграция в систему
Итак, у нас есть два скрипта, условно запускающиеся следующим образом:
bash "$workdir_root"/main.sh today # Создает файл с описанием работы за сегодня
python3 "$workdir_root"/send_form.py today # Показывает окошко с описанием работы и отправляет данные по кнопке
Можно запускать питоновский скрипт из первого скрипта, в случае если время не затрекано, и добавить первый скрипт в крон, если у вас линукс.
Я же решил воспользоваться встроенным в мак приложением «Быстрые команды», чтобы можно было запускать голосом.
Как можно заметить, перед тем как запустить питоновский скрипт — подключается VPN, за которым доступна Джира, а в конце выключается VPN.
Такую же страничку я сделал для yesterday и добавил обе в запуск по расписанию на конец рабочего дня. Если я заканчиваю пораньше, могу просто сказать «Привет, Siri, затрекай время».