Package: git-adapter
Git-Adapter: A Python interface to the Git command line.[[TOC]]OverviewGit-Adapterprovides Python bindings to the Git command line (and also
to other command-line applications).
It is useful when you know exactly what Git commands you would call from
the shell prompt or in a shell script.Calling agitcommand returns astrgenerator representing the lines
of output (at least in the default, greedy API, see below).Note that ingitcommands and command-line flags, hyphens are replaced
by underscores.
Also, Python reserved keywords have to be escaped with a trailing
underscore.
E.g.git.cherry_pick(commit)forlineingit.grep("keyword",ignore_case=True,break_=True):print(line)corresponds togitcherry-pick$commitgitgrep--ignore-case--breakkeywordin the shell.Examplegit=Git.clone_repo("git@git.host:test-repo")log_lines:Iterator[str]=git.log(first_parent=True,max_count=20,author="me")# Process output from a git command:forfile_ingit.ls_files():print("Git file:",file_)withopen("greeting.txt","w")asfh:fh.write("Hello world\n")git.commit("greeting.txt",m="Greet the world.",author="me")origin=git.remote().first_line()branch=git.current_branch()# not an original git commandgit.push(origin,branch)# Python reserved keywords have to be escaped with a trailing# underscore:matches=git.grep("keyword",break_=True,heading=True)# Catch errorstry:git.fetch("non-existing-origin").first_line()exceptCmdErrorase:print("Got error{}when running{}".format(e.returncode,e.cmd_line))print("Error message:\n{}".format("\n".join(e.error_lines)))TheGitclassTheGitclass has a few specific commands, likegit.hard_reset()orgit.current_branch(); any other method call 'git.<CMD>(…)' is converted
to a system call 'git CMD …', e.g.fromgit_adapter.gitimportGitgit=Git(".")# Create a (greedy) Git objectgit.log()↦gitlogfiles:Iterator[str]=git.ls_files()↦gitls-filesgit.commit(m="T'was brillig.",author="me")↦gitcommit-m"T'was brillig."--author=melog_lines:Iterator[str]=git.log(first_parent=True,max_count=20,author="me")↦gitlog--first-parent--max-count=20--author=megit.worktree.add(PATH,BRANCH)↦gitworktreeaddPATHBRANCHThere are actually two versions of the API, which we call greedy and a lazy.A greedy command immediately runs and consumes all its output before
the call finishes.Constructing a command runs it until the process exits.The Git object returned from the constructor is an iterator over the
lines of output from the command.A lazy command is initiated at construction time and immediately starts
to relay any output it receives.Constructing a command starts to run it, but stops after the first
line of output. To run it to the end, one has to call itsrun()method.Thelines()method provides an iterator over the lines of output
from the command.The examples above were written for the greedy API.
In the lazy API, the last set of examples becomes:fromgit_adapter.gitimportGitgit=Git(".").lazy# Create a LazyGit objectgit.log().run()↦gitlogfiles:Iterator[str]=git.ls_files().lines()↦gitls-filesgit.commit(m="T'was brillig.",author="me").run()↦gitcommit-m"T'was brillig."--author=melog_lines:Iterator[str]=git.log(first_parent=True,max_count=20,author="me").lines()↦gitlog--first-parent--max-count=20--author=megit.worktree.add(PATH,BRANCH).run()↦gitworktreeaddPATHBRANCHAs one can see, translating between the two APIs is straight-forward:GreedyLazygreedy_git = Git(*args, **kwargs)lazy_git = Git(*args, **kwargs).lazygreedy_git.command(*args, **kwargs)lazy_git.command(*args, **kwargs).run()for line in greedy_git.command(*args, **kwargs):for line in lazy_git.command(*args, **kwargs).lines():greedy_git.command(*args, **kwargs).first_line()lazy_git.command(*args, **kwargs).first_line()So, iterating over the output from a git is written asfiles=list(greedy_git.ls_files())files=list(lazy_git.ls_files().lines())For git commands that produce a lot of output, the greedy idiomgit=Git(sandbox=".")# greedy APIforlineingit.rev_list(objects=True,all=True).lines():# It may take a long time ere we start processing the first lineif"Makefile"inline:print(line,end="")break# has already consumed all lines of outputwaits until all output has been read, while the lazy idiomgit=Git(sandbox=".").lazy# lazy APIforlineingit.rev_list(objects=True,all=True).lines():# Starts immediately when the first line of output arrivesif"Makefile"inline:print(line,end="")break# interrupts the commandcan process the output immediately and abort the git command if needed.The syntax for retrieving just the first line of output looks the same for
both APIs:head=git.rev_parse("HEAD").first_line()The difference being that in the greedy API, the whole output is read
before its first line is returned, while the lazy API does only the work
needed.Both APIs can be used together:git=Git(sandbox=".")# We don't expect these commands to produce much output, so run them greedily:git.fetch(all=True)git.commit(m="T'was brillig.",author="me")# This, however, may generate lots of output and we want immediate results:forlineingit.lazy.rev_list(objects=True,all=True).lines():if"Makefile"inline:print(line,end="")break# interrupts the command# Another short command where it shouldn't matter much which API we use:git.merge("@{u}")Note that in the command name and in thekwargs, underscores are mapped to
hyphens, so there currently is no way to callgit strange_command --strange_option, because git_adapter would try to
callgit strange-command --strange-optioninstead.TheCommandclassTheCommandclass can be used pretty much likeGitfor other shell
commands:Command(["touch"])("file1")↦run'touch file1'ip=Command(["ip"])all:Iterator[str]=ip.address.show()↦ipaddressshowlo:Iteratore[str]=ip.address.show("dev","lo")↦ipaddressshowdevlolo=ip.address.show.dev("lo")↦dittolo=ip.address.show.dev.lo()↦dittoCommand(["git"]).fetch(all=True)↦run'git fetch --all'orCommand(["touch"])("file1").lazy.run()↦run'touch file1'ip=Command(["ip"]).lazyall=ip.address.show().lines()↦ipaddressshowlo=ip.address.show("dev","lo").lines()↦ipaddressshowdevlolo=ip.address.show.dev("lo").lines()↦dittolo=ip.address.show.dev.lo().lines()↦dittoCommand(["git"]).lazy.fetch(all=True).run()↦run'git fetch --all'Again, the result provides a generator ofstrrepresenting the lines of
output from the command.LoggingWhen usingGit-Adapterfrom long-running scripts, the user may want to
get output from commands without long delays, but also all output being
logged to a file.
This can be achieved with theFileLoggerclass inGit-Adapter:fromgit_adapter.gitimportGit,FileLogger,run_cmd# Run a long-running shell command, refreshing output each time a line is# printed:run_cmd(["sh","-c","for i in $(seq 5); do echo $i; sleep 1; done"],verbose=True)# The same, but also log to a file:run_cmd(["sh","-c","for i in $(seq 5); do echo $i; sleep 1; done"],logger=FileLogger(log_dir=".",log_file="cmd.log"))# Run a long-running git command, refreshing output each time a line is# printed:git=Git(".",verbose=True)importtime,sysforrefingit.ls_remote():time.sleep(0.01)print(".",end="")sys.stdout.flush()print()# The same, but also log to a file:git=Git(".",FileLogger(log_dir=".",log_file="git.log"))importtime,sysforrefingit.ls_remote():time.sleep(0.01)print(".",end="")sys.stdout.flush()print()If you cannot open the log file when you create theGitobject (e.g.
because you want to log to the directory thatgit clonewill create), you
can use theset_logger()method later:sandbox="./sandbox"git=Git.clone_repo(repository_url,sandbox=sandbox)git.set_logger(FileLogger(sandbox,"git.log"))AlternativesThere are a number of ways to rungitcommands directly from Python:Usingsubprocess:importsubprocesssubprocess.check_call(["git","rev-parse","HEAD"])subprocess.check_call(["git","ls-files"])UsingGitPython:fromgitimportRepogit=Repo(".").gitgit.rev_parse('HEAD')git.ls_files()Usingsh(as recommended inthis post):importshgit=sh.gitprint(git("rev-parse","HEAD"))print(git("ls-files"))Git-Adapterdiffers from these solutions inOutput handlingLoggingRequirementsGit-Adaptercurrently requires Python 3.6.9 or later.