Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Tests

on:
push:
branches: [master, main]
pull_request:
branches: [master, main]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Create dummy config files
run: |
mkdir -p configs
cp configs/config.ini.example configs/config.ini || true
cp configs/templates.json.example configs/templates.json || true
if [ ! -f configs/config.ini ]; then
cat > configs/config.ini << 'INI'
[DEFAULT]
base_url = https://gitlab.example.com
group_id = 1
custom_template = Custom
GITLAB_TOKEN = test-token
delete_branch_after_merge = true
squash_commits = true
INI
fi
if [ ! -f configs/templates.json ]; then
echo '{"templates": [], "reviewers": [], "productionMappings": {}}' > configs/templates.json
fi

- name: Run syntax checks
run: |
python -m py_compile gitHappens.py main.py config.py templates.py gitlab_api.py git_utils.py interactive.py
python -m py_compile commands/create_issue.py commands/open_mr.py commands/review.py commands/deploy.py commands/summary.py

- name: Run tests
run: python -m unittest discover -s tests -v
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,20 @@ Add following line to your `.bashrc` or `.zshrc` file

Run `source ~/.zshrc` or restart terminal.

## Usage ⚡
## Project Structure 📁

The codebase has been refactored into a modular architecture for better maintainability:



### Running Tests

Error: Unauthorized (401). Your GitLab token is probably expired, invalid, or missing required permissions.
Please generate a new token and update your configs/config.ini.

---

## Usage## Usage ⚡

### Project selection

Expand Down
Empty file added commands/__init__.py
Empty file.
Binary file added commands/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file added commands/__pycache__/create_issue.cpython-314.pyc
Binary file not shown.
Binary file added commands/__pycache__/deploy.cpython-314.pyc
Binary file not shown.
Binary file added commands/__pycache__/open_mr.cpython-314.pyc
Binary file not shown.
Binary file added commands/__pycache__/review.cpython-314.pyc
Binary file not shown.
Binary file added commands/__pycache__/summary.cpython-314.pyc
Binary file not shown.
92 changes: 92 additions & 0 deletions commands/create_issue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from config import API_URL, GITLAB_TOKEN, MAIN_BRANCH
from templates import TEMPLATES
from gitlab_api import create_branch, create_merge_request
from interactive import select_template, getIssueSettings, get_milestone, get_iteration, get_epic


def createIssue(title, project_id, milestoneId, epic, iteration, settings):
if settings:
issueType = settings.get('type') or 'issue'
return executeIssueCreate(project_id, title, settings.get('labels'), milestoneId, epic, iteration, settings.get('weight'), settings.get('estimated_time'), issueType)
print("No settings in template")
exit(2)
pass

def executeIssueCreate(project_id, title, labels, milestoneId, epic, iteration, weight, estimated_time, issue_type='issue'):
labels = ",".join(labels) if type(labels) == list else labels
assignee_id = getAuthorizedUser()['id']
issue_command = [
"glab", "api",
f"/projects/{str(project_id)}/issues",
"-f", f'title={title}',
"-f", f'assignee_ids={assignee_id}',
"-f", f'issue_type={issue_type}'
]
if labels:
issue_command.append("-f")
issue_command.append(f'labels={labels}')

if weight:
issue_command.append("-f")
issue_command.append(f'weight={str(weight)}')

if milestoneId:
issue_command.append("-f")
issue_command.append(f'milestone_id={str(milestoneId)}')

if epic:
epicId = epic['id']
issue_command.append("-f")
issue_command.append(f'epic_id={str(epicId)}')

# Set the description, including iteration, estimated time, and other info
description = ""
if iteration:
iterationId = iteration['id']
description += f"/iteration *iteration:{str(iterationId)} "

if estimated_time:
description += f"\n/estimate {estimated_time}m "

issue_command.extend(["-f", f'description={description}'])

issue_output = subprocess.check_output(issue_command)
return json.loads(issue_output.decode())

def startIssueCreation(project_id, title, milestone, epic, iteration, selectedSettings, onlyIssue):
# Prompt for estimated time
estimated_time = inquirer.prompt([
inquirer.Text('estimated_time',
message='Estimated time to complete this issue (in minutes, optional)',
validate=lambda _, x: x == '' or x.isdigit())
])['estimated_time']

# If multiple project IDs, split the estimated time
if isinstance(project_id, list):
estimated_time_per_project = int(estimated_time) / len(project_id) if estimated_time else None
else:
estimated_time_per_project = estimated_time

# Modify settings to include estimated time
if estimated_time_per_project:
selectedSettings = selectedSettings.copy() if selectedSettings else {}
selectedSettings['estimated_time'] = int(estimated_time_per_project)

createdIssue = createIssue(title, project_id, milestone, epic, iteration, selectedSettings)
print(f"Issue #{createdIssue['iid']}: {createdIssue['title']} created.")

if onlyIssue:
return createdIssue

createdBranch = create_branch(project_id, createdIssue)

createdMergeRequest = create_merge_request(project_id, createdBranch, createdIssue, selectedSettings.get('labels'), milestone)
print(f"Merge request #{createdMergeRequest['iid']}: {createdMergeRequest['title']} created.")

print("Run:")
print(" git fetch origin")
print(f" git checkout -b '{createdMergeRequest['source_branch']}' 'origin/{createdMergeRequest['source_branch']}'")
print("to switch to new branch.")

return createdIssue

70 changes: 70 additions & 0 deletions commands/deploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import configparser
import subprocess

from config import API_URL
from interactive import getActiveIteration
from commands.create_issue import createIssue
from interactive import selectLabels


def process_report(text, minutes):
# Get the incident project ID from config
try:
incident_project_id = config.get('DEFAULT', 'incident_project_id')
except (configparser.NoOptionError, configparser.NoSectionError):
print("Error: incident_project_id not found in config.ini")
print("Please add your incident project ID to configs/config.ini under [DEFAULT] section:")
print("incident_project_id = your_project_id_here")
return

issue_title = f"Incident Report: {text}"

selected_label = selectLabels('Department')

incident_settings = {
'labels': ['incident', 'report'],
'onlyIssue': True,
'type': 'incident'
}

if selected_label:
incident_settings['labels'].append(selected_label)

try:
# Create the incident issue
iteration = getActiveIteration()
created_issue = createIssue(issue_title, incident_project_id, False, False, iteration, incident_settings)
issue_iid = created_issue['iid']

closeOpenedIssue(issue_iid, incident_project_id)
print(f"Incident issue #{issue_iid} created successfully.")
print(f"Title: {issue_title}")

# Add time tracking to the issue
time_tracking_command = [
"glab", "api",
f"/projects/{incident_project_id}/issues/{issue_iid}/add_spent_time",
"-f", f"duration={minutes}m"
]

try:
subprocess.run(time_tracking_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"Added {minutes} minutes to issue time tracking.")
except subprocess.CalledProcessError as e:
print(f"Error adding time tracking: {str(e)}")

except Exception as e:
print(f"Error creating incident issue: {str(e)}")

def closeOpenedIssue(issue_iid, project_id):
issue_command = [
"glab", "api",
f"/projects/{project_id}/issues/{issue_iid}",
'-X', 'PUT',
'-f', 'state_event=close'
]
try:
subprocess.run(issue_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as e:
print(f"Error closing issue: {str(e)}")

17 changes: 17 additions & 0 deletions commands/open_mr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import subprocess
import webbrowser

from config import BASE_URL
from gitlab_api import getActiveMergeRequestId
from git_utils import getCurrentBranch


def openMergeRequestInBrowser():
try:
merge_request_id = getActiveMergeRequestId()
remote_url = subprocess.check_output(["git", "config", "--get", "remote.origin.url"], text=True).strip()
url = BASE_URL + '/' + remote_url.split(':')[1][:-4]
webbrowser.open(f"{url}/-/merge_requests/{merge_request_id}")
except subprocess.CalledProcessError:
return None

38 changes: 38 additions & 0 deletions commands/review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import subprocess

from config import API_URL
from gitlab_api import getCurrentIssueId, addReviewersToMergeRequest
from interactive import chooseReviewersManually


def track_issue_time():
# Get the current merge request
try:
project_id = get_project_id()
issue_id = getCurrentIssueId()
except Exception as e:
print(f"Error getting issue details: {str(e)}")
return

# Prompt for actual time spent
spent_time = inquirer.prompt([
inquirer.Text('spent_time',
message='How many minutes did you actually spend on this issue?',
validate=lambda _, x: x.isdigit())
])['spent_time']

# Add spent time to the issue description
time_tracking_command = [
"glab", "api",
f"/projects/{project_id}/issues/{issue_id}/notes",
"-f", f"body=/spend {spent_time}m"
]

try:
subprocess.run(time_tracking_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
print(f"Added {spent_time} minutes to issue {issue_id} time tracking.")
except subprocess.CalledProcessError as e:
print(f"Error adding time tracking: {str(e)}")
except Exception as e:
print(f"Error tracking issue time: {str(e)}")

38 changes: 38 additions & 0 deletions commands/summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from config import config
from git_utils import get_two_weeks_commits


def generate_smart_summary():
commits = get_two_weeks_commits(return_output=True)
if not commits:
return

# Check if OpenAI API key is set
openai_api_key = config.get('DEFAULT', 'OPENAI_API_KEY', fallback=None)
if not openai_api_key:
print("OpenAI API key not set. Skipping AI summary generation.")
return

# Dynamically import openai only if API key is present
try:
import openai
except ImportError:
print("OpenAI package not installed. Please install it using: pip install openai")
return

openai.api_key = openai_api_key

try:
response = openai.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a helpful assistant that summarizes git commits. Provide a concise, well-organized summary of the main changes and themes."},
{"role": "user", "content": f"Please summarize these git commits in a clear, bulleted format:\n\n{commits}"}
]
)

print("\n📋 AI-Generated Summary of Recent Changes:\n")
print(response.choices[0].message.content)
except Exception as e:
print(f"Error generating AI summary: {e}")

20 changes: 20 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import os
import configparser

config = configparser.ConfigParser()
absolute_config_path = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(absolute_config_path, 'configs', 'config.ini')
config.read(config_path)

BASE_URL = config.get('DEFAULT', 'base_url')
API_URL = BASE_URL + '/api/v4'
GROUP_ID = config.get('DEFAULT', 'group_id')
CUSTOM_TEMPLATE = config.get('DEFAULT', 'custom_template')
GITLAB_TOKEN = config.get('DEFAULT', 'GITLAB_TOKEN').strip('"\'')
DELETE_BRANCH = config.get('DEFAULT', 'delete_branch_after_merge').lower() == 'true'
DEVELOPER_EMAIL = config.get('DEFAULT', 'developer_email', fallback=None)
SQUASH_COMMITS = config.get('DEFAULT', 'squash_commits').lower() == 'true'
PRODUCTION_PIPELINE_NAME = config.get('DEFAULT', 'production_pipeline_name', fallback='deploy')
PRODUCTION_JOB_NAME = config.get('DEFAULT', 'production_job_name', fallback=None)
PRODUCTION_REF = config.get('DEFAULT', 'production_ref', fallback=None)
MAIN_BRANCH = 'master'
Loading