Last active
September 16, 2017 18:28
-
-
Save jayrm/b1fbc8199ce0d38858fe48076fc090d2 to your computer and use it in GitHub Desktop.
`git dirs' custom command, get the status of multiple local repositories and submodules recursively, written in freebasic
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'' -------------------------------------------------------- | |
'' git-dirs.bas | |
'' -------------------------------------------------------- | |
'' get status of multiple git directories, recursively | |
'' copy to {GITDIR}/libexec/git-core/git-dirs[.exe] | |
'' use `git dirs -h' for usage or see help at bottom of this file | |
type GIT_DIRS_OPTIONS | |
recursive as boolean = false | |
alldirs as boolean = false | |
showhelp as boolean = false | |
incolor as boolean = true | |
sorted as boolean = false | |
fetch as boolean = false | |
status as boolean = false | |
system_dirs as boolean = false | |
verbose as boolean = false | |
end type | |
type GIT_STATUS_RESULT | |
detached as boolean '' current branch is detached head | |
currbranch as string '' name of the current branch | |
staged as boolean '' staged changes to commit (use commit) | |
modified as boolean '' modified files to stage (use add) | |
noupstream as boolean '' no upstream remote configured | |
ahead as integer '' current branch is ahead (use push) | |
behind as integer '' current branch is behind (use pull) | |
end type | |
dim shared opts as GIT_DIRS_OPTIONS | |
'' -------------------------------------------------------- | |
'' DIRECTORY | |
'' - read contents of directory | |
'' - use fb's dir() function | |
'' - store all directories and filenames except . & .. | |
'' -------------------------------------------------------- | |
#include once "dir.bi" | |
type FILE_INFO | |
filename as string | |
attrib as integer | |
end type | |
type DIRECTORY | |
private: | |
m_files(any) as FILE_INFO | |
m_num_files as integer | |
m_max_files as integer | |
declare sub addfile( byref f as const string, byval attrib as integer ) | |
declare sub quicksort( byval lo as integer, byval hi as integer ) | |
public: | |
declare constructor() | |
declare destructor() | |
declare sub Clear() | |
declare function ReadDirectory( byref path as const string, byval system as boolean ) as boolean | |
declare property Count() as integer | |
declare property FileName( byval index as integer ) as string | |
declare property FileAttrib( byval index as integer ) as integer | |
declare sub Sort() | |
end type | |
'' | |
private sub DIRECTORY.addfile( byref f as const string, byval attrib as integer ) | |
if( m_num_files = m_max_files ) then | |
m_max_files *= 2 | |
redim preserve m_files( 1 to m_max_files ) | |
end if | |
m_num_files += 1 | |
with m_files(m_num_files) | |
.filename = f | |
.attrib = attrib | |
end with | |
end sub | |
'' | |
sub DIRECTORY.Clear() | |
for i as integer = 1 to m_num_files | |
m_files(i).filename = "" | |
next | |
m_num_files = 0 | |
end sub | |
'' | |
constructor DIRECTORY() | |
m_num_files = 0 | |
m_max_files = 16 | |
redim m_files( 1 to m_max_files ) as FILE_INFO | |
end constructor | |
'' | |
destructor DIRECTORY() | |
this.Clear() | |
erase m_files | |
end destructor | |
'' | |
function DIRECTORY.ReadDirectory( byref path as const string, byval system as boolean ) as boolean | |
this.Clear() | |
dim attrib as integer | |
dim d as string | |
d = dir( path & "*.*", &hff, attrib ) | |
while( len(d) > 0 ) | |
select case d | |
case "." | |
case ".." | |
case else | |
'' only include system files if system=true | |
if( (system = true) orelse ((attrib and fbSystem) = 0) ) then | |
addfile( d, attrib ) | |
end if | |
end select | |
d = dir( attrib ) | |
wend | |
function = cbool( m_num_files > 0 ) | |
end function | |
'' | |
property DIRECTORY.Count() as integer | |
property = m_num_files | |
end property | |
'' | |
property DIRECTORY.FileName( byval index as integer ) as string | |
if( index >= 1 andalso index <= m_num_files ) then | |
property = m_files(index).filename | |
else | |
property = "" | |
end if | |
end property | |
'' | |
property DIRECTORY.FileAttrib( byval index as integer ) as integer | |
if( index >= 1 andalso index <= m_num_files ) then | |
property = m_files(index).attrib | |
else | |
property = 0 | |
end if | |
end property | |
'' | |
private sub DIRECTORY.quicksort( byval lo as integer, byval hi as integer ) | |
if( lo < hi ) then | |
dim pivot as string = m_files(hi).filename | |
'' parition | |
dim i as integer = lo - 1 | |
for j as integer = lo to hi - 1 | |
if m_files(j).filename < pivot then | |
i += 1 | |
swap m_files(i), m_files(j) | |
end if | |
next | |
if( m_files(hi).filename < m_files(i+1).filename ) then | |
swap m_files(i+1), m_files(hi) | |
end if | |
dim p as integer = i+1 | |
'' sort each side of the partition | |
quicksort( lo, p-1 ) | |
quicksort( p+1, hi ) | |
end if | |
end sub | |
'' | |
sub DIRECTORY.Sort() | |
this.quicksort( 1, m_num_files ) | |
end sub | |
'' -------------------------------------------------------- | |
'' COLOURS | |
'' -------------------------------------------------------- | |
dim shared last_color as integer = 7 | |
const COLOR_IS_GIT_DIR = 7 | |
const COLOR_IS_GIT_SUB = 6 | |
const COLOR_IS_NORMAL_DIR = 4 | |
'' | |
sub git_dir_save_color() | |
if( opts.incolor ) then | |
last_color = color() | |
end if | |
end sub | |
'' | |
sub git_dir_load_color() | |
if( opts.incolor ) then | |
color LOWORD(last_color) | |
end if | |
end sub | |
'' | |
sub git_dir_color( byval clr as integer ) | |
if( opts.incolor ) then | |
color clr | |
end if | |
end sub | |
'' -------------------------------------------------------- | |
'' STATUS | |
'' -------------------------------------------------------- | |
'' | |
sub verbose( byref cmd as const string ) | |
if( opts.verbose ) then | |
print cmd | |
end if | |
end sub | |
'' | |
function git_dir_get_status( byref path as const string ) as GIT_STATUS_RESULT | |
'' make multiple `git' command shell calls and | |
'' check results to determine the status of each | |
'' git directory | |
dim h as integer = freefile | |
dim cmd as string | |
dim ret as GIT_STATUS_RESULT | |
'' ---------- | |
'' get the name of the current branch and test for detached head | |
'' git [-C path] symbolic-ref HEAD --short --quiet -- | |
cmd = "git" | |
if( path > "" ) then cmd &= " -C """ & path & """" | |
cmd &= " symbolic-ref HEAD --short --quiet --" | |
verbose( "CMD: " & cmd ) | |
if( open pipe ( cmd for input as #h ) = 0 ) then | |
dim x as string | |
line input #h, x | |
close #h | |
ret.currbranch = x | |
end if | |
ret.detached = cbool( ret.currbranch = "" ) | |
'' ---------- | |
'' if the head is detached, then don't bother going any further | |
'' we will leave up to the user to investigate more | |
if( ret.detached = true ) then | |
goto abort | |
end if | |
'' ---------- | |
'' test for staged changes not yet committed | |
'' returns 0 = no, 1 = yes | |
'' git [-C path] diff-index --quiet --cached HEAD -- | |
cmd = "git" | |
if( path > "" ) then cmd &= " -C """ & path & """" | |
cmd &= " diff-index --quiet --cached HEAD --" | |
verbose( "CMD: " & cmd ) | |
ret.staged = cbool( shell( cmd ) ) | |
'' ---------- | |
'' test for changes not yet staged | |
'' returns 0 = no, 1 = yes | |
'' git [-C path] diff-files --quiet | |
cmd = "git" | |
if( path > "" ) then cmd &= " -C """ & path & """" | |
cmd &= " diff-files --quiet" | |
verbose( "CMD: " & cmd ) | |
ret.modified = cbool( shell( cmd ) ) | |
'' ---------- | |
'' test for untracked files | |
'' git [-C path] ls-files --others | |
'' !!! not implemented | |
'' ---------- | |
'' test for untracked and unignored | |
'' git [-C path] ls-files --exclude-standard --others | |
'' !!! not implemented | |
'' ---------- | |
'' test for upstream remote tracking for current branch | |
'' git [-C path] branch -r --color=never | |
cmd = "git" | |
if( path > "" ) then cmd &= " -C """ & path & """" | |
cmd &= " branch -r --color=never" | |
verbose( "CMD: " & cmd ) | |
ret.noupstream = true | |
if( open pipe ( cmd for input as #h ) = 0 ) then | |
dim x as string | |
while eof(h) = false | |
line input #h, x | |
if( len(x) > len(ret.currbranch) ) then | |
if( right( x, len(ret.currbranch) + 1 ) = "/" & ret.currbranch ) then | |
ret.noupstream = false | |
exit while | |
end if | |
end if | |
wend | |
close #h | |
end if | |
'' ---------- | |
'' if no upstream remote for current branch, then abort status checks | |
if( ret.noupstream ) then | |
goto abort | |
end if | |
'' ---------- | |
'' determine number of commits ahead/behind | |
'' git [-C path] rev-list --left-right --count HEAD...@{u} | |
'' returns N1<tab>N2 | |
'' N1 = number of commits current branch is AHEAD of tracking branch | |
'' N2 = number of commets current branch is BEHIND tracking branch | |
cmd = "git" | |
if( path > "" ) then cmd &= " -C """ & path & """" | |
cmd &= " rev-list --left-right --count HEAD...@{u}" | |
verbose( "CMD: " & cmd ) | |
if( open pipe ( cmd for input as #h ) = 0 ) then | |
dim x as string | |
line input #h, x | |
close #h | |
dim i as integer = instr( x, chr(9) ) | |
if( i > 0 ) then | |
ret.ahead = val(left(x,i-1)) | |
ret.behind = val(mid(x,i+1)) | |
else | |
ret.ahead = val(x) | |
ret.behind = 0 | |
end if | |
end if | |
abort: | |
function = ret | |
end function | |
'' | |
function git_dir_ReadDirectory _ | |
( _ | |
byref basepath as const string, _ | |
byref path as const string, _ | |
byval follow as const boolean = false _ | |
) as boolean | |
dim lst as DIRECTORY | |
lst.ReadDirectory( basepath + path, opts.system_dirs ) | |
if( opts.sorted ) then | |
lst.Sort() | |
end if | |
dim is_git_dir as boolean = false | |
dim is_git_sub as boolean = false | |
for i as integer = 1 to lst.count | |
if( lst.filename(i) = ".git" ) then | |
if(( lst.fileattrib(i) and fbDirectory ) <> 0 ) then | |
is_git_dir = true | |
else | |
is_git_sub = true | |
end if | |
exit for | |
end if | |
next | |
dim c as integer = 0 | |
dim msg as string = "" | |
if( is_git_dir ) then | |
if( path = "" ) then | |
msg = "<current directory>" | |
else | |
msg = path | |
end if | |
c = COLOR_IS_GIT_DIR | |
elseif( is_git_sub ) then | |
if( path = "" ) then | |
msg = "<current directory>" | |
else | |
msg = path | |
end if | |
c = COLOR_IS_GIT_SUB | |
else | |
if( opts.alldirs ) then | |
msg = path | |
c = COLOR_IS_NORMAL_DIR | |
end if | |
end if | |
if( is_git_dir orelse is_git_sub orelse opts.alldirs ) then | |
git_dir_save_color() | |
git_dir_color( c ) | |
print chr(9) + msg | |
git_dir_load_color() | |
end if | |
if( is_git_dir orelse is_git_sub ) then | |
if( opts.fetch ) then | |
dim cmd as string | |
cmd = "git" | |
if( path > "" ) then cmd &= " -C """ & path & """" | |
cmd &= " fetch --all --tags" | |
verbose( cmd ) | |
shell( cmd ) | |
end if | |
if( opts.status ) then | |
dim s as GIT_STATUS_RESULT = git_dir_get_status( path ) | |
if( s.detached ) then | |
print chr(9) & chr(9) & "detached head (use ""git commit"", ""git reset"", or ""git checkout"", to fix)" | |
end if | |
dim prefix as string = chr(9) & chr(9) & "[" & s.currbranch & "]: " | |
if( s.modified ) then | |
print prefix & "files modified (use ""git add"")" | |
end if | |
if( s.staged ) then | |
print prefix & "files staged (use ""git commit"")" | |
end if | |
if( s.noupstream ) then | |
print prefix & "no upstream branch (use ""git push --set-upstream origin " & s.currbranch & """)" | |
end if | |
if( s.ahead <> 0 ) then | |
print prefix & s.ahead & " commit(s) ahead (use ""git push"")" | |
end if | |
if( s.behind <> 0 ) then | |
print prefix & s.behind & " commit(s) behind (use ""git pull"")" | |
end if | |
end if | |
end if | |
if( follow ) then | |
for i as integer = 1 to lst.count | |
if( (lst.fileattrib(i) and fbDirectory) <> 0 ) then | |
if( lcase(lst.filename(i)) <> ".git" ) then | |
dim p as string = path + lst.filename(i) + "/" | |
git_dir_ReadDirectory( basepath, p, opts.recursive ) | |
end if | |
end if | |
next | |
end if | |
function = true | |
end function | |
'' -------------------------------------------------------- | |
'' GIT DIRS -- MAIN | |
'' -------------------------------------------------------- | |
dim i as integer = 1 | |
while command(i) <> "" | |
select case lcase(command(i)) | |
case "-h", "-help" | |
opts.showhelp = true | |
case "-a", "--all" | |
opts.alldirs = true | |
case "--color", "--color=y", "--color=yes" | |
opts.incolor = true | |
case "--color=n", "--color=no", "--color=never" | |
opts.incolor = false | |
case "-r", "--recursive" | |
opts.recursive = true | |
case "-s", "--sorted" | |
opts.sorted = true | |
case "--status" | |
opts.status = true | |
case "--fetch" | |
opts.fetch = true | |
case "--system" | |
opts.system_dirs = true | |
case "--verbose", "-v" | |
opts.verbose = true | |
case else | |
print "arg '" & command(i) & "' ignored" | |
end select | |
i += 1 | |
wend | |
if( opts.showhelp ) then | |
print "usage: git dirs [options]" | |
print " -a, --all all directories (except system)" | |
print " --color[=yes|no] color formatting" | |
print " -r, --recursive recursive" | |
print " -s, --sorted sorted" | |
print " --status query status of each git dir" | |
print " --fetch perform a 'fetch --all --tags' at each git dir" | |
print " --system include system directories" | |
print " -v, --verbose be more verbose" | |
end | |
end if | |
git_dir_ReadDirectory( "./", "", true ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment