GitKV

gitkv lets you use a git repo as a key-value store using open-like semantics.

>>> import gitkv
>>> # ... test setup ...
>>> import tempfile
>>> tmpdir = tempfile.TemporaryDirectory()
>>> # The repo url can be anything that git recognizes
>>> # as a git repo
>>> repo_url = tmpdir.name  # Here it is a local path
>>> gitkv.Repo.git_init(repo_url, bare=True)
>>> # ... /test isetup ...
>>> with gitkv.open(repo_url, 'yourfile', 'w') as f:
...     f.write('Your content.')
13
>>> # When exiting the with block, a commit is created.
>>> # Later...
>>> with gitkv.open(repo_url, 'yourfile') as f:
...     f.read()
'Your content.'
>>> # Multiple reads and writes can happen within one with block,
>>> # to avoid creating multiple commits,
>>> # by using the Repo class
>>> with gitkv.Repo(repo_url) as repo:
...     with repo.open('yourfile', 'a') as f:
...         f.write('Additional content.')
...     data = repo.open('yourfile').read()
...     data = data.replace('Your', 'My')
...     with repo.open('yourfile', 'w') as f:
...         f.write(data)
...     with repo.open('anotherfile', 'w') as f:
...         f.write('something')
...     # The commit message may be specified before the block closes
...     repo.commit_message = 'Multiple edits'
19
30
9
>>> with gitkv.open(repo_url, 'yourfile') as f:
...     f.read()
'My content.Additional content.'
>>> # A repo's history is accessible with the git_log() function
>>> repo = gitkv.Repo(repo_url)
>>> [repo.message(c).strip() for c in repo.git_log()]
['Multiple edits', 'GitKV: yourfile', 'GitKV: initial commit']
>>> # Which can be called on a file to get its specific history
>>> with gitkv.open(repo_url, 'anotherfile') as f:
...     [f.repo.message(c).strip() for c in f.git_log()]
['Multiple edits']
>>> # show the plain content of a file in a commit
>>> with gitkv.open(repo_url, 'yourfile') as f:
...     commit = [c for c in f.git_log()
...                     if f.repo.message(c).strip() == 'GitKV: yourfile'][0]
...     f.show_blob(commit)
'Your content.'
class gitkv.open(url, filename, *args, **kwargs)

Open a file in a repository.

It is usually instanciated as a context manager.

This method clones the repo in a local temporary directory.

When close() is called on the returned object (e.g. when one exits from the with block), an automatic commit is added to our clone, and is then pushed to the repo at url.

__init__(url, filename, *args, **kwargs)

Return the file-like object

Parameters:
  • url – git repository where you want to open a file. It can be anything that is accepted by git, such as a relative or absolute path, a http or https url, or a user@host:repo type url, etc. See Repo.__init__()
  • filename – the file you want to open
  • kwargs (args) – all other arguments are passed as is to the ‘true’ open function
Returns:

a stream-like object

close()

Close our FileInRepo instance and our Repo instance.

see Repo.close() and FileInRepo.close()

class gitkv.Repo(url=None)

A git repository.

It is usually instanciated as a context manager.

The provided repo is cloned. Upon the exiting the context, a commit is created and pushed to the original repo.

An PushError exception will be raised when gitkv can’t push on the remote because of a conflict.

Some calls can be made within the context of the cloned repo, with automatic module importing:

>>> import gitkv
>>> with gitkv.Repo() as repo:
...     # Instead of
...     # import os
...     # os.makedirs(repo.path+'example/')
...     # one can write:
...     repo.os.makedirs('example/')
...     # it works as expected
...     import os
...     os.path.exists(repo.path+'example/')
...     # one could have written:
...     repo.os.path.exists('example/')
...
True
True

The lookup is dynamic, any call that is not understood by Repo directly will lead to a call with the Repo‘s path attribute prepended to the first argument. Anything between the Repo and the function is interpreted as a module name.

__init__(url=None)

Return the context manager.

Parameters:url – git repository where you want to open a file. It can be anything that is accepted by git, such as a relative or absolute path, a http or https url, or a user@host:repo type url, etc.

If url is a directory with a non bare git repo in it, please configure your git repository beforehand:

git config receive.denyCurrentBranch ignore

You will be able to checkout your changes with:

git checkout -f

If url is None, an empty git repository is created in a temporary directory.

static git_clone(url, path)

Clone the remote repo at url in path.

git_commit(message=None)

Create a commit.

static git_init(path, bare=False)

“Initialize an empty repo at path.

git_log(*options, custom_filter=<function Repo.<lambda>>)

Return the list of commits in reverse chronological order

Parameters:
  • options – String array, will be passed as arguments to git log
  • custom_filter – func, optional, filter commits according to an arbitrary criterion
Returns:

list of commits,

git_pull()

Pull from remote repository.

git_push()

Push to remote repository.

initial_commit_if_empty()

Commit an empty .gitignore file if the given repo is empty

is_empty()

Return True self is an empty repo

list_files(id_commit='HEAD')

List all files in repo in a commit.

message(commit='HEAD')

Return the commit message of the given commit.

open(filename, *args, **kwargs)

Open a file in this Repo

Param:filename: the file you want to open.
Param:*args, **kwargs: all other arguments are passed as is to the ‘true’ open function.
Returns:a stream-like object
remote_sync()

Create a commit of our changes and push it to the remote repo.

class gitkv.FileInRepo(filename, repo, *args, **kwargs)

Manage a file in a git repository.

Some calls can be made within the context of the FileInRepo object, with automatic module importing:

For example with json module:

>>> # ... test setup ...
>>> # For exemple, a content type json as content_json
>>> import json
>>> content_json = json.loads('{"success" : "ok"}')
>>> # ... /test setup ...
>>>
>>> # call functions of module json:
>>> import gitkv
>>> with gitkv.Repo() as repository:
...     # call function dump of json
...     with repository.open('jsonfile', mode='w') as jsonfile:
...         jsonfile.json.dump(content_json)
...     # call function loads of json
...     with repository.open('jsonfile', mode='r') as jsonfile:
...         result_json_load = jsonfile.json.load()
>>> result_json_load['success']
'ok'

This mechanism works with any module, such as the csv module, for example.

__init__(filename, repo, *args, **kwargs)

Return the context manager.

This class should be instanciated with py:func:gitkv.open or py:func:Repo.open, and not directly.

close()

Close the stream object

git_commit(message=None)

Create a commit

git_log(*options)

Return a list of all commits that modified this instance’s file, sorted from most recent to most ancient.

Parameters:options – String array, will be passed as arguments to git log
Returns:list of commits.
>>> # ... test setup ...
>>> import tempfile
>>> import gitkv
>>> import time
>>> tmpdir = tempfile.TemporaryDirectory()
>>> # The repo url can be anything that git recognizes
>>> # as a git repo
>>> repo_url = tmpdir.name  # Here it is a local path
>>> gitkv.Repo.git_init(repo_url, bare=True)
>>> # Simulate a conflict with 2 clones
>>> repo_a = gitkv.Repo(repo_url)
>>> with repo_a.open('myfile','w') as f:
...     f.write('Create myfile')
13
>>> repo_a.git_commit('Create myfile')
>>> repo_a.remote_sync()
>>> repo_b = gitkv.Repo(repo_url)
>>> with repo_b.open('myfile','w') as f:
...     f.write('\nB write')
8
>>> repo_b.git_commit('B write')
>>> with repo_b.open('otherfile','w') as f:
...     f.write('Create otherfile')
16
>>> repo_b.git_commit('Create otherfile')
>>> repo_b.remote_sync()
>>> with repo_a.open('myfile','w') as f:
...     f.write('\nA write')
8
>>> time.sleep(1)
>>> repo_a.git_commit('A write')
>>> # Error because a conflict
>>> repo_a.remote_sync()
Traceback (most recent call last):
...
gitkv.PushError: Conflict when pushing
>>> # Resolve this conflict
>>> with repo_a.open('myfile','w') as f:
...     f.write('Create myfile\nA write\nB write')
29
>>> repo_a.git_commit('Merge')
>>> repo_a.remote_sync()
>>> # ... /test setup ...
>>> # A commit history like this
>>> # 0 <- Master   Merge
>>> # |__
>>> # 0  |          A write
>>> # |  0          Create otherfile
>>> # |  |
>>> # |  0          B write
>>> # |__|
>>> #    0          Create myfile
>>> #    |
>>> #    0          GitKV: Initial commit
>>> # Call git log of myfile
>>> with gitkv.open(repo_url,'myfile') as f:
...     [f.repo.message(c).strip() for c in f.git_log('--date-order')]
['Merge', 'A write', 'B write', 'Create myfile']
show_blob(commit='HEAD')

Return the contents of self at a commit

Parameters:id_commit – type str, commit hex.
Returns:this file’s data
>>> import gitkv
>>> repo = gitkv.Repo()
>>> repo.os.makedirs('dossier')
>>> with repo.open('dossier/afile', 'w') as f:
...     f.write('Initial')
7
>>> repo.remote_sync()
>>> with repo.open('dossier/afile', 'w') as f:
...     f.write('Edit')
4
>>> repo.remote_sync()
>>> with repo.open('dossier/afile') as f:
...     for cid in f.git_log():
...         print(f.show_blob(cid))
Edit
Initial
exception gitkv.PushError

Raised when gitkv can’t push in a remote repository because a conflict.

Installation

# pip3 install gitkv