| #!/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() |