blob: fcffed4f2573a9d72e010f0f60bdaa8794e7bf0e [file] [log] [blame]
#!/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()