#!/usr/bin/env python
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Display active git branches and code changes in a ChromiumOS workspace."""

import optparse
import os
import re
import subprocess
import threading

class Result(object):
  '''A class to synchronize multiple repository request threads.

  Multiple parallel running threads collect text information (for instance,
  state of different git repositories, one per thread).

  Using this class object allows these multiple threads to synchronously
  attach collected information to a list.

  This class also provides a facility to unblock the master thread once all
  parallel running threads have finished running.

  To make matters simpler, the number of parallel threads to expect to run is
  given to this class during initialization.

  Attributes:

    thread_lock - a lock object, used as a mutex to control access to the count
                  of running threads

    finished_lock - a lock object, used as a mutex for the master thread. This
                 lock is locked as soon as the first parallel thread starts
                 and stays locked until the last parallel thread finishes.

    thread_count - an integer, number of parallel threads this object will
                 have to handle.

    results - a list of strings, each representing a result of a running
                 parallel thread.
  '''

  def __init__(self, max_threads):
    '''Initialize the object with the number of parallel threads to handle.'''

    self.thread_lock = threading.Lock()
    self.finished_lock = threading.Lock()
    self.finished_lock.acquire()
    self.thread_count = max_threads
    self.results = []

  def ReportFinishedThread(self, result):
    '''Report that a parallel thread finished running.

    Each time a finished thread is reported, decrement the running thread
    count. Once the running thread count reaches zero - unblock the master
    thread lock.

    result - a string, representing the output of the thread, could be empty.
             If not empty - add this string to the results attribute.
    '''

    self.thread_lock.acquire()
    if result:
      self.results.append(result)
    self.thread_count = self.thread_count - 1
    self.thread_lock.release()
    if self.thread_count == 0:
      self.finished_lock.release()

  def GetResults(self):
    '''Wait till all parallel threads finish and return results.

    This function blocks until all parallel threads finish running. Once they
    do, sort the results[] list and return it.
    '''

    self.finished_lock.acquire()
    self.results.sort()
    return self.results

def RunCommand(path, command):
  """Run a command in a given directory, return stdout."""

  return subprocess.Popen(command,
                          cwd=path,
                          stdout=subprocess.PIPE).communicate()[0].rstrip()

#
# Taken with slight modification from gclient_utils.py in the depot_tools
# project.
#
def FindFileUpwards(filename, path):
  """Search upwards from the a directory to find a file."""

  path = os.path.realpath(path)
  while True:
    file_path = os.path.join(path, filename)
    if os.path.exists(file_path):
      return file_path
    (new_path, _) = os.path.split(path)
    if new_path == path:
      return None
    path = new_path


def GetName(relative_name, color):
  """Display the directory name."""

  if color:
    return '\033[44m\033[37m%s\033[0m' % relative_name
  else:
    return relative_name


def GetBranches(full_name, color):
  """Return a list of branch descriptions."""

  command = ['git', 'branch', '-vv']

  if color:
    command.append('--color')

  branches = RunCommand(full_name, command).splitlines()

  if re.search(r"\(no branch\)", branches[0]) and len(branches) == 1:
    return []

  return branches

def GetStatus(full_name, color):
  """Return a list of files that have modifications."""

  command = ['git', 'status', '-s']

  return RunCommand(full_name, command).splitlines()


def GetHistory(full_name, color, author, days):
  """Return a list of oneline log messages.

  The messages are for the author going back a specified number of days.
  """

  command = ['git', 'log',
             '--author=' + author,
             '--after=' + '-' + str(days) + 'days',
             '--pretty=oneline',
             'm/master']

  return RunCommand(full_name, command).splitlines()


def ShowDir(full_name, color, logs, author, days, result):
  """Report active work in a single git repository."""

  branches = GetBranches(full_name, color)
  status = GetStatus(full_name, color)

  text = []
  if logs:
    history = GetHistory(full_name, color, author, days)
  else:
    history = []

  if branches or status or history:
      # We want to use the full path for testing, but we want to use the
      # relative path for display. Add an empty string to the list to have the
      # output sections separated.
    text = [ '', GetName(os.path.relpath(full_name), color), ]

    for extra in (branches, status, history):
        if extra: text = text + extra

  result.ReportFinishedThread('\n'.join(text))


def FindRoot():
  """Returns the repo root."""

  repo_file = '.repo'
  repo_path = FindFileUpwards(repo_file, os.getcwd())

  if repo_path is None:
    raise Exception('Failed to find %s.' % repo_file)

  return os.path.dirname(repo_path)


def main():
  parser = optparse.OptionParser(usage = 'usage: %prog [options]\n')

  parser.add_option('-l', '--logs', default=False,
                    help='Show the last few days of your commits in short '
                         'form.',
                    action='store_true',
                    dest='logs')

  parser.add_option('-d', '--days', default=8,
                    help='Set the number of days of history to show.',
                    type='int',
                    dest='days')

  parser.add_option('-a', '--author', default=os.environ['USER'],
                    help='Set the author to filter for.',
                    type='string',
                    dest='author')

  options, arguments = parser.parse_args()

  if arguments:
    parser.print_usage()
    return 1

  color = os.isatty(1)
  root = FindRoot()
  repos = RunCommand(root, ['repo', 'forall', '-c', 'pwd']).splitlines()

  result = Result(len(repos))

  for full in repos:
    t = threading.Thread(
        target=ShowDir,
        args=(
            full, color, options.logs, options.author, options.days, result))
    t.start()

  print '\n'.join(result.GetResults())

if __name__ == '__main__':
  main()
