blob: 13961027d5f8fa081f83322abd06d17a98daf33f [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.
fast_report - a boolean, if True - report results as they arrive, do not
wait for all thread results to arrive and get sorted
started_threads - an integer, number of threads started so far.
results - a list of strings, each representing a result of a running
parallel thread.
'''
def __init__(self, max_threads, fast_report=False):
'''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.fast_report = fast_report
self.started_threads = 0
self.results = []
def ReportThreadStatus(self):
'''Print status line when enabled.
If enabled, print a line showing numbers of started and still running
threads. Make sure this line is printed in the same screen position (using
the ANSI cursor control escape sequence).
'''
if self.fast_report:
return
print '\n%c[1AStarted threads: %3d, remaining threads: %3d' % (
27, self.started_threads, self.thread_count),
def ReportStartedThread(self):
'''Increment started thread count.
And report status, if enabled.
'''
self.thread_lock.acquire()
self.started_threads = self.started_threads + 1
self.ReportThreadStatus()
self.thread_lock.release()
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 - ether add this string to the results attribute or
print it, if fast reporting is enabled.
'''
self.thread_lock.acquire()
if result:
if self.fast_report:
print result
else:
self.results.append(result)
self.thread_count = self.thread_count - 1
self.ReportThreadStatus()
self.thread_lock.release()
if self.thread_count == 0:
if not self.fast_report:
# Erase the status line.
print '\n%c[1A%c[K%c[1A' % (27, 27, 27),
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."""
result.ReportStartedThread()
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')
parser.add_option('-f', '--fast-report', default=False,
help='Print results as they arrive, do not sort.',
action='store_true',
dest='fast_report')
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), options.fast_report)
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()