While building an AI agent with Git capabilities, I found myself repeating the same logic: checking whether the agent’s working directory was a Git repository before attempting any Git operation. This check appeared inside every Git-related tool that I made available to the agent.

For example, if I wanted to commit a Git message I would have written something like this:


def commit_git_message(working_directory, directory=None, message):
    """
    This function checks if the given directory is a Git repo and executes a git commit operation
    """

    # get the absolute path of the given directory
    abs_working_dir = Path(working_directory).resolve()

    if directory:
        target_dir = abs_working_dir / directory
    else:
        target_dir = abs_working_dir

    # check if target directory is inside the working directory
    if not target_dir.is_relative_to(abs_working_dir):
        return f'Error: Cannot work on "{target_dir}" as it is outside the permitted working directory'

    # check if the working directory is indeed a directory
    if not target_dir.is_dir():
        return f'Error: "{target_dir}" is not a directory'

    try:
		# the following command returns "true" if the directory is a Git repo or inside a Git repo
        check_git_dir = subprocess.run(
            ["git", "rev-parse", "--is-inside-work-tree"],
            cwd=str(target_dir),
            capture_output=True,
            check=True,
        )
        if check_git_dir.returncode != 0:
            return f'Error: "{target_dir}" is not a Git directory'
    except subprocess.CalledProcessError as e:
        return str(e)
	### test of the logic

Now imagine this logic repeated inside every Git tool like some boilerplate code. Not very DRY.

Hopefully the example above makes sense, and you can already see why wrapping the logic in a helper function is a better design choice. It’s cleaner and the code becomes far more maintainable.

So I thought of how I could do this.

Now, the descriptive messages returned in the code above, whether success or failure, matter because they serve as guardrails for the agent when carrying out tasks. AI agents rely heavily on feedback messages to understand the situation and adjust their next actions accordingly. Even more importantly, those messages are crucial for safety and control. They prevent the agent from making assumptions and continuing down a wrong path.

With this in mind, I knew I didn’t want to return just None in cases where the working directory wasn’t a Git repo or some other thing happened.

Go’s approach: the comma-ok idiom

In Go, I would just use the comma-ok pattern. This means returning the message and a boolean. It helps that Go natively supports multiple return values. It’s also simpler and less verbose to handle compared to returning a string and an error type. Something like this:

func checkGitCommand(directory string) error {
    cmd := exec.Command("git", "rev-parse", "--is-inside-work-tree")
    cmd.Dir = directory
    err := cmd.Run()
    if err != nil {
        return fmt.Errorf("%q is not a git directory: %w", directory, err)
    }
    return nil
}

func checkGitRepo(directory string) (string, bool) {
    absWorkingDir, err := filepath.Abs(directory)
    if err != nil {
        return fmt.Sprintf("error: failed to resolve absolute path: %v", err), false
    }

    targetDir := absWorkingDir
    info, err := os.Stat(targetDir)
    if err != nil {
        if os.IsNotExist(err) {
            return fmt.Sprintf("directory %q does not exist", targetDir), false
        }
        return fmt.Sprintf("failed to check directory %q: %v", targetDir, err), false
    }

    if !info.IsDir() {
        return fmt.Sprintf("Error: %q is not a directory", targetDir), false
    }

    if err := checkGitCommand(targetDir); err != nil {
        return fmt.Sprintf("Error: %q is not a Git repository", targetDir), false
    }

    return fmt.Sprintf("Success: %q is a valid Git repository", targetDir), true
}

You would then call like so:

func main() {
    // some logic...
    directory := "/path/to/directory"

    message, ok := checkGitRepo(directory)
    if !ok {
        fmt.Println(message)
        os.Exit(1)
    }
    // do something else

Python Approaches Compared

With Python, I had one of three options:

  1. I could mirror the Go approach and return a tuple containing a string and a boolean
  2. Use a dataclass for more structure
  3. Raise and handle exceptions

Let’s take it one after the other.

1. Returning a tuple

Python does not natively support multiple return values like Go, but you can return a tuple containing your desired values and unpack them at the point of usage. It’s simple enough and shares the same syntax with the Go approach. Although one could argue it has some negligible overhead compared to Go.

def check_git_repo(directory: str) -> tuple[str, bool]:
    abs_working_dir = Path(directory).resolve()
    target_dir = abs_working_dir

    if not target_dir.is_dir():
        return f'Error: "{target_dir}" is not a directory', False

    try:
        check_git_dir = subprocess.run(
            ["git", "rev-parse", "--is-inside-work-tree"],
            cwd=str(target_dir),
            capture_output=True,
            check=True,
        )
        if check_git_dir.returncode != 0:
            return f'Error: "{target_dir}" is not a git directory', False
    except subprocess.CalledProcessError as e:
        return f'Error: {str(e)}', False

    return f'Success: "{target_dir}" is a Git directory', True

At the point of usage, you just do:

message, is_git = check_git_repo("/path") # Tuple unpacking
if is_git:
    # do something...

In our case, that will be:

def commit_git_message(working_directory, directory=None, message):
    """
    This function checks if the given directory is a Git repo and executes a git commit operation
    """
    # get the absolute path of the given directory
    abs_working_dir = Path(working_directory).resolve()

    if directory:
        target_dir = abs_working_dir / directory
    else:
        target_dir = abs_working_dir

    # check if target directory is inside the working directory
    if not target_dir.is_relative_to(abs_working_dir):
        return f'Error: Cannot work on "{target_dir}" as it is outside the permitted working directory'

    # Use the helper
    message, is_git = check_git_repo(str(target_dir))
    if not is_git:
        return message

    try:
        result = subprocess.run(
            ["git", "commit", "-m", message],
            cwd=str(target_dir),
            capture_output=True,
        )
        if result.returncode != 0:
            return f'Error: {result.stderr.decode("utf-8")}'

    except subprocess.CalledProcessError as e:
        print(str(e))
        return f'Error: {e.stderr.decode("utf-8")}'

    return f'Commit successful: "{message}"'

Simple unpacking. Works well for a simple use case like mine.

2. Returning a dataclass

This is the more verbose but structured approach. Useful if you want to include some additional metadata for more complex applications.

@dataclass
class GitRepoData:
    success: bool
    message: str
    path: str
    error_code: str | None = None

    @property
    def is_ok(self) -> bool:
        return self.success


def check_git_repo(directory) -> GitRepoData:
    abs_working_dir = Path(directory).resolve()
    target_dir = abs_working_dir

    if not target_dir.is_dir():
        return GitRepoData(
            success=False,
            message=f'"{target_dir}" is not a directory',
            path=str(target_dir),
        )

    try:
        check_git_dir = subprocess.run(
            ["git", "rev-parse", "--is-inside-work-tree"],
            cwd=str(target_dir),
            capture_output=True,
            check=True,
        )
        if check_git_dir.returncode != 0:
            return GitRepoData(
                success=False,
                message=f'"{target_dir}" is not a git directory',
                path=str(target_dir),
            )
    except subprocess.CalledProcessError as e:
        return GitRepoData(
            success=False,
            message=str(e),
            path=str(target_dir),
        )

    return GitRepoData(
        success=True, message=f'"{target_dir}" is a Git directory', path=str(target_dir)
    )

You would then use this as follows:

result = check_git_repo(directory)
if result.is_ok():
	# do something

The idea of using the dot operator makes this approach slightly more readable but the implementation is a bit more verbose.

3. Exceptions approach

I could also raise different specific exceptions for different errors and then do some error handling when I want to call the helper function. This, I would argue, seems like the most “pythonic” way to do this.

class GitRepoError(Exception):
    def __init__(self, message: str, path: str, error_code: str | None = None):
        self.message = message
        self.path = path
        self.error_code = error_code
        super().__init__(message)

class NotDirectoryError(GitRepoError):
    pass

class NotGitRepoError(GitRepoError):
    pass

class GitCommandError(GitRepoError):
    pass


def check_git_repo(directory) -> str:
    abs_working_dir = Path(directory).resolve()
    target_dir = abs_working_dir

    if not target_dir.is_dir():
        raise NotDirectoryError(
            f'"{target_dir}" is not a directory', str(target_dir), "NOT_DIRECTORY"
        )

    try:
        check_git_dir = subprocess.run(
            ["git", "rev-parse", "--is-inside-work-tree"],
            cwd=str(target_dir),
            capture_output=True,
            check=True,
        )
        if check_git_dir.returncode != 0:
            raise NotGitRepoError(
                f'"{target_dir}" is not a Git directory',
                str(target_dir),
                "NOT_GIT_REPO",
            )
    except subprocess.CalledProcessError as e:
        raise GitCommandError(str(e), str(target_dir), "GIT_COMMAND_FAILED")

    return f'"{target_dir}" is a git directory'

You would then use this as follows:

#...
try:
    message = check_git_repo(directory)
    # do some git operations
except NotDirectoryError:
    # Maybe create directory?
except NotGitRepoError:
    # Maybe initialize git repo?
except GitCommandError:
    # Handle git command failure
except GitRepoError:
    # Handle other errors

The last approach would be a no-brainer if I was writing a library for other developers. It would make sense because of the specific error handling. But that’s not what I am doing. I only want to check if a directory is a Git repo but with an accompanying status message.

As you may have guessed, I went with the tuple approach as it was the simplest and made the most sense for my use case.

Reminds me of the KISS principle

KISS is an acronym for “Keep It Simple, Stupid.” It is a design philosophy that suggests that complex code should be avoided in favor of simple, well-documented code.

In the past, I would most likely have gone for the most sophisticated implementation, even when it offered no concrete benefit. But growing as a software engineer means understanding that the best code isn’t always the most complex-looking. Rather, it’s the solution that solves your problem clearly and simply.

I now know that every line of code is a potential liability. In my use case, that meant using the tuple approach. For you, it may mean taking the exceptions route.