Magiccommit: Effortless Git Commits with AI
import click
from magiccommits.src.commands.config import config
from magiccommits.src.commands.mc import mc

#local import
from magiccommits.src.exception.error_handler import handleError
@click.group(invoke_without_command=True)
@click.option('-t','--ticket',help='Set the ticket number')
@click.option('-a','--add',is_flag=True, flag_value=True,help='Perform [git add .] operation')
@click.option('-u','--update',is_flag=True, flag_value=True,help='Perform [git add --update] operation')
@click.version_option(message="Magiccommits v1.0-dev" ,help='Current version of magiccommit')
@click.pass_context
@handleError
def cli(ctx,ticket,add,update):
    """This is the main command group."""
    mc(ctx=ctx,ticket=ticket,add=add,update=update)


cli.add_command(config)
magiccommits/main.py
prakash-O4/magiccommits
import click

#local import
from magiccommits.src.exception.error_handler import handleError
from magiccommits.src.utils.config.config_helper import set_configs, get_config
from magiccommits.src.utils.style import format_response
@click.command('config', short_help='Configuration for magiccommit')
@click.argument('mode',type=click.Choice(['get','set']))
@click.argument('key_value',nargs=-1)
@handleError
def config(mode,key_value):
    """Set your configuration value for the magic commit [Learn More]"""
    if(mode == 'get'):
        all_value=False
        if(len(key_value)==0):
            all_value=True
        config_value =  get_config(key_value,internal=all_value)
        format_response(config_value.all())
    elif(mode == 'set'): 
        value = []
        if len(key_value) == 0:
            click.secho("Enter value in mc config set key=value",fg='red')
        else:
            for kv in key_value:
                splitted_text = str(kv).strip().split("=")
                if(len(splitted_text) > 1):
                    value.append((splitted_text[0],splitted_text[1]))
                    set_configs(value)
                else:
                    click.secho("The key must be in the format key=value.",fg='red')
                    break
magiccommits/src/commands/config.py
prakash-O4/magiccommits
import click

#local import
from magiccommits.src.utils.config.config_helper import  get_config
from magiccommits.src.utils.custom.loading import Loading
from magiccommits.src.utils.git import (
    assert_git_repo,
    get_detected_message,
    get_staged_diff,
    stage_change,
)
from magiccommits.src.utils.style import multiple_answers
from magiccommits.src.utils.open_ai_commit import generate_commit_message

@click.option('-t','--ticket',help='Set the ticket number')
@click.option('-a','--add',is_flag=True, flag_value=True,help='Perform git add . operation')
def mc(ctx,ticket,add,update):
    if(ctx.invoked_subcommand is None):
        loading = Loading('⌛ Generating Commits.. ')
        try:
            # check if git is initialized or not
            assert_git_repo()
            # get the git diff of the current project
            if(add):
                stage_change(True)
            elif(update):
                stage_change(False)
            diff =  get_staged_diff()
            if(diff is None):
                click.secho('No stage files detected.',fg='red')
            else:
                click.secho(get_detected_message(diff['files']),fg='green')
                # get all the value from the config file i.e .mc file in the '~' directory
                config = get_config(internal=True)
                loading.start()
                # generate commit
                commit = generate_commit_message(config.OPENAI_KEY,config.model,config.locale,diff['diff'],config.generate,config.max_length,config.type,config.max_token,config.timeout)
                loading.stop()
                multiple_answers(commit_message=commit,ticket=ticket,copy_commit=config.copy_commit)
        except KeyboardInterrupt as e:
            loading.stop(is_forced=True)
            raise e
        except Exception as e: 
            loading.stop(is_forced=True)
            raise e
    else:
        pass
magiccommits/src/commands/mc.py
prakash-O4/magiccommits
class KnownError(Exception):
    pass

class NetworkError(Exception):
    def __init__(self, error_dict):
        self.error = error_dict

class UnKnownError(Exception):

    def __init__(self,message):
        self.message = message

def error(message: str):
    raise KnownError(message)
magiccommits/src/exception/error.py
prakash-O4/magiccommits
import click
from magiccommits.src.exception.error import KnownError, NetworkError


def handleError(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except KnownError as e:
            click.secho(click.style(f'❌ {e}', fg='red'))
            click.get_current_context().exit(code=1)
        except NetworkError as e:
            click.secho(e.error.get('message'),fg='red')
            click.get_current_context().exit(code=1)
        except KeyboardInterrupt as e:
            click.secho("Exit code 1",fg='red')
            click.get_current_context().exit(code=1)
        except Exception as e:
            click.secho("Oops, we got an untracked error, Please report it to https://github.com/prakash-O4/magiccommits/issues.",fg='red')
            click.get_current_context().exit(code=1)
    return wrapper

magiccommits/src/exception/error_handler.py
prakash-O4/magiccommits
import os
import configparser
from typing import Dict, List, Tuple

from magiccommits.src.exception.error import error
from magiccommits.src.utils.config.config_parser import ConfigParser
from magiccommits.src.utils.config.config_response import ConfigResponse


CONFIG_SECTION = "CONFIG"


# Mocking the 'fileExists' and 'error' functions
def fileExists(path: str) -> bool:
    return os.path.exists(path)


config_parsers = {
    'OPENAI_KEY': ConfigParser.parse_OPENAI_KEY,
    'locale': ConfigParser.parse_locale,
    'generate': ConfigParser.parse_generate,
    'type': ConfigParser.parse_type,
    'proxy': ConfigParser.parse_proxy,
    'model': ConfigParser.parse_model,
    'timeout': ConfigParser.parse_timeout,
    'max_length': ConfigParser.parse_max_length,
    'max_token': ConfigParser.parse_max_token,
    'copy_commit': ConfigParser.parse_copy_commit,
}

# Mocking 'configPath'
config_path = os.path.join(os.path.expanduser("~"), '.mc')

# Mocking 'readConfigFile' function
def read_config_file() -> Dict[str, str]:
    config_exists = fileExists(config_path)
    config_value = {}
    if not config_exists:
        return config_value
    config_parser = configparser.ConfigParser()
    config_parser.optionxform = str
    config_parser.read(config_path)

    for section in config_parser.sections():
        config_value = dict(config_parser[section])
    return config_value

def get_config(cli_config: Tuple = None, internal: bool = False) -> ConfigResponse:
    config = read_config_file()
    parsed_config = {}

    if(internal):
        for key in config_parsers.keys():
            value = config.get(key)
            parsed_config[key] = True if value == 'True' else False if value == 'False' else config_parsers[key](value)
    else:
        for key in config_parsers.keys():
            parser = config_parsers[key]
            value = None
            if cli_config:
                if key in cli_config:
                    value = config.get(key)
                    parsed_config[key] = True if value == 'True' else False if value == 'False' else parser(value)
    
        for c_config in cli_config:
            if c_config not in parsed_config:
                raise error(f'{c_config.capitalize()} not found.')
    return ConfigResponse(parsed_config)
    

# Mocking 'setConfigs' function
def set_configs(key_values: List[Tuple[str, str]]):
    config = read_config_file()
    
    for key, value in key_values:
        if key not in config_parsers:
            error(f'Invalid config property: {key}')
        parsed = config_parsers[key](value)
        config[key] = parsed
    with open(config_path, 'w') as config_file:
        config_parser = configparser.ConfigParser()
        config_parser.add_section(CONFIG_SECTION)
        config_parser.optionxform = str
        for section, values in config.items():
            config_parser.set(section=CONFIG_SECTION, option=section,value=values)
        config_parser.write(config_file)



magiccommits/src/utils/config/config_helper.py
prakash-O4/magiccommits
import re
from typing import Union
from magiccommits.src.exception.error import error

class ConfigParser:

    def parse_OPENAI_KEY(key: str) -> str:
        if not key:
            error('Please set your OpenAI API key via `mc config set OPENAI_KEY=<your token>`')
        if not key.startswith('sk-'):
            error('Must start with "sk-"')
        return key

    def parse_locale(locale: str) -> str:
        if not locale:
            return 'en'
        if not re.match(r'^[a-z-]+$', locale, re.IGNORECASE):
            error('Must be a valid locale (letters and dashes/underscores). You can consult the list of codes in: https://wikipedia.org/wiki/List_of_ISO_639-1_codes')
        return locale
    

    def parse_generate(count: str) -> int:
        if not count:
            return 4
        if not re.match(r'^\d+$', count):
            error('Must be an integer')
        parsed = int(count)
        if parsed <= 0 or parsed > 5:
            error('Must be between 1 and 5')
        return parsed

    def parse_type(commit_type: str) -> str:
        if not commit_type:
            return 'conventional'
        if commit_type not in ['', 'conventional','conventional-emoji','message','message-emoji']:
            error('"Invalid commit type. Options: conventional, conventional-emoji, message, message-emoji."')
        return commit_type

    def parse_proxy(url: str) -> Union[str, None]:
        if not url or len(url) == 0:
            return 'http'
        if not re.match(r'^https?:\/\/', url):
            error('Must be a valid URL')
        return url

    def parse_model(model: str) -> str:
        if not model or len(model) == 0:
            return 'gpt-3.5-turbo'
        return model

    def parse_timeout(timeout: str) -> int:
        if not timeout:
            return 10000
        if not re.match(r'^\d+$', timeout):
            error('Must be an integer')
        parsed = int(timeout)
        if parsed < 500:
            error('Must be greater than 500ms')
        return parsed

    def parse_max_length(max_length: str) -> int:
        if not max_length:
            return 50
        if not re.match(r'^\d+$', max_length):
            error('Must be an integer')
        parsed = int(max_length)
        if parsed < 20:
            error('Must be greater than 20 characters')
        return parsed
    
    def parse_max_token(max_token: str) -> int:
        if not max_token:
            return 200
        if not re.match(r'^\d+$', max_token):
            error('Must be an integer')
        parsed = int(max_token)
        if parsed < 10:
            error('Must be greater than 10 tokens')
        return parsed
    
    def parse_copy_commit(copy_commit: str) -> str:
        if not copy_commit:
            return 'True'  
        copy_commit = str(copy_commit).lower()  
        if copy_commit == 'true':
            return 'True'
        elif copy_commit == 'false':
            return 'False'
        else:
            raise error("Input must be 'True' or 'False'")
        
magiccommits/src/utils/config/config_parser.py
prakash-O4/magiccommits
class ConfigResponse:
    def __init__(self, dictionary):
        self._dict = dictionary

    def __getattr__(self, key):
        if key in self._dict:
            return self._dict[key]
        else:
            raise AttributeError(f"Config Response object has no attribute '{key}'")
        
    def all(self):
        return self._dict
magiccommits/src/utils/config/config_response.py
prakash-O4/magiccommits
import click


def get_confirmation():
    while True:
        confirmation = click.prompt(
            "Enter [c/C] to commit or [cp/CP] to commit and push or [n/N] to cancel: ",
            default="n",
            show_default=False,
        ).lower()

        if confirmation.lower() == "c":
            return "c"
        elif confirmation.lower() == "cp":
            return "cp"
        elif confirmation.lower() == 'n':
            return "n"
        else:
            click.secho("\nInvalid input. Please enter [c/C] or [cp/CP] or [n/N].\n", fg='red')
magiccommits/src/utils/custom/confirmation_input.py
prakash-O4/magiccommits

import click

from magiccommits.src.exception.error import KnownError


class CustomIntRange(click.ParamType):
    def __init__(self, min_value, max_value):
        self.min_value = min_value
        self.max_value = max_value

    def convert(self, value, param, ctx):
        try:
            value = int(value)
            if self.min_value <= value <= self.max_value:
                return value
        except ValueError:
            pass
        raise KnownError()
magiccommits/src/utils/custom/input_validator.py
prakash-O4/magiccommits
from typing import List
import click
import readchar


class DefaultKeys:
    """List of default keybindings.

    Attributes:
        interrupt(List[str]): Keys that cause a keyboard interrupt.
        select(List[str]): Keys that trigger list element selection.
        confirm(List[str]): Keys that trigger list confirmation.
        delete(List[str]): Keys that trigger character deletion.
        down(List[str]): Keys that select the element below.
        up(List[str]): Keys that select the element above.
    """

    interrupt: List[str] = [readchar.key.CTRL_C, readchar.key.CTRL_D]
    select: List[str] = [readchar.key.SPACE]
    confirm: List[str] = [readchar.key.ENTER]
    delete: List[str] = [readchar.key.BACKSPACE]
    down: List[str] = [readchar.key.DOWN, "j"]
    up: List[str] = [readchar.key.UP, "k"]


def select(
    options,
    caption_indices=None,
    deselected_prefix="[ ] ",
    selected_prefix="[x] ",
    caption_prefix="",
    selected_index=0,
    confirm_on_select=True,
):
    print("\n" * (len(options) - 1))
    if caption_indices is None:
        caption_indices = []
    while True:
        print(f"\033[{len(options) + 1}A")
        for i, option in enumerate(options):
            if i not in caption_indices:
                line = selected_prefix + option if i == selected_index else deselected_prefix + option
                click.secho(message=line,fg='green') if i == selected_index else click.secho(message=line)
            elif i in caption_indices:
                print(f"\033[K{caption_prefix}{options[i]}")
        keypress = readchar.readkey()
        if keypress in DefaultKeys.up:
            new_index = selected_index
            while new_index > 0:
                new_index -= 1
                if new_index not in caption_indices:
                    selected_index = new_index
                    break
        elif keypress in DefaultKeys.down:
            new_index = selected_index
            while new_index < len(options) - 1:
                new_index += 1
                if new_index not in caption_indices:
                    selected_index = new_index
                    break
        elif (
            keypress in DefaultKeys.confirm
            or confirm_on_select
            and keypress in DefaultKeys.select
        ):
            break
        elif keypress in DefaultKeys.interrupt:
            raise KeyboardInterrupt
        
    return selected_index
magiccommits/src/utils/custom/interactive_terminal.py
prakash-O4/magiccommits
import time
import click
import threading

class Loading:
    def __init__(self,message) -> None:
        self.isLoading = False
        self.loading_thread = None
        self.message = 'Loading...' if message is None else message

    def start(self):
        if not self.isLoading:
            self.isLoading = True
            self.loading_thread = threading.Thread(target=self._animate_loading)
            self.loading_thread.start()

    def stop(self,is_forced: bool = False):
        if self.isLoading:
            self.isLoading = False
            if self.loading_thread:
                self.loading_thread.join()
            click.secho(" Done!",dim=True) if not is_forced else click.echo(' \n')

    def _animate_loading(self):
        click.echo(click.style(self.message, dim=True), nl=False)
        animation = "|/-\\"
        i = 0
        while self.isLoading:
            click.echo(animation[i % len(animation)], nl=False)
            time.sleep(0.1)
            click.echo("\b", nl=False)
            i += 1
magiccommits/src/utils/custom/loading.py
prakash-O4/magiccommits
import subprocess
from magiccommits.src.exception.error import error

def assert_git_repo():
    try:
        # Run the 'git rev-parse --show-toplevel' command using subprocess
        result = subprocess.run(['git', 'rev-parse', '--show-toplevel'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)

        # If the command is successful, return the top-level directory path
        return result.stdout.strip()

    except subprocess.CalledProcessError:
        # If the command failed, raise a KnownError exception
        raise error('The current directory must be a git repository!')


def get_current_branch():
    try:
        branch_name = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).decode('utf-8').strip()
        return branch_name
    except Exception:
        return None

def get_staged_diff():
    diff_cached = ['diff', '--cached', '--diff-algorithm=minimal', '--ignore-space-change']
    
    # Run the 'git diff --cached --name-only' command to get staged files
    result_files = subprocess.run(['git', *diff_cached, '--name-only'], stdout=subprocess.PIPE, text=True, check=True)
    files = result_files.stdout.strip().split('\n')
    if not files or files == ['']:
        return None

    # Run the 'git diff --cached' command to get the staged diff
    result_diff = subprocess.run(['git', *diff_cached], stdout=subprocess.PIPE, text=True, check=True)
    diff = result_diff.stdout
    return {
        'files': files,
        'diff': diff,
    }

def get_detected_message(files):
    if len(files) == 1:
        return f"💡 Detected {len(files):,} staged file"
    else:
        return f"💡 Detected {len(files):,} staged files"

def stage_change(add:bool):
    try:
        if(add):
            subprocess.run(['git', 'add', '.'], check=True)
        else:
            subprocess.run(['git', 'add', '--update'], check=True)
    except subprocess.CalledProcessError:
        raise error("Error: Couldn't stage the changes. Please try to stage them manually using 'git add'.")
   

def add_commit_message(message) -> bool:
    if message is not None:
        try:
            subprocess.run(['git','commit','-m',message],check=True)
            return True
        except Exception:
            return False
    else:
        return False
    
def push_to_origin() -> bool:
    try:
        branch_name = get_current_branch()
        if(branch_name is not None):
            subprocess.run(['git','push','origin',branch_name],check=True)
            return True
        else: return False
    except Exception:
        return False

# Run 'git status --porcelain' to get the status of the repository
# result = subprocess.run(['git', 'status', '--porcelain'], capture_output=True, text=True, check=True)

# Split the output into lines and filter staged files (status starts with "A", "M", "R", etc.)
# staged_files = [line for line in result.stdout.splitlines() if line.startswith(('A', 'M', 'R', 'C', 'D'))]

# Count the number of staged files
# number_of_staged_files = len(staged_files)

# return number_of_staged_files
magiccommits/src/utils/git.py
prakash-O4/magiccommits
import http.client
import json
import socket
from http.client import HTTPResponse

from magiccommits.src.exception.error import NetworkError
from magiccommits.src.utils.prompt import generate_prompt


def https_post(hostname, path, headers, json_data, timeout, proxy=None):
    post_content = json.dumps(json_data)
    connection = http.client.HTTPSConnection(hostname, timeout=timeout)

    if proxy:
        connection.set_tunnel(proxy)

    headers["Content-Type"] = "application/json"
    headers["Content-Length"] = str(len(post_content))

    try:
        connection.request("POST", path, post_content, headers)
        response: HTTPResponse = connection.getresponse()
        response_data = response.read()
        connection.close()
    except Exception as e:
        if not has_internet_connection():
            raise NetworkError({"message": f"Are you connected to Internet?"})
        raise NetworkError({"message": f"Error making POST request: {str(e)}"})

    return {
        "response": response,
        "data": response_data.decode("utf-8")
    }


def has_internet_connection():
    try:
        # Attempt to connect to Google's DNS server (8.8.8.8) on port 53 (DNS)
        socket.create_connection(("8.8.8.8", 53), timeout=5)
        return True  # If successful, there is internet connectivity
    except OSError:
        pass
    return False

def create_chat_completion(api_key, json_data, timeout, proxy=None):
    headers = {
        "Authorization": f"Bearer {api_key}"
    }

    result = https_post(
        "api.openai.com",
        "/v1/chat/completions",
        headers,
        json_data,
        timeout,
        proxy
    )

    response = result["response"]
    data = result["data"]
    if not response.status or response.status < 200 or response.status > 299:
        error_message = {"error_type": f"OpenAI API Error: {response.status} - {response.reason}"}
        if data:
            error_message['message'] = json.loads(data)['error']['message']
        if response.status == 500:
            error_message += "\n\nCheck the API status: https://status.openai.com"
        raise NetworkError(error_message)

    return json.loads(data)


def sanitize_message(message):
    return message.strip().replace("\n", "").replace("\r", "").rstrip(".")


def generate_commit_message(api_key, model, locale, diff, completions, max_length, type,max_token ,timeout, proxy=None):
    try:
        system_message = generate_prompt(locale, max_length, type)  # Define generate_prompt function
        messages = [
            {"role": "system", "content": system_message},
            {"role": "user", "content": diff},
        ]

        json_data = {
            "model": model,
            "messages": messages,
            "temperature": 0.7,
            "top_p": 1,
            "frequency_penalty": 0,
            "presence_penalty": 0,
            "max_tokens": max_token,
            "stream": False,
            "n": completions,
        }

        completion = create_chat_completion(api_key, json_data, timeout, proxy)
        sanitized_messages = [sanitize_message(choice["message"]["content"]) for choice in completion["choices"] if
                              choice.get("message", {}).get("content")]
        return list(set(sanitized_messages))
    
    except Exception as e:
        raise e
magiccommits/src/utils/open_ai_commit.py
prakash-O4/magiccommits
from typing import Dict

# Define a custom type for CommitType
CommitType = str

# Define commit type formats and messages in Python
commit_type_formats: Dict[CommitType, str] = {
    'conventional': '<type>(<optional scope>): <commit message>',
    'conventional-emoji': '<type>(<optional scope>): <commit message> <emoji>',
    'message': '<commit message>',
    'message-emoji': '<commit message> <emoji>',
}

# Function to specify commit format
def specify_commit_format(commit_type: CommitType) -> str:
    return f"The output response must be in format:\n{commit_type_formats[commit_type]}"

emoji_types = """Select the emoji based on the generated commit <type> from the given dictionary
{
    "docs": "📚",
    "style": "💎",
    "refactor": "📦",
    "perf": "🚀",
    "test": "🚨",
    "build": "🛠",
    "ci": "⚙️",
    "chore": "♻️",
    "revert": "🗑",
    "feat": "✨",
    "fix": "🐛"
}"""

# Define commit types and descriptions
commit_types: Dict[CommitType, str] =  """Choose a type from the type-to-description JSON below that best describes the git diff:
{
    "docs": "Documentation only changes",
    "style": "Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",
    "refactor": "A code change that neither fixes a bug nor adds a feature",
    "perf": "A code change that improves performance",
    "test": "Adding missing tests or correcting existing tests",
    "build": "Changes that affect the build system or external dependencies",
    "ci": "Changes to our CI configuration files and scripts",
    "chore": "Other changes that don't modify src or test files",
    "revert": "Reverts a previous commit",
    "feat": "A new feature",
    "fix": "A bug fix"
}"""


# Function to generate the prompt
def generate_prompt(locale: str, max_length: int, commit_type: CommitType) -> str:
    prompt_parts = [
        'Generate a concise Git commit message in the present tense based on the provided git diff. Your commit message should not be detailed technical message, it should be simple summary of the provided git diff.',
        f"Message language of all commits should be in: {locale}",
        f"Each commit message should have maximum {max_length} characters in it.",
        "Please exclude any superfluous information, such as translation details. Your entire response will be directly used as the git commit message.",
        commit_types,
        emoji_types if commit_type != "message" else "",
        specify_commit_format(commit_type)
    ]
    # Remove empty parts and join the lines
    prompt = '\n'.join(filter(None, prompt_parts))
    return prompt

#Example usage
# locale = "English"
# max_length = 80
# commit_type = "conventional"
# emoji_type = "conventional"
# prompt = generate_prompt(locale, max_length, commit_type)
#print(specify_commit_format('message-emoji'))

magiccommits/src/utils/prompt.py
prakash-O4/magiccommits
import click
import pyperclip
from magiccommits.src.exception.error import KnownError
from magiccommits.src.utils.custom.confirmation_input import get_confirmation

from magiccommits.src.utils.custom.input_validator import CustomIntRange
from magiccommits.src.utils.custom.interactive_terminal import select
from magiccommits.src.utils.git import add_commit_message, push_to_origin


def format_response(response_dict):
    for key, value in response_dict.items():
        formatted_key = click.style(key, fg='green', bold=True)
        formatted_value = click.style(str(value), fg='blue')
        click.echo(f"{formatted_key}: {formatted_value}")

def multiple_answers(commit_message, ticket, copy_commit):
    messages = commit_message
    click.echo(click.style("\n📜 Generated Commit Messages:", fg='cyan', bold=True))
    
    try:
        if ticket is not None:
            messages = [str(ticket) + ' ' + message for message in messages]

        selected_index = select(messages)
        selected_message = messages[selected_index]
        click.echo(click.style("\n🤔 Please choose one of the following actions:", fg='cyan', bold=True))

        confirmation_index = select(["Commit","Commit and Push","Cancel"])
        if confirmation_index == 0 or confirmation_index == 1:
            is_committed = add_commit_message(selected_message)
            if is_committed:
                if(confirmation_index == 1):
                    is_pushed = push_to_origin()
                if(confirmation_index == 0):
                    click.secho("✔ Successfully Committed", fg='green')
                elif (confirmation_index == 1 and is_pushed):
                    click.secho("✔ Successfully committed and pushed to the origin.", fg='green')
                elif (not is_pushed):
                    click.secho("😔 Message committed, could not be pushed to origin.", fg='red')
            else:
                click.secho("😔 Message not committed.", fg='red')
        else:
            if copy_commit:
                pyperclip.copy(selected_message)
                click.secho("📋 Message copied to clipboard", fg='green')
            else:
                click.secho("Use copy_commit=True to copy the commit ✅", fg='yellow')
    except KeyboardInterrupt:
        click.secho("Exit code 1",fg='red')
    except Exception as e:
        raise e 
magiccommits/src/utils/style.py
prakash-O4/magiccommits
test_diff = """
diff --git a/app/main.py b/app/main.py
index e5f1a12..4c8a2f7 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,5 +1,9 @@
 # MyApp - Main Application
 
 import requests
+from my_new_feature import process_new_data
 
 def fetch_data(url):
     response = requests.get(url)
     data = response.json()
     return data
 
 def main():
     data = fetch_data("https://api.example.com/data")
+    processed_data = process_new_data(data)
+    if processed_data:
+        print("New feature result:", processed_data)
+
 if __name__ == "__main__":
     main()
"""

test_diff_pref = """
diff --git a/app/processing.py b/app/processing.py
index 7c8d39f..f2a1a67 100644
--- a/app/processing.py
+++ b/app/processing.py
@@ -1,8 +1,12 @@
 # Performance Improvement in Data Processing
 
+from functools import lru_cache
 import time
 
-def process_data(data):
+@lru_cache(maxsize=None)
+def cached_process_data(data):
     # Existing data processing code
-    result = complex_data_processing(data)
+    if data in processed_data_cache:
+        return processed_data_cache[data]
+    result = complex_data_processing(data)
     return result
 
+processed_data_cache = {}
 
 def complex_data_processing(data):
     # Existing complex processing logic
@@ -14,6 +18,9 @@ def complex_data_processing(data):
     return result
 
 def main():
+    # Prepopulate cache for frequently used data
+    for data in commonly_used_data:
+        cached_process_data(data)
     start_time = time.time()
     data = load_large_dataset()
     result = process_data(data)
@@ -21,6 +28,7 @@ def main():
     end_time = time.time()
     print("Processing time: {:.2f} seconds".format(end_time - start_time))
 
+commonly_used_data = [1, 2, 3, 4, 5]  # Add frequently used data here
+
 if __name__ == "__main__":
     main()
"""
test_api_key = "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"
test_locale = "en"
test_completions = 5
test_max_length = 80
test_type = "conventional"
test_timeout = 600

test_prompt = """Generate a concise Git commit message in the present tense based on the provided git diff. 
Your commit message should not be detailed technical message, it should be simple summary of the provided git diff.
Please exclude any superfluous information, such as translation details. Your entire response will be directly used as the git commit message.

Message language: English
Maximum Commit Message Length: 80 characters

Choose a type from the type-to-description JSON below that best describes the git diff:

{
    "docs": "Documentation only changes",
    "style": "Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",
    "refactor": "A code change that neither fixes a bug nor adds a feature",
    "perf": "A code change that improves performance",
    "test": "Adding missing tests or correcting existing tests",
    "build": "Changes that affect the build system or external dependencies",
    "ci": "Changes to our CI configuration files and scripts",
    "chore": "Other changes that don't modify src or test files",
    "revert": "Reverts a previous commit",
    "feat": "A new feature",
    "fix": "A bug fix"
}

Choose a emoji from the emoji-to-description JSON below that best describes the git diff:

{
 "📚": "Documentation only changes",
    "💎": "Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",
    "📦": "A code change that neither fixes a bug nor adds a feature",
    "🚀": "A code change that improves performance",
    "🚨": "Adding missing tests or correcting existing tests",
    "🛠": "Changes that affect the build system or external dependencies",
    "⚙️": "Changes to our CI configuration files and scripts",
    "♻️": "Other changes that don't modify src or test files",
    "🗑": "Reverts a previous commit",
    "✨": "A new feature",
    "🐛": "A bug fix"
}
The output response must be in format:
<type>(<optional scope>): <commit message> <emoji>
"""

test_diff_doc = """
@@ -1,6 +1,8 @@
 # Utility Functions
 
 def calculate_average(numbers):

-    Calculate the average of a list of numbers.
+    Calculate the average of a list of numbers.
+
+    :param numbers: A list of numbers.
+    :type numbers: list of int or float
     :return: The average of the numbers.
     :rtype: float

@@ -8,7 +10,10 @@ def calculate_average(numbers):
     return sum(numbers) / len(numbers)
 
 
+def validate_email(email):
+    
+    Validate an email address.
 
-def validate_email(email):
+    :param email: The email address to validate.
+    :type email: str
     :return: True if the email is valid, False otherwise.
     :rtype: bool

"""

magiccommits/src/utils/test.py
prakash-O4/magiccommits
"""Magic Commit is a Python-based automation tool that revolutionizes the Git commit process. It intelligently analyzes code changes and generates concise commit messages, simplifying version control. Install Magic Commit from PyPI with:

pip install magiccommits

Navigate to your project directory and execute:

magiccommits or mc

Magic Commit scans your codebase, crafting meaningful commit messages by default. Experience streamlined version control effortlessly.

For in-depth usage instructions, run:

mc --help

Visit the Magic Commit repository for comprehensive documentation: https://github.com/prakash-O4/magiccommits

Developed by Prakash Basnet, Inspired from [https://github.com/Nutlope/aicommits]"""


from setuptools import setup, find_packages

with open("README.md", "r") as f:
    long_description = f.read()

setup(
    name='magiccommits',
    version='1.0-dev',
    author='Prakash Basnet',
    url="https://github.com/prakash-O4/magiccommits",
    license="MIT",
    long_description_content_type="text/markdown",
    description="Magiccommit: Effortless Git Commits with AI",
    long_description=long_description,
    keywords=["Git","Commits","Automation", "Code","Version","Control","ai commit","Commit messages"],
    include_package_data=True,
    packages=find_packages(exclude=["dist", "build", "*.egg-info"]),
    py_modules= ['magiccommits'],
    install_requires=[
        'click',
        'pyperclip',
        'readchar',
    ],
    entry_points={
        'console_scripts': [
            'magiccommits=magiccommits.main:cli',
            'mc=magiccommits.main:cli'
        ],
    },
     classifiers=[
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
        "Programming Language :: Python :: 3.11",
        "License :: OSI Approved :: MIT License",
    ],
)

print(
     """
--------------------------------
Magic Commit installation complete, Enjoy Coding!
--------------------------------
"""
)
setup.py
prakash-O4/magiccommits