import io, tempfile
from contextlib import redirect_stdout
from fastcore.test import test_eq,expect_failAPI
Imports
callgit
def callgit(
path, *args, uname:NoneType=None, pre:NoneType=None
):Run git in path, returning stripped stdout+stderr as a single str
callgit returns git’s stdout and stderr combined as a single str, just as a terminal shows them. Call .splitlines() on the result when you need line-oriented output.
with tempfile.TemporaryDirectory() as td:
msg = callgit(td, 'init', '-b', 'main')
assert 'Initialized' in msg
test_eq(callgit(td, 'rev-parse', '--git-dir'), '.git')
assert '\n' in callgit(td, 'status')get_top
def get_top(
folder
):Call self as a function.
with tempfile.TemporaryDirectory() as td:
test_eq(get_top(td), None)
msg = callgit(td, 'init', '-b', 'main')
assert get_top(td) in msgGit represents a python wrapper for the git command. It will execute every command as if git were being run with the working directory being the directory d passed in at init time.
As a result, callers should take care not to checkout a branch which does not have the directory used to initialize Git.
Pass pre to prefix every git command with additional arguments, e.g. a sudo invocation to run as another user.
Keyword arguments become git options: single-letter names are short options (n=1 → -n 1) and longer names are long options (pretty='format:%s' → --pretty=format:%s), with underscores converted to dashes. True passes the flag alone, and False omits it. Use the __ parameter to pass path arguments after --.
Failed commands print the error and return None. Pass mute_errors=True to silence the message, or raise_exc=True (per call, or at init to make it the instance default) to raise the CalledProcessError instead.
Git
def Git(
d, pre:NoneType=None, raise_exc:bool=False
):Run git commands in dir d
with tempfile.TemporaryDirectory() as td:
g = Git(td)
assert not g.exists
msg = g.init(b='main')
assert g.top() in msgOptions compose naturally: values like n=1 are stringified, and False omits the option entirely:
with tempfile.TemporaryDirectory() as td:
g = Git(td)
g.init(b='main')
(g.d/'a.txt').write_text('a')
g.add('a.txt'); g.commit(m='first')
test_eq(g.log(n=1, format='%s'), 'first')
test_eq(g.log(n=1, format='%s', p=False), 'first')Errors print as a terse one-line message (like git itself) and return None by default, which suits interactive and quick-tooling use. Library code that consumes a command’s output should pass raise_exc (at init, or per call) so failures raise instead of returning None:
with tempfile.TemporaryDirectory() as td:
test_eq(Git(td).log(mute_errors=True), None)
s = io.StringIO()
with redirect_stdout(s): Git(td).log()
test_eq(s.getvalue().count('\n'), 1) # errors print as one terse git-style line
assert s.getvalue().startswith('ERROR: git log: fatal:')
with expect_fail(CalledProcessError): Git(td, raise_exc=True).log()
with expect_fail(CalledProcessError): Git(td).log(raise_exc=True)Git.last_commit
def last_commit():Call self as a function.
Git.commits
def commits():Call self as a function.
A repo with no commits yet has an empty commits list:
with tempfile.TemporaryDirectory() as td:
g = Git(td)
g.init(b='main')
test_eq(g.commits, [])Git.current_branch
def current_branch():Call self as a function.
current_branch returns the currently checked-out branch. Note that git writes checkout’s status messages to stderr, which fastgit includes in the returned output just as a terminal would show them:
with tempfile.TemporaryDirectory() as td:
g = Git(td)
g.init(b='main')
test_eq(g.current_branch, 'main')
assert 'Switched' in g.checkout('-b', 'tmp')
test_eq(g.current_branch, 'tmp')with tempfile.TemporaryDirectory() as td:
g = Git(td, pre=['env'])
g.init()
(Path(td)/'a.txt').write_text('a')
g.add('a.txt')
g.commit(m='first')
test_eq(len(g.commits), 1)
assert 'first' in g.commits[0]
with expect_fail(Exception, 'not both'): callgit(td, 'status', uname='x', pre=['env'])