#! /bin/sh
# by pts@fazkeas.hu at Mon Nov 11 17:20:59 CET 2013

""":" #git-dcommit: Create commit date based on file last-modification time.

type python2.7 >/dev/null 2>&1 && exec python2.7 -- "$0" ${1+"$@"}
type python2.6 >/dev/null 2>&1 && exec python2.6 -- "$0" ${1+"$@"}
type python2.5 >/dev/null 2>&1 && exec python2.5 -- "$0" ${1+"$@"}
type python2.4 >/dev/null 2>&1 && exec python2.4 -- "$0" ${1+"$@"}
exec python -- ${1+"$@"}
exit 1

This script is a drop-in replacement for `git dcommit'. The only difference is
that `git commit' uses the current time as the date for the newly created
commit, and `git dcommit' uses the largest last-modification time of the
files involved.

To use this script, put it onto your $PATH, and run `git dcommit ...' instead
of `git commit ...'.

This script has been tested on Linux with Git 1.7. It should also work on other
Unix system.s

Takes the maximum last-modification date of files changed.
"""

import errno
import os
import os.path
import stat
import subprocess
import sys
import time


def call(cmd):
  exitcode = subprocess.call(cmd)
  assert not exitcode, 'error: %r: %d' % (cmd, exitcode)


def readpipe(cmd, stdin=None):
  stdin_arg = stdin
  if isinstance(stdin, str):
    stdin_arg = subprocess.PIPE
  else:
    stdin = None
  p = subprocess.Popen(cmd, stdin=stdin_arg, stdout=subprocess.PIPE)
  try:
    return p.communicate(stdin)[0]
  finally:
    exitcode = p.wait()
    assert not exitcode, 'error: %r: %d' % (cmd, exitcode)


def parse_commit_data(commit_data):
  i = 0
  tree_id = None
  parent_ids = []
  commit_msg = None
  while 1:
    j = commit_data.find('\n', i)
    assert j > 0, 'Unexpected end of commit data.'
    line = commit_data[i : j]  # Without the trailing '\n'.
    i = j + 1
    if line.startswith('tree '):
      tree_id = line.split(' ', 1)[1]
    if line.startswith('parent '):
      parent_ids.append(line.split(' ', 1)[1])
    if not line:  # Commit message follows.
      commit_msg = commit_data[i:]
      break
  assert tree_id is not None, 'Missing tree_id in commit data.'
  # We don't check for parent_ids, it can be empty (for the initial commit).
  return tree_id, parent_ids, commit_msg


def parse_commit_id_line(commit_id_line):
  # TODO(pts): Use a regexp.
  assert len(commit_id_line) == 41
  assert commit_id_line.find('\n') == 40
  return commit_id_line[:-1]


def parse_line(line):
  assert line
  assert line.find('\n') == len(line) - 1
  return line[:-1]


def single_quote(s):
  assert '\0' not in s
  return "'%s'" % s.replace("'", "'\\''")


def main(argv):
  do_default_committer = False
  if len(argv) > 1 and argv[1] == '--default-committer':
    argv = list(argv)
    del argv[1]
    do_default_committer = True
  exitcode = subprocess.call(('git', 'commit') + tuple(argv[1:]))
  if exitcode:
    sys.exit(exitcode)
  commit_data = readpipe(('git', 'cat-file', 'commit', 'HEAD'))
  tree_id, parent_ids, commit_msg = parse_commit_data(commit_data)
  old_commit_id = parse_commit_id_line(readpipe(
      ('git', 'rev-parse', '--revs-only', 'HEAD')))
  files_changed = (readpipe(
      ('git', 'diff-tree', '--no-commit-id', '--name-only', '-r', 'HEAD'))
      .splitlines())
  if not files_changed:
    print >>sys.stderr, 'warning: no files changed by the commit'
    sys.exit(0)
  # E.g. '', 'foo/' or 'foo/bar/'.
  prefix = readpipe(('git', 'rev-parse', '--show-prefix'))
  dotback = '../' * prefix.count('/')  # TODO(pts): What if symlinks?
  timestamps = []
  for filename in files_changed:
    try:
      st = os.lstat(dotback + filename)
    except OSError, e:
      if e[0] != errno.ENOENT:
        raise
      continue
    timestamps.append(int(st.st_mtime))
  if not timestamps:
    # Deleted files can't be used.
    print >>sys.stderr, 'error: no files in the commit, cannot get mtime'
    sys.exit(2)
  timestamp = max(timestamps)
  assert timestamp >  99999999
  print >>sys.stderr, 'info: dcommit timestamp: %s: %s +local' % (
      timestamp, time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp)))
  # It's enough to send the environment updates to `git commit-tree'.
  # git automatically adds the local time zone delta (e.g. +0100).
  os.environ['GIT_AUTHOR_DATE'] = str(timestamp)
  if not do_default_committer:
    if 'GIT_AUTHOR_NAME' in os.environ:
      os.environ['GIT_COMMITTER_NAME'] = os.environ['GIT_AUTHOR_NAME']
    else:
      os.environ['GIT_COMMITTER_NAME'] = parse_line(readpipe(
          ('git', 'config', 'user.name')))
    if 'GIT_AUTHOR_EMAIL' in os.environ:
      os.environ['GIT_COMMITTER_EMAIL'] = os.environ['GIT_AUTHOR_EMAIL']
    else:
      os.environ['GIT_COMMITTER_EMAIL'] = parse_line(readpipe(
          ('git', 'config', 'user.email')))
    os.environ['GIT_COMMITTER_DATE'] = str(timestamp)
  parent_id_args = []
  for parent_id in parent_ids:
    parent_id_args.append('-p')
    parent_id_args.append(parent_id)
  new_commit_id = parse_commit_id_line(readpipe(
      ('git', 'commit-tree', tree_id) + tuple(parent_id_args), commit_msg))
  call(('git', '-c', 'core.logAllRefUpdates=false', 'update-ref', 'HEAD',
        new_commit_id, old_commit_id))
  git_dir = parse_line(readpipe(('git', 'rev-parse', '--git-dir')))
  obj_dir = os.path.join(git_dir, 'objects', old_commit_id[:2])
  obj_file = os.path.join(obj_dir, old_commit_id[2:])
  os.remove(obj_file)
  try:
    os.rmdir(obj_dir)
  except OSError, e:
    if e[0] != errno.ENOTEMPTY:
      raise


if __name__ == '__main__':
  sys.exit(main(sys.argv))