sandzhaj.space

Меню
  • главная
  • cтатьи
  • книги
  • про автора
Меню

Как облегчить трекинг времени

Опубликовано в 27.06.202227.06.2022 от sandzhaj

Большая часть людей, работающая в 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, там же будут храниться текстовые файлы с описанием коммитов:

Директории old и merged используются во время работы скрипта и самоочищаются

Функции

# Описание: В зависимости он передаваемой в функцию $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, затрекай время».

  • автоматизация
  • трекинг
  • Поиск

    Подпишись

    Теги

    bash git helm python zsh автоматизация нейросеть ооп терминал трекинг

    ©2025 sandzhaj.space