# MA 02110-1301, USA.
"""Common functionality of the adm module"""
+import os
+import subprocess
+
+from collections import defaultdict
+from datetime import datetime
+
+
+class AdmError(Exception):
+ """Exception class for this module"""
+ pass
+
def pprint_sz(size):
"""Pretty print file size in human readable format
break
return "%.1f %s" % (float(size) / pow(1024, power - 1), unit)
+def run_at(cmd, path):
+ """Run command in cwd"""
+ popen = subprocess.Popen(cmd, cwd=path,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ out, err = popen.communicate()
+ if popen.returncode:
+ raise AdmError("Failed to run '%s' in '%s': %s" %
+ (' '.join(cmd), path, err))
+ return out
+
+def git_cmd(cmd, args, path):
+ """Run git command in repo"""
+ par_path = os.path.abspath(os.path.join(path, '..'))
+ cmd = ['git', cmd] + args
+ popen = subprocess.Popen(cmd, cwd=path,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ env={'GIT_CEILING_DIRECTORIES': par_path})
+ out, err = popen.communicate()
+ if popen.returncode:
+ raise AdmError('Git command (%s) in %s failed: %s' %
+ (' '.join(cmd), path, err))
+ return out
+
+def repo_is_bare(path):
+ """Check if a path is a bare repository"""
+ try:
+ out = git_cmd('rev-parse', ['--is-bare-repository'], path)
+ except AdmError:
+ raise AdmError('Not a git repository at %s' % path)
+ return out.splitlines()[0].strip() == 'true'
+
+def _file_stat(path):
+ """Get repocache stat of file"""
+ if not os.path.isfile(path):
+ raise AdmError('%s is not a file' % path)
+
+ stat = {}
+ fstat = os.stat(path)
+
+ stat['path'] = os.path.abspath(path)
+ stat['du'] = fstat.st_blocks * fstat.st_blksize
+ #stat['mtime'] = datetime.fromtimestamp(fstat.st_mtime)
+ stat['mtime'] = str(datetime.fromtimestamp(fstat.st_mtime))
+
+ return stat
+
+def _repo_stat(path):
+ """Get repocache stat of a git repository"""
+
+ stat = {}
+ stat['bare'] = repo_is_bare(path)
+
+ remote = git_cmd('remote', ['-v'], path).splitlines()[0]
+ stat['remote_url'] = remote.split(None, 1)[1].rsplit(None, 1)[0]
+
+ return stat
+
+def _list_file_stat(path):
+ """Get stat of all non-directory files in a directory"""
+ stat = {}
+ for fname in os.listdir(path):
+ if not os.path.isdir(os.path.join(path, fname)):
+ stat[fname] = _file_stat(os.path.join(path, fname))
+ return stat
+
+def cache_stat(path):
+ """Get stat of a repo cache"""
+
+ if not os.path.isdir(path):
+ raise AdmError("Repocache basedir '%s' not found" % path)
+
+ stat = {'dirs': defaultdict(dict)}
+
+
+ # Get disk usage of directories, du output is something like:
+ # 1234 ./foo/bar
+ # 4567 ./foo
+ # 8901 .
+ out = run_at(['du', '-d2', '-B1', '-0'], path)
+ d_u = [(split[1], split[0]) for split in
+ [line.split() for line in out.split('\0') if line]]
+
+ for entry in d_u:
+ base, sub = os.path.split(entry[0])
+ if sub == '.':
+ dstat = stat
+ elif base == '.':
+ dstat = stat['dirs'][sub]
+ else:
+ basedir = os.path.split(base)[1]
+ if 'dirs' not in stat['dirs'][basedir]:
+ stat['dirs'][basedir]['dirs'] = {sub: {}}
+ else:
+ stat['dirs'][basedir]['dirs'][sub] = {}
+ dstat = stat['dirs'][basedir]['dirs'][sub]
+ try:
+ dstat['repostat'] = _repo_stat(os.path.join(path, base, sub))
+ except AdmError:
+ pass
+
+ dstat['path'] = os.path.abspath(os.path.normpath(os.path.join(path,
+ entry[0])))
+ dstat['du'] = entry[1]
+ # Don't list files inside repos
+ if not 'repostat' in dstat:
+ stat['files'] = _list_file_stat(dstat['path'])
+
+ return stat
+
class SubcommandBase(object):
"""Base class / API for subcommand implementations"""