Skip to main content

Git-based version information from Python script

I had this idea of generating version information for a Python script that uses ArgParse. The code is a little more than I was expecting but I think it works well. Here is the code:

#! /usr/bin/env python
import os
import re
import sys
import logging
import argparse
import datetime
import subprocess
def run(cmd):
(rc, stdout, stderr) = (None, '', '')
if isinstance(cmd, basestring):
cmd = cmd.split()
try:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except Exception as e:
log.debug('Ignoring `{e!s}` from {cmd}'.format(**locals()))
else:
(stdout, stderr) = p.communicate()
rc = p.wait()
log.debug('{cmd}: {rc}, {stdout!r}, {stderr!r}'.format(**locals()))
if (rc == 0) and (not stdout):
rc = None
return (rc, stdout, stderr)
def get_version():
git_used = False
ret = '?'
dir = os.path.dirname(sys.argv[0])
base = os.path.basename(sys.argv[0])
cwd = os.getcwd()
try:
os.chdir(dir)
except:
pass
else:
(rc, stdout, stderr) = run(['git', 'log', '-1', base])
"""
commit {SHA1}
Author: {FIRST_NAME} {LAST_NAME} <{EMAIL_ADDRESS}>
Date: Wed Jan 16 09:32:03 2019 -0500
.
.
.
"""
match = re.search(r'^commit\s+(\S+).*\nDate:\s+(([A-Z][a-z]{2} ){2}[ 0123]\d (\d{2}:){2}\d{2} \d{4})', stdout, re.DOTALL)
log.debug('`git log 0` search groups: {groups}'.format(groups=match.groups() if match else None))
if match:
commit = match.group(1)[:6]
timestamp = datetime.datetime.strptime(match.group(2), '%a %b %d %H:%M:%S %Y')
log.debug('timestamp: {timestamp!s}'.format(**locals()))
(rc, stdout, stderr) = run('git branch')
match = re.search(r'\*\s(\S+)', stdout, re.DOTALL)
log.debug('`git branch` search groups: {groups}'.format(groups=match.groups() if match else None))
if match:
branch = match.group(1)
(rc, stdout, stderr) = run('git remote -v')
"""
origin https://github.com/pfuntner/gists.git (fetch)
"""
hits = list(re.finditer(r'(\S+)\s(https?://\S+)\s\(fetch\)', stdout))
log.debug('`git remote -v` hits: {hits}'.format(hits=[hit.groups() for hit in hits]))
if hits:
hits = ['{name}:{url}'.format(name=hit.group(1), url=hit.group(2)) for hit in hits]
ret = '{commit}, {branch}, {timestamp!s}, {hits}'.format(**locals())
git_used = True
os.chdir(cwd)
if not git_used:
ret = str(datetime.datetime.fromtimestamp(os.path.getmtime(sys.argv[0])))
return ret
logging.basicConfig(format='%(asctime)s %(levelname)s %(pathname)s:%(lineno)d %(msg)s')
log = logging.getLogger()
log.setLevel(logging.WARNING)
parser = argparse.ArgumentParser(description='Example of doing a nifty --version')
parser.add_argument('-v', '--verbose', dest='verbose', action='count', help='Print more messages')
parser.add_argument('--version', action='version', version=get_version(), help='See wonderful version information')
args = parser.parse_args()
log.setLevel(logging.WARNING - (args.verbose or 0) * 10)
# print get_version()
view raw version-example hosted with ❤ by GitHub

Usage

Here is an example of its usage if the script is part of a git repository:

$ ./version-example --version
b92798, master, 2019-01-18 10:35:02,
['origin:https://github.com/pfuntner/gists.git']
$

It contains:
  • The SHA1 of last git commit that changed the script
  • The current branch of the repository
  • The date of the commit - I think the timezone element is present in this but I didn't want to deal with timezones so I'm ignoring it
  • A list of the remote repositories

This is printed on two lines but that's something that ArgParse is doing, not me.

Here is an example of its usage if the script is not part of a git repository - we don't have much information to work from but we can at least get the timestamp of the script:

$ ~/tmp/version-example --version
2019-01-18 11:07:33.873846
$

You'll also get this output if the script is in the repository directory but hasn't been committed to it.

Comments

Popular posts from this blog

Dynamic Python script loading

I have a bunch of toys and tools in a Git repository - I affectionately call this my toys repo . Most are just scripts that I use from a Unix (Cygwin or Git bash on Windoze) command line but there are some Python classes that I sometimes use in another script. Today at work, I was coming up with a new Python script that made use of a couple of my public classes. The script is good enough to share with my colleagues but I'm faced with the problem of my public classes: I imagine that most of my colleagues haven't even heard of my public classes and I don't expect them to download the entire repo just to get a couple of classes If I'm going to distribute the classes as separate files I introduce new problems: It could be confusing to have the files tag along. What is the user supposed to do with them? The answer is nothing - they should leave them alone and make sure they are in the same directory as the main script in case they decide to move th...