├── imgs ├── gm-legend.png ├── gmscreenshot.png └── gm-master-view.png ├── helper_scripts ├── git-push-remotes.py ├── git-push-projects.py ├── git-push-all.py ├── git-add-remote.py ├── git-manager-stat.py ├── gm_resources.py ├── git-manager.py ├── git-local-setup.py └── gm_custom.py ├── gmconfig-example ├── LICENSE ├── utility.py ├── README.md ├── git_report_structure.py └── git-manager-gui.py /imgs/gm-legend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etola/git-manager/HEAD/imgs/gm-legend.png -------------------------------------------------------------------------------- /imgs/gmscreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etola/git-manager/HEAD/imgs/gmscreenshot.png -------------------------------------------------------------------------------- /imgs/gm-master-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etola/git-manager/HEAD/imgs/gm-master-view.png -------------------------------------------------------------------------------- /helper_scripts/git-push-remotes.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | import sys 3 | 4 | from gm_custom import git_push_remotes 5 | 6 | argc = len(sys.argv) 7 | if argc < 3: 8 | print 9 | print 'Push a repo to multiple remotes' 10 | print 11 | print ' Usage: gm-pr project_name remote_names' 12 | print ' Ex: gm-pr kortex local github borg' 13 | print 14 | sys.exit(0) 15 | 16 | repo_name = sys.argv[1] 17 | remote_list = sys.argv[2:argc] 18 | 19 | git_push_remotes( repo_name, remote_list, 'master' ) 20 | 21 | 22 | -------------------------------------------------------------------------------- /helper_scripts/git-push-projects.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | import sys 3 | 4 | from gm_custom import * 5 | 6 | argc = len(sys.argv) 7 | if argc < 3: 8 | print 9 | print 'Push multiple projects to a single remote' 10 | print 11 | print ' Usage: gm-pp remote_name repo_names' 12 | print ' Ex: gm-pp borg kortex kortex-ext-3d blender' 13 | print 14 | sys.exit(0) 15 | 16 | remote_name = sys.argv[1] 17 | repo_names = sys.argv[2:argc] 18 | 19 | if is_valid_remote( remote_name ) is False: 20 | print 'Not a valid remote name ['+remote_name+']' 21 | sys.exit(1) 22 | 23 | git_push_projects( repo_names, 'master', remote_name ) 24 | 25 | -------------------------------------------------------------------------------- /gmconfig-example: -------------------------------------------------------------------------------- 1 | repo[kortex][/home/tola/src/cpp/lib/kortex] 2 | repo_d[kortex-ext-advanced][src/cpp/lib] 3 | repo_d[kortex-ext-3d][src/cpp/lib] 4 | repo_d[kortex-ext-calibration][src/cpp/lib] 5 | repo_d[kortex-ext-opencv][src/cpp/lib] 6 | repo_d[cosmos][src/cpp/lib] 7 | repo_d[karpet][src/cpp/lib] 8 | repo_d[kutility][src/cpp/lib] 9 | repo_d[argus][src/cpp/lib] 10 | repo_d[margus][src/cpp/lib] 11 | separator 12 | repo_d[git_manager][src/python] 13 | repo_d[shellscripts][src] 14 | separator 15 | repo_dir[/home/tola/src/cpp/applications][] 16 | separator 17 | repo_d[color-to-gray][src/cpp/applications-beta] 18 | repo_d[inpainting][src/cpp/applications-beta] 19 | 20 | remote[HEAD][H] 21 | remote[local][L] 22 | remote[borg][B] 23 | remote[cruzer][C] 24 | remote[github][G] 25 | remote[echelon][E] 26 | remote[enterprise][N] 27 | remote[pegasus][P] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 engin tola 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /helper_scripts/git-push-all.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import os, sys, string, math 4 | import commands 5 | 6 | # from gm_resources import * 7 | from gm_custom import * 8 | 9 | sys.path.append(os.path.abspath(os.getenv('HOME')+"/src/python/git_manager/helper_scripts/")) 10 | 11 | if len(sys.argv) != 2: 12 | print 'usage: ' + sys.argv[0] + ' remote_name' 13 | sys.exit(1) 14 | 15 | remote_name = sys.argv[1] 16 | 17 | repos = get_repositories() 18 | 19 | smsg = "" 20 | for repo in repos: 21 | if repo == "": 22 | continue 23 | 24 | rfolder = get_repository_path( repo ) 25 | 26 | g = generate_git_report( repo ) 27 | if g.isrepo == -1: 28 | smsg += ' skipped [' + repo + '] - not a git repo\n' 29 | continue 30 | if is_valid_repo_dir( repo ) is False: 31 | smsg += ' skipped [' + repo + '] - not a valid directory\n' 32 | continue 33 | 34 | os.chdir( rfolder ) 35 | cmd = 'git push '+ remote_name + ' master' 36 | print repo.ljust(25) + ' -> ' + cmd 37 | rc, git_st = commands.getstatusoutput(cmd) 38 | if rc != 0: 39 | smsg += ' failed [' + repo + ']\n' 40 | 41 | 42 | print '---------------------------------------------------------' 43 | print smsg 44 | -------------------------------------------------------------------------------- /utility.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | import os, sys, commands 3 | 4 | def get_folders(root_dir): 5 | folders = [ name for name in os.listdir(root_dir) if os.path.isdir(os.path.join(root_dir, name)) ] 6 | return folders 7 | 8 | def get_files(root_dir): 9 | files = [ name for name in os.listdir(root_dir) if os.path.isfile(os.path.join(root_dir, name)) ] 10 | return files 11 | 12 | def get_home(): 13 | return os.path.abspath(os.getenv('HOME')) 14 | 15 | def get_pwd(): 16 | return os.path.abspath(os.getenv('PWD')) 17 | 18 | def is_valid_dir( path ): 19 | return os.path.isdir( path ) 20 | 21 | def is_valid_file( path ): 22 | return os.path.isfile( path ) 23 | 24 | def run_command( cmd ): 25 | rc, cmd_info = commands.getstatusoutput(cmd) 26 | if rc != 0: 27 | print 'error processing command ['+cmd+']' 28 | print 'msg: ' 29 | print cmd_info 30 | return rc, cmd_info 31 | 32 | def get_mount_point( disk_name ): 33 | 34 | rc, cmd_info = commands.getstatusoutput( 'mount | grep -w '+disk_name ) 35 | if len(cmd_info) == 0: 36 | return '' 37 | else: 38 | rc, mount_dir = commands.getstatusoutput('echo \''+cmd_info+'\' | awk \'{print $3}\'') 39 | if rc != 0: 40 | print 'error processing command!' 41 | print mount_dir 42 | return mount_dir 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /helper_scripts/git-add-remote.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import os, sys, string, math 4 | import commands 5 | 6 | from gm_custom import * 7 | 8 | sys.path.append(os.path.abspath(os.getenv('HOME')+"/src/python/git_manager/")) 9 | 10 | if len(sys.argv) != 2: 11 | print 'usage: ' + sys.argv[0] + ' remote_name' 12 | sys.exit(1) 13 | 14 | remote_name = sys.argv[1] 15 | 16 | repos = get_repositories() 17 | 18 | smsg = "" 19 | for repo in repos: 20 | if repo == "": 21 | continue 22 | 23 | rfolder = get_repository_path( repo ) 24 | 25 | if is_valid_repo_dir( repo ) is False: 26 | smsg += ' skipped [' + repo + '] - not a valid directory [' + rfolder + ']\n' 27 | continue 28 | 29 | g = generate_git_report( repo ) 30 | if g.isrepo == -1: 31 | smsg += ' skipped [' + repo + '] - not a git repo\n' 32 | continue 33 | 34 | os.chdir( rfolder ) 35 | 36 | # repo_url = '/media/'+remote_name+'/backup/gitrepos/'+repo+'.git' 37 | repo_url = get_home()+'/gitrepos/'+repo+'.git' 38 | 39 | # cmd = 'git remote add '+remote_name+' '+repo_url 40 | cmd = 'git remote set-url '+remote_name+' '+repo_url 41 | # cmd = 'git remote remove '+remote_name 42 | 43 | # cmd = 'git remote rename origin local' 44 | 45 | print repo.ljust(25) + ' -> ' + cmd 46 | rc, git_st = commands.getstatusoutput(cmd) 47 | if rc != 0: 48 | smsg += ' failed [' + repo + ']\n' 49 | 50 | print '---------------------------------------------------------' 51 | print smsg 52 | -------------------------------------------------------------------------------- /helper_scripts/git-manager-stat.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | import sys, os 3 | 4 | sys.path.append(os.path.abspath(os.getenv('HOME')+"/src/python/git_manager/")) 5 | 6 | from gm_custom import * 7 | 8 | if len(sys.argv) < 2: 9 | print 10 | print 'Usage: ' + sys.argv[0] + ' repo_name' 11 | print 12 | sys.exit(0) 13 | 14 | raw_out = False 15 | for arg in sys.argv: 16 | if arg == '--raw': 17 | raw_out = True 18 | 19 | repo_name = sys.argv[1] 20 | 21 | if is_valid_repo_dir( repo_name ) is False: 22 | print 'could not find repo [ ' + repo_name + ' ]' 23 | sys.exit(1) 24 | 25 | folder_path = get_repository_path( repo_name ) 26 | 27 | g = generate_git_report( repo_name ) 28 | 29 | if raw_out is False: 30 | g.display() 31 | else : 32 | os.chdir( folder_path ) 33 | print 'Remotes' 34 | print '--------------------------------------------------------------' 35 | # rc, git_st = commands.getstatusoutput('git remote -v') 36 | # print git_st 37 | print g.raw_remotes 38 | print 39 | 40 | print 'Short History' 41 | print '--------------------------------------------------------------' 42 | # rc, git_st = commands.getstatusoutput('git hists') 43 | # print git_st 44 | print g.raw_hist 45 | print 46 | 47 | print 'Status' 48 | print '--------------------------------------------------------------' 49 | # rc, git_st = commands.getstatusoutput('git st') 50 | # print git_st 51 | print g.raw_status 52 | print 53 | 54 | print 'Pushed Remotes' 55 | print '--------------------------------------------------------------' 56 | print g.lcsyncrem 57 | print 58 | -------------------------------------------------------------------------------- /helper_scripts/gm_resources.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | from utility import get_folders, get_home 4 | 5 | def get_repositories(): 6 | rep_names = [ "kortex", 7 | "kortex-ext-advanced", 8 | "kortex-ext-opencv", 9 | "cosmos", 10 | "karpet", 11 | "kutility", 12 | "argus", 13 | "margus", 14 | "" 15 | ] 16 | 17 | app_names = get_folders( get_home() + '/src/cpp/applications') 18 | rep_names.extend( app_names ) 19 | 20 | # rep_names.extend( [ "" ] ) 21 | # app_extra_names = get_folders( get_home() + '/src/cpp/applications-test' ); 22 | # rep_names.extend( app_extra_names ) 23 | 24 | # rep_names.extend( [ "", "color-to-gray", "skanner", "levmar"] ) 25 | rep_names.extend( [ "", "shellscripts", "git_manager", "makefile-heaven" ] ) 26 | # rep_names.extend( [ "makefile-heaven" ] ) 27 | 28 | rep_names.extend( [ "", "applications-test" ] ) 29 | 30 | return rep_names 31 | 32 | def get_repositories_search_dirs(): 33 | homedir = get_home() 34 | search_paths = [ homedir + '/src/cpp/lib/', 35 | homedir + '/src/cpp/applications/', 36 | homedir + '/src/cpp/applications-beta/', 37 | homedir + '/src/python/', 38 | homedir + '/src/cpp/', 39 | homedir + '/src/' ] 40 | return search_paths 41 | 42 | def get_remote_names(): 43 | remotes = [ 'HEAD', 44 | 'local', 45 | 'arctic', 46 | 'antarctic', 47 | 'borg', 48 | 'cruzer', 49 | 'github', 50 | 'echelon', 51 | 'pegasus', 52 | 'vault' ] 53 | 54 | remote_symbols = [ 'H', 'L', 'A', 'N', 'B', 'C', 'G', 'E', 'P', 'V' ] 55 | 56 | return remotes, remote_symbols 57 | -------------------------------------------------------------------------------- /helper_scripts/git-manager.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | import os, sys, string, math, commands 3 | from gm_custom import * 4 | 5 | def rep_state( rname, rlist, tval ): 6 | if rname in rlist: 7 | return tval 8 | else: 9 | return '.' 10 | 11 | def var_state( var ): 12 | if var: 13 | return '+' 14 | else: 15 | return ' ' 16 | 17 | def num_list_elem( l, n_just ): 18 | lsz = len(l) 19 | if lsz == 0: 20 | return '.'.rjust(n_just) 21 | else: 22 | return str(lsz).rjust(n_just) 23 | 24 | def repo_remote_state( g ): 25 | msg = '' 26 | remotes, remote_symbols = get_remote_names() 27 | cnt = 0 28 | for remote in remotes: 29 | rsymbol = remote_symbols[cnt] 30 | msg += rep_state( remote, g.lcsyncrem, rsymbol ) 31 | cnt += 1 32 | return msg 33 | 34 | remotes, remote_symbols = get_remote_names() 35 | remote_header = '' 36 | for rsym in remote_symbols: 37 | remote_header += rsym 38 | 39 | print 40 | print 'repository_name'.rjust(31) + ' |C| '+remote_header+' |' + 'Branch'.rjust(7) + ' |' + 'S'.rjust(2) + 'C'.rjust(2) + 'U'.rjust(3) 41 | print ' ------------------------------------------------------------' 42 | 43 | smsg = '' 44 | msg = '' 45 | 46 | cnt = 0 47 | 48 | repos = get_repositories() 49 | 50 | for repo in repos: 51 | if repo == "": 52 | msg += ' ------------------------------------------------------------\n' 53 | continue 54 | g = generate_git_report( repo ) 55 | if g.isrepo == -1: 56 | smsg += ' skipped ['+repo+'] - not a git repo\n' 57 | continue 58 | 59 | msg += ' [' + str( repos.index(repo) ).rjust(2) + ']' 60 | msg += g.repo_name.rjust(25) + ' |' + var_state(g.commit) + '| ' 61 | msg += repo_remote_state( g ) 62 | msg += ' |' 63 | msg += g.branch.rjust(7) + ' |' 64 | msg += num_list_elem(g.sfiles,2) 65 | msg += num_list_elem(g.cfiles,2) 66 | msg += num_list_elem(g.ufiles,3) 67 | if repo != repos[ len(repos)-1 ]: 68 | msg += '\n' 69 | 70 | cnt += 1 71 | 72 | print msg 73 | print ' ------------------------------------------------------------' 74 | 75 | remotes, remote_symbols = get_remote_names() 76 | cnt = 0 77 | msg = ' ' 78 | for remote in remotes: 79 | rsymbol = remote_symbols[cnt] 80 | msg += (rsymbol+': '+remote).ljust(15) 81 | cnt += 1 82 | if cnt%4 == 0 and ( cnt != len(remotes) ): 83 | msg += '\n ' 84 | print msg 85 | print ' ------------------------------------------------------------' 86 | print ' S : staged C : changed U : untracked' 87 | 88 | if smsg : 89 | print ' ------------------------------------------------------------' 90 | print ' Skipped Repos' 91 | print smsg 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | git-manager 2 | =========== 3 | 4 | An interactive command line interface for displaying the state of multiple git 5 | repositories in a single screen. ( screenshots below ) 6 | 7 | - This scripts are for managing multiple git repositories from command line. It 8 | checks the state of 'push'es to remotes and reports the up-to-date'ness of them. 9 | 10 | - It also displays detailed raw output for repositories if there's enough screen 11 | space. Otherwise, you can display detail repo information by switching the 12 | display mode of the manager. 13 | 14 | - Also, support for performing simple git operations on the repos, singly or in a 15 | batch manner will be implemented soon. 16 | 17 | - Adding and Removing repositories to track (editing of the .gmconfig file) will 18 | be made through the interface in future versions 19 | 20 | 21 | file explanation 22 | ================ 23 | 24 | git-manager-gui.py : interactive manager - curses based 25 | 26 | .gmconfig : git manager gui is now configured through the ~/.gmconfig file. 27 | 28 | - Adding a Repository: 29 | 30 | you can add your repos by adding entries like 31 | 32 | repo[kortex][/home/tola/src/cpp/lib/kortex] 33 | repo[KX-OCV][/home/tola/src/cpp/lib/kortex-ext-opencv] 34 | 35 | there are other ways to add repositories 36 | 37 | for example a repository named 'kortex' at location '/home/tola/src/cpp/lib/kortex' 38 | could be included using 39 | 40 | repo[kortex][/home/tola/src/cpp/lib/kortex] 41 | 42 | or 43 | 44 | repo_d[kortex][src/cpp/lib] --> looks inside ${HOME} 45 | 46 | if you have a master directory where you keep all your 47 | projects, you can add all the files under the directory 48 | using: 49 | 50 | repo_dir[/full/path/to/directory][] 51 | 52 | for example: 53 | 54 | repo_dir[/home/tola/src/cpp/applications][] adds 55 | 56 | everything 57 | 58 | - 'seperator' keyword: 59 | 60 | this keyword displays a horizontal line in the display 61 | for visual separation of repositories 62 | 63 | - Defining a remote: 64 | you can define remote names and symbols as: 65 | remote[HEAD][H] 66 | remote[borg][B] 67 | remote[local][L] 68 | 69 | symbols need to be single letters but it is not checked 70 | in this version... 71 | 72 | 73 | some tips: 74 | ========== 75 | 76 | - To display the keybindings, press 'l' 77 | 78 | - Repository information is cached once the application is started. To update it, 79 | press 'u' 80 | 81 | 82 | Some Example Screenshots: 83 | ========================= 84 | 85 | Full Screen View (Enough space in screen to display detailed info for repo): 86 | ![Alt text](/imgs/gmscreenshot.png?raw=true "Screenshot for Full Screen Mode") 87 | 88 | Legend Overlay: 89 | ![Alt text](/imgs/gm-legend.png?raw=true "Legend Overlay") 90 | 91 | Only Summary View is displayed if not enough space is available: 92 | ![Alt text](/imgs/gm-master-view.png?raw=true "Single View") 93 | -------------------------------------------------------------------------------- /helper_scripts/git-local-setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | import os, sys, string, math 3 | import commands 4 | 5 | sys.path.append(os.path.abspath(os.getenv('HOME')+"/src/python/git_manager/")) 6 | sys.path.append(os.path.abspath(os.getenv('HOME')+"/src/python/git_manager/helper_scripts/")) 7 | from gm_resources import * 8 | from utility import * 9 | from gm_custom import * 10 | 11 | local_git_dir= get_home()+'/gitrepos/' 12 | 13 | 14 | if len(sys.argv) == 2: 15 | repo_name=sys.argv[1] 16 | repo_path=get_repository_path(repo_name) 17 | if not is_valid_dir(repo_path): 18 | print 19 | print 'no such directory exists ['+repo_path+']' 20 | print 21 | sys.exit(1) 22 | elif is_valid_dir(get_pwd()+'/.git'): 23 | repo_path = get_pwd() 24 | print 25 | print 'running inside a git repo ['+repo_path+']' 26 | print 27 | repo_name = os.path.basename(get_pwd()) 28 | else: 29 | print 'either run inside a repo folder or provide the name of the repo' 30 | print 'usage: ' + sys.argv[0] + ' repository_name' 31 | sys.exit(1) 32 | 33 | 34 | full_local_path=local_git_dir+repo_name+'.git' 35 | 36 | print 'repo_name : [ '+repo_name.ljust(40)+']' 37 | print 'repo_path : [ '+repo_path.ljust(40)+']' 38 | print 'full_local_path : [ '+full_local_path.ljust(40)+']' 39 | print 40 | 41 | g = generate_git_report( repo_name ) 42 | 43 | # init bare repo if it does not exist 44 | if is_valid_dir(full_local_path) is False: 45 | print 'running on local:' 46 | print '' 47 | print ' mkdir -p '+full_local_path 48 | run_command( 'mkdir -p '+full_local_path ) 49 | print ' cd '+full_local_path 50 | os.chdir( full_local_path ) 51 | print ' git --bare init' 52 | run_command( 'git --bare init' ) 53 | print ' setup done' 54 | os.chdir( repo_path ) 55 | print '' 56 | else: 57 | print '- local repo exists [ '+full_local_path+' ]' 58 | 59 | # add local as remote if it does not exist 60 | if 'local' not in g.remotes: 61 | print 'running: git remote add local '+full_local_path 62 | run_command('git remote add local '+full_local_path) 63 | print 64 | else: 65 | print '- local remote exists' 66 | 67 | print ' push local' 68 | print ' running: git push local master' 69 | run_command('git push local master') 70 | 71 | remotes, rsymbols = get_remote_names() 72 | # print remotes 73 | 74 | # installing remotes if they are mounted 75 | remote_ops=False 76 | for remote in remotes: 77 | mount_point = get_mount_point( remote ) 78 | if len(mount_point) == 0: 79 | continue 80 | print 'REMOTE ['+remote+']' 81 | remote_path = generate_remote_path( remote, repo_name ) 82 | if is_valid_dir( remote_path ) is False: 83 | print ' copying local git repo to the remote' 84 | print ' running \'cp -r '+full_local_path+' '+mount_point+'/backup/gitrepos/\'' 85 | run_command('cp -r '+full_local_path+' '+mount_point+'/backup/gitrepos/') 86 | 87 | if remote not in g.remotes: 88 | print ' running \'git remote add '+remote+' '+remote_path+'\'' 89 | run_command('git remote add '+remote+' '+remote_path) 90 | 91 | print ' running push' 92 | run_command( 'git push '+remote+' master' ) 93 | remote_ops=True 94 | 95 | if not remote_ops: 96 | print '- no external remote detected' 97 | 98 | print 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /helper_scripts/gm_custom.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | import os, sys, string, math 3 | 4 | sys.path.append(os.path.abspath(os.getenv('HOME')+"/src/python/git_manager/")) 5 | 6 | from git_report_structure import * 7 | from gm_resources import * 8 | from utility import * 9 | 10 | def is_valid_repo_dir( repo_name ): 11 | rep_dir = get_repository_path( repo_name ) 12 | return is_valid_dir( rep_dir ) 13 | 14 | def is_valid_remote( rname ): 15 | remotes, rsyms = get_remote_names() 16 | if rname in remotes: 17 | return True 18 | else : 19 | return False 20 | 21 | 22 | def get_repository_path( repo_name ): 23 | 24 | search_paths = get_repositories_search_dirs() 25 | 26 | folder_path = repo_name 27 | for rp in search_paths: 28 | ppath = rp + repo_name 29 | if is_valid_dir( ppath ) is True: 30 | folder_path = ppath 31 | break 32 | return folder_path 33 | 34 | def git_push_remotes( repo_name, remote_list, branch_name ): 35 | 36 | rep_path = get_repository_path( repo_name ) 37 | 38 | os.chdir( rep_path ) 39 | rc, git_st = commands.getstatusoutput('git st') 40 | if rc != 0: 41 | print 'could not find a repository at ['+rep_path+']' 42 | sys.exit(1) 43 | 44 | smsg = '' 45 | for remote in remote_list: 46 | 47 | if is_valid_remote( remote ) is False: 48 | smsg += 'skipping - not a valid remote ['+remote+']\n' 49 | continue 50 | 51 | cmd = 'git push '+ remote + ' ' + branch_name 52 | print repo_name + ' -> ' + cmd 53 | rc, git_st = commands.getstatusoutput(cmd) 54 | if rc != 0: 55 | print 'failed to push ['+remote+']' 56 | smsg += 'problem pushing'.ljust(40) + ': ' + repo + '\n' 57 | print git_st 58 | 59 | print '---------------------------------------------------------' 60 | print smsg 61 | 62 | 63 | def git_push_projects( repo_names, branch_name, remote_name ): 64 | 65 | smsg = '' 66 | 67 | for repo in repo_names: 68 | repo_path = get_repository_path( repo ) 69 | 70 | if is_valid_repo_dir( repo_path ) is False: 71 | smsg += 'skipping push - not a valid directory'.ljust(40) + ': ' + repo + '\n' 72 | continue 73 | 74 | os.chdir( repo_path ) 75 | cmd = 'git push ' + remote_name + ' ' + branch_name 76 | print repo.ljust(24) + ' -> ' + cmd 77 | 78 | g = generate_git_report( repo ) 79 | 80 | if g.isrepo == -1: 81 | smsg += 'skipping push - could not find'.ljust(40) + ': ' + repo + '\n' 82 | sys.exit(1) 83 | if g.commit == 1: 84 | smsg += 'skipping push - dirty commit'.ljust(40) + ': ' + repo + '\n' 85 | continue 86 | 87 | rc, git_staus = commands.getstatusoutput(cmd) 88 | if rc != 0: 89 | smsg += 'problem pushing'.ljust(40) + ': ' + repo + '\n' 90 | print git_staus 91 | continue 92 | 93 | print '---------------------------------------------------------' 94 | print smsg 95 | 96 | def generate_git_report( repo_name ): 97 | if is_valid_repo_dir( repo_name ) is False: 98 | return None 99 | 100 | g = GitReport() 101 | g.repo_name = repo_name 102 | g.path = get_repository_path( repo_name ) 103 | g.parse_status() 104 | g.parse_remote() 105 | g.parse_last_commit_history() 106 | return g 107 | 108 | def generate_remote_path( remote_name, repo_name ): 109 | return get_mount_point(remote_name)+'/backup/gitrepos/'+repo_name+'.git' 110 | -------------------------------------------------------------------------------- /git_report_structure.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | import os, sys, string, re, commands 3 | 4 | class GitReport: 5 | def __init__(self): 6 | self.repo_name = '' 7 | self.path = '' 8 | self.isrepo = -1 9 | self.commit = -1 10 | self.wdc = -1 11 | self.branch = '' 12 | self.ufiles = '' # untracked files 13 | self.cfiles = '' # changed files 14 | self.sfiles = '' # staged files 15 | self.remotes = '' # remote repos 16 | self.lcdate = '' # last commit date 17 | self.lcuser = '' # last commit user 18 | self.lcmsg = '' # last commit message 19 | self.lcrev = '' # last commit revision id 20 | self.lcsyncrem = '' # last commit synced remotes 21 | self.raw_status = '' 22 | self.raw_hist = '' 23 | self.raw_remotes = '' 24 | 25 | def clear(self): 26 | self.isrepo = -1 27 | self.commit = -1 28 | self.wdc = -1 29 | self.branch = '' 30 | self.ufiles = '' # untracked files 31 | self.cfiles = '' # changed files 32 | self.sfiles = '' # staged files 33 | self.remotes = '' # remote repos 34 | self.lcdate = '' # last commit date 35 | self.lcuser = '' # last commit user 36 | self.lcmsg = '' # last commit message 37 | self.lcrev = '' # last commit revision id 38 | self.lcsyncrem = '' # last commit synced remotes 39 | self.raw_status = '' 40 | self.raw_hist = '' 41 | self.raw_remotes = '' 42 | 43 | def display(self): 44 | kwd_sz = 30 45 | print 46 | print 'name'.ljust(kwd_sz)+'['+ self.repo_name.rjust(kwd_sz) + ']' 47 | print 'path'.ljust(kwd_sz)+'['+ self.path.rjust(kwd_sz) + ']' 48 | print 'isrepo'.ljust(kwd_sz)+'['+ str(self.isrepo).rjust(kwd_sz) + ']' 49 | print 'branch'.ljust(kwd_sz)+'['+ self.branch.rjust(kwd_sz) + ']' 50 | if self.commit == 1: 51 | print 'commit'.ljust(kwd_sz)+'['+ str('necessary').rjust(kwd_sz) + ']' 52 | else: 53 | print 'commit'.ljust(kwd_sz)+'['+ str('-').rjust(kwd_sz) + ']' 54 | print 'remotes'.ljust(kwd_sz)+'['+ str( len(self.remotes) ).rjust(kwd_sz) + ']', self.remotes 55 | 56 | if self.wdc == 0: 57 | print 'workdir'.ljust(kwd_sz)+'['+'clean'.rjust(kwd_sz)+']' 58 | else: 59 | print 'workdir'.ljust(kwd_sz)+'['+'dirty'.rjust(kwd_sz)+']' 60 | print 'Staged'.ljust(kwd_sz) + '[' + str( len(self.sfiles) ).rjust(kwd_sz) + ']', self.sfiles 61 | print 'Changed'.ljust(kwd_sz) + '[' + str( len(self.cfiles) ).rjust(kwd_sz) + ']', self.cfiles 62 | print 'Untracked'.ljust(kwd_sz) + '[' + str( len(self.ufiles) ).rjust(kwd_sz) + ']', self.ufiles 63 | print 'Last Commit'.ljust(kwd_sz) + '[ ' + self.lcdate + ' ' + self.lcmsg + ' ' + ']', self.lcsyncrem 64 | print 65 | 66 | def parse_last_commit_history( self ): 67 | rc, git_hists = commands.getstatusoutput('git log --pretty=format:\"%h %ad | %s%d [%an]\" --graph --date=short --max-count=10') 68 | self.raw_hist = git_hists 69 | if rc != 0: 70 | return 71 | git_hists = re.sub(r'\x1b[^m]*m','',git_hists) 72 | last_commit = git_hists.splitlines()[0].strip() 73 | regps = re.match( r'(.*)\|(.*)\((.*)\)(.*)', last_commit, re.M) 74 | if regps: 75 | self.lcrev = regps.group(1).strip().split(' ')[0] 76 | self.lcdate = regps.group(1).strip().split(' ')[1] 77 | self.lcmsg = regps.group(2).strip() 78 | self.lcsyncrem = [ f.strip() for f in filter(None, regps.group(3).strip().replace('/'+self.branch,'').split(',')) ] 79 | self.lcuser = regps.group(4).strip().replace('[','').replace(']','') 80 | 81 | def parse_remote( self ): 82 | if self.path == '': 83 | print 'path not set' 84 | return 85 | os.chdir( self.path ) 86 | rc, self.raw_remotes = commands.getstatusoutput('git remote') 87 | if rc == 0: 88 | self.remotes = self.raw_remotes.splitlines() 89 | # re.split( '\n', raw_remotes ) 90 | 91 | def parse_status( self ): 92 | if self.path == '': 93 | print 'path not set' 94 | return 95 | os.chdir( self.path ) 96 | rc, report = commands.getstatusoutput('git status') 97 | self.raw_status = report 98 | if rc != 0: 99 | return 100 | 101 | if report.find('Not a git repository') != -1: 102 | self.isrepo = 0 103 | else: 104 | self.isrepo = 1 105 | if report.find('branch') != -1: 106 | reout = re.search(r'branch (.*)',report) 107 | if reout: 108 | self.branch = reout.group(1) 109 | 110 | if report.find('working directory clean') != -1: 111 | self.wdc = 0 112 | else : 113 | if report.find('Untracked files:') != -1: 114 | # if this is not an initial commit following should return something 115 | reout = re.split( '(nothing added to commit but untracked files present).*', report, re.M) 116 | ufiles = reout[0] 117 | reout = re.split( '(no changes added to commit).*', ufiles, re.M) 118 | ufiles = reout[0] 119 | # get to te relevant section 120 | reout = re.split( '(Untracked files:).*', ufiles, re.M) 121 | ufiles = reout[2] 122 | reout = re.split( '(what will be committed\)).*', ufiles, re.M) 123 | ufiles = re.sub(r'\x1b[^m]*m','',reout[2]) 124 | ufiles = re.split( '\n', ufiles ) 125 | ufiles = filter( None, ufiles ) 126 | self.ufiles = [ f.replace('\t', '') for f in ufiles] 127 | 128 | if report.find('Changes not staged for commit:') != -1: 129 | # if this is not an initial commit following should return something 130 | reout = re.split( '(nothing added to commit but untracked files present).*', report, re.M) 131 | cfiles = reout[0] 132 | reout = re.split( '(no changes added to commit).*', cfiles, re.M) 133 | cfiles = reout[0] 134 | reout = re.split( '(Untracked files:).*', cfiles, re.M) 135 | cfiles = reout[0] 136 | # get to te relevant section 137 | reout = re.split( '(Changes not staged for commit:).*', cfiles, re.M) 138 | cfiles = reout[2] 139 | reout = re.split( '(discard changes in working directory\)).*', cfiles, re.M) 140 | cfiles = re.sub(r'\x1b[^m]*m','',reout[2]) 141 | cfiles = re.split( '\n', cfiles ) 142 | cfiles = filter( None, cfiles ) 143 | cfiles = [ f.replace('\t', '') for f in cfiles] 144 | self.cfiles = [ f.replace('modified:', '').strip() for f in cfiles] 145 | 146 | if report.find('Changes to be committed:') != -1: 147 | # if this is not an initial commit following should return something 148 | reout = re.split( '(nothing added to commit but untracked files present).*', report, re.M) 149 | sfiles = reout[0] 150 | reout = re.split( '(no changes added to commit).*', sfiles, re.M) 151 | sfiles = reout[0] 152 | reout = re.split( '(Untracked files:).*', sfiles, re.M) 153 | sfiles = reout[0] 154 | reout = re.split( '(Unmerged paths:).*', report, re.M) 155 | sfiles = reout[0] 156 | reout = re.split( '(Changes not staged for commit:).*', sfiles, re.M) 157 | sfiles = reout[0] 158 | # get to te relevant section 159 | reout = re.split( '(Changes to be committed:).*', sfiles, re.M) 160 | sfiles = reout[2] 161 | if sfiles.find( '..." to unstage\)' ) != -1: 162 | reout = re.split( '(..." to unstage\)).*', sfiles, re.M) 163 | sfiles = reout[2] 164 | 165 | sfiles = re.sub(r'\x1b[^m]*m','',reout[2]) 166 | sfiles = re.split( '\n', sfiles ) 167 | sfiles = filter( None, sfiles ) 168 | sfiles = [ f.replace('\t', '') for f in sfiles] 169 | sfiles = [ f.replace('new file:', '').strip() for f in sfiles] 170 | self.sfiles = [ f.replace('modified:', '').strip() for f in sfiles] 171 | 172 | if report.find('Unmerged paths:') != -1: 173 | # if this is not an initial commit following should return something 174 | reout = re.split( '(nothing added to commit but untracked files present).*', report, re.M) 175 | sfiles = reout[0] 176 | reout = re.split( '(no changes added to commit).*', sfiles, re.M) 177 | sfiles = reout[0] 178 | reout = re.split( '(Untracked files:).*', sfiles, re.M) 179 | sfiles = reout[0] 180 | reout = re.split( '(Changes not staged for commit:).*', sfiles, re.M) 181 | sfiles = reout[0] 182 | reout = re.split( '(Unmerged paths:).*', sfiles, re.M) 183 | sfiles = reout[0] 184 | # get to te relevant section 185 | reout = re.split( '(Changes to be committed:).*', sfiles, re.M) 186 | sfiles = reout[2] 187 | if sfiles.find( '..." to unstage\)' ) != -1: 188 | reout = re.split( '(..." to unstage\)).*', sfiles, re.M) 189 | sfiles = reout[2] 190 | 191 | sfiles = re.sub(r'\x1b[^m]*m','',reout[2]) 192 | sfiles = re.split( '\n', sfiles ) 193 | sfiles = filter( None, sfiles ) 194 | sfiles = [ f.replace('\t', '') for f in sfiles] 195 | sfiles = [ f.replace('new file:', '').strip() for f in sfiles] 196 | sfiles = [ f.replace('modified:', '').strip() for f in sfiles] 197 | sfiles = [ f.replace('both modified:', '').strip() for f in sfiles] 198 | self.sfiles += sfiles 199 | 200 | if len( self.sfiles ) + len( self.cfiles ) != 0 : 201 | self.commit = 1 202 | else : 203 | self.commit = 0 204 | 205 | -------------------------------------------------------------------------------- /git-manager-gui.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import sys, os, curses, re, math 4 | 5 | from git_report_structure import * 6 | from utility import * 7 | 8 | CONFIG_FILE = get_home() + '/.gmconfig' 9 | 10 | class Layout: 11 | def __init__(self): 12 | self.xs = 0 # screen coordinates 13 | self.ys = 0 14 | self.xe = 0 15 | self.ye = 0 16 | self.x0 = 0 # starting coordinates for the pad 17 | self.y0 = 0 18 | 19 | def update_screen_coords( self, ys, ye, xs, xe ): 20 | self.xs = xs 21 | self.ys = ys 22 | self.xe = xe 23 | self.ye = ye 24 | 25 | def refresh( self, pad ): 26 | pad.refresh( self.y0, self.x0, self.ys, self.xs, self.ye, self.xe ) 27 | 28 | def erase( self ): 29 | tmp_pad = curses.newpad( self.ye-self.ys+1, self.xe-self.xs+1 ) 30 | tmp_pad.refresh( 0, 0, self.ys, self.xs, self.ye, self.xe ) 31 | 32 | def h( self ): 33 | return self.ye-self.ys+1 34 | 35 | def w( self ): 36 | return self.xe-self.xs+1 37 | 38 | 39 | def num_list_elem( l ): 40 | lsz = len(l) 41 | if lsz == 0: 42 | return '.' 43 | else: 44 | return str(lsz) 45 | 46 | def var_state( var ): 47 | if var: 48 | return '+' 49 | else: 50 | return ' ' 51 | 52 | def rep_state( rname, rlist, tval ): 53 | if rname in rlist: 54 | return tval 55 | else: 56 | return '.' 57 | 58 | def repo_remote_state( g ): 59 | msg = '' 60 | cnt = 0 61 | for remote in remotes: 62 | rsymbol = remote_symbols[cnt] 63 | msg += rep_state( remote, g.lcsyncrem, rsymbol ) 64 | cnt += 1 65 | return msg 66 | 67 | def add_new_repo( scr ): 68 | global msg_pad 69 | curses.echo() 70 | h,w=scr.getmaxyx() 71 | 72 | scr.hline( h-2, 0, curses.ACS_HLINE, w, curses.color_pair(5) ) 73 | scr.addstr( h-1, 0, 'repo-name: ', curses.color_pair(2) ) 74 | 75 | rname = scr.getstr(h-1,11,20) 76 | 77 | cname = re.sub( r'[^A-Za-z \-]', '', rname ) 78 | 79 | if rname != re.sub( r'[^A-Za-z \-]', '', rname ): 80 | msg_pad.addstr( 1, 0, 'cancelled input - not a valid name ['+rname+']', curses.color_pair(2) ) 81 | else: 82 | if rname == '----': 83 | msg_pad.addstr( 1, 0, 'adding separator' ) 84 | config_file_add_new_repo( '----', None ) 85 | reload_repos() 86 | else: 87 | 88 | scr.addstr( h-1, len(rname)+12, 'repo-address: ~/', curses.color_pair(2) ) 89 | rpath = get_home() + '/' + scr.getstr(h-1,len(rname)+28,w-30-len(rname)) # Creates an "input box" at the location (0,1) with an input buffer of 15 chars 90 | 91 | if is_valid_dir(rpath) is True: 92 | msg_pad.addstr( 1, 0, 'added rule repo['+rname+']['+rpath+'] to the '+CONFIG_FILE, curses.A_STANDOUT ); 93 | config_file_add_new_repo( rname, rpath ) 94 | reload_repos() 95 | else: 96 | msg_pad.addstr( 1, 0, 'not a valid path ['+rpath+'] - not added', curses.color_pair(2) ) 97 | 98 | curses.noecho() 99 | 100 | def get_input( scr ): 101 | global msg_pad 102 | curses.echo() # Allows out input to be echo'd to the screen 103 | 104 | # draw_content() 105 | # draw_main( scr ) 106 | # scr.refresh() 107 | h,w=scr.getmaxyx() 108 | 109 | scr.hline( h-2, 0, curses.ACS_HLINE, w, curses.color_pair(5) ) 110 | 111 | cmd = scr.getstr(h-1,1,15) # Creates an "input box" at the location (0,1) with an input buffer of 15 chars 112 | curses.noecho() # Turns echo back off 113 | 114 | msg_pad.addstr( 1, 0, cmd, curses.color_pair(2) ) 115 | 116 | 117 | def render_text( scr, text, y, x, h, w ): 118 | 119 | text = re.sub( r'\x1b[^m]*m','', text ) 120 | text = text.splitlines() 121 | 122 | max_len = 0 123 | c = 0 124 | for st in text: 125 | lstr = len( st ) 126 | if y+c >= h-2: 127 | break 128 | scr.addstr( y+c, x+3, st ) 129 | if w <= (x+5+lstr): 130 | c = c + int( math.ceil((lstr+x+5)/float(w)) ) 131 | else: 132 | c = c + 1 133 | max_len = max( max_len, lstr ) 134 | 135 | y = y+c 136 | return y, max_len 137 | 138 | def draw_repo_details( scr, g ): 139 | 140 | scr.erase() 141 | os.chdir( g.path ) 142 | git_st = re.sub( r'\x1b[^m]*m', '', g.raw_hist ) 143 | 144 | commits = git_st.splitlines() 145 | 146 | h,w = scr.getmaxyx() 147 | 148 | y = 0 149 | x = 1 150 | scr.vline( y, 0, curses.ACS_VLINE, h ) 151 | 152 | scr.addstr( y, x, ''.center(w-x), curses.A_STANDOUT ) 153 | scr.addstr( y+1, x, 'Raw Output'.center(w-x), curses.A_STANDOUT ) 154 | scr.addstr( y+2, x, ''.center(w-x), curses.A_STANDOUT ) 155 | 156 | x = x+1 157 | y = y+4 158 | scr.addstr( y, x, 'Repository'.ljust(14)+': ', curses.color_pair(5) ) 159 | scr.addstr( y, x+16, g.repo_name, curses.color_pair(2) ) 160 | scr.addstr( y+1, x, 'Path'.ljust(14)+': ', curses.color_pair(5) ) 161 | scr.addstr( y+1, x+16, g.path, curses.color_pair(2) ) 162 | 163 | lcd = commits[0].split(' ')[2] 164 | scr.addstr( y+2, x, 'Last Commit'.ljust(14)+': ', curses.color_pair(5) ) 165 | scr.addstr( y+2, x+16, lcd, curses.color_pair(2) ) 166 | 167 | y0 = y+5 168 | y = y0 169 | scr.addstr( y, x, 'Short History', curses.color_pair(5) ) 170 | 171 | y, ml = render_text( scr, g.raw_hist, y+2, x, h, w ) 172 | scr.hline( y0+1, x, curses.ACS_HLINE, w-x-1, curses.color_pair(5) ) 173 | scr.hline( y, x, curses.ACS_HLINE, w-x-1, curses.color_pair(5) ) 174 | 175 | y0 = y+2 176 | y = y0 177 | scr.addstr( y, x, 'Status', curses.color_pair(5) ) 178 | scr.hline ( y+1, x, curses.ACS_HLINE, w-x-1, curses.color_pair(5) ) 179 | y, ml = render_text( scr, g.raw_status, y+2, x, h, w ) 180 | 181 | y0 = y+2 182 | y = y0 183 | scr.addstr( y, x, 'Remotes', curses.color_pair(5) ) 184 | scr.hline ( y+1, x, curses.ACS_HLINE, w-x-1, curses.color_pair(5) ) 185 | y, ml = render_text( scr, g.raw_remotes, y+2, x, h, w ) 186 | 187 | def draw_repo_man_screen( scr ): 188 | scr.erase() 189 | y = 0 190 | x = 0 191 | h,w = scr.getmaxyx() 192 | scr.addstr( y, x, 'Repositories', curses.color_pair(5) ) 193 | scr.hline( y+1, x, curses.ACS_HLINE, w-2*x, curses.color_pair(5) ) 194 | 195 | rpsz = LGD[1][1] 196 | 197 | n_legs = len( LGD ) 198 | 199 | leg = LGD[0] 200 | scr.addstr( y, x, ''.center(leg[1]), curses.A_STANDOUT ) 201 | scr.addstr( y+1, x, 'ID'.center(leg[1]), curses.A_STANDOUT ) 202 | scr.addstr( y+2, x, ''.center(leg[1]), curses.A_STANDOUT ) 203 | x = x+leg[1]+1 204 | scr.vline( y, x-1, curses.ACS_VLINE, 3 , curses.A_STANDOUT ) 205 | scr.vline( y+3, x-1, curses.ACS_VLINE, NR ) 206 | 207 | leg = LGD[1] 208 | scr.addstr( y, x, ''.center(leg[1]), curses.A_STANDOUT ) 209 | scr.addstr( y+1, x, leg[0].rjust(leg[1]), curses.A_STANDOUT ) 210 | scr.addstr( y+2, x, ''.center(leg[1]), curses.A_STANDOUT ) 211 | x = x+leg[1]+1 212 | scr.vline( y, x-1, curses.ACS_VLINE, 3 , curses.A_STANDOUT ) 213 | scr.vline( y+3, x-1, curses.ACS_VLINE, NR ) 214 | 215 | scr.addstr( y, x, ''.center(w-x), curses.A_STANDOUT ) 216 | scr.addstr( y+1, x, ' PATH'.ljust(w-x), curses.A_STANDOUT ) 217 | scr.addstr( y+2, x, ''.center(w-x), curses.A_STANDOUT ) 218 | 219 | y = y + 3 220 | cnt = 0 221 | for g in GRepos: 222 | x = 0 223 | if g.repo_name == "": 224 | scr.hline( y+cnt, x, curses.ACS_HLINE, w-2*x, curses.color_pair(5) ) 225 | else: 226 | leg = LGD[0] 227 | scr.addstr( y+cnt, x, (str(cnt)+' ').rjust(leg[1]) ) 228 | x = x + leg[1]+1 229 | scr.addstr( y+cnt, x, (g.repo_name+' ').rjust( rpsz ), curses.color_pair(5) ) 230 | scr.addstr( y+cnt, x+rpsz+2, g.path ) 231 | cnt = cnt+1 232 | 233 | 234 | 235 | def show_message( scr, msg ): 236 | global display_mode 237 | scr.erase() 238 | if display_mode == 'single': 239 | scr.addstr( ML.h()/2, max(0,ML.w()/2-len(msg)/2), msg, curses.color_pair(2) ) 240 | ML.refresh( scr ) 241 | elif display_mode == 'split': 242 | scr.addstr( S0.h()/2, max(0,S0.w()/2-len(msg)/2), msg, curses.color_pair(2) ) 243 | S0.refresh( scr ) 244 | 245 | 246 | def draw_legend( scr ): 247 | y = 1 248 | x = 2 249 | h,w = scr.getmaxyx() 250 | scr.addstr( y, x, 'Keybindings', curses.color_pair(5) ) 251 | scr.hline ( y+1, x, curses.ACS_HLINE, w-2*x, curses.color_pair(5) ) 252 | scr.addstr( y+2, x+5, 'm'.ljust(5)+': return to main screen' ) 253 | scr.addstr( y+3, x+5, 'd'.ljust(5)+': detailed output for repo toggle (possible only in single view)' ) 254 | scr.addstr( y+4, x+5, 'u'.ljust(5)+': update git reports' ) 255 | scr.addstr( y+5, x+5, 'r'.ljust(5)+': repository management view' ) 256 | scr.addstr( y+6, x+5, 'l'.ljust(5)+': display legend - toggles' ) 257 | 258 | y = y + 8 259 | scr.addstr( y, x, 'Legend', curses.color_pair(5) ) 260 | scr.hline ( y+1, x, curses.ACS_HLINE, w-2*x, curses.color_pair(5) ) 261 | 262 | y = y+1 263 | 264 | cnt = 0 265 | for remote in remotes: 266 | rsymbol = remote_symbols[cnt] 267 | ys = cnt/3 + 1 268 | xs = 15*(cnt%3) + 5 269 | scr.addstr( y+ys, x+xs, (rsymbol+': '+remote).ljust(15) ) 270 | cnt += 1 271 | 272 | ys = len(remotes)/3 + 3 273 | scr.addstr( y+ys, x+5, 'S: Staged'.ljust(15) + 'C: Changed'.ljust(15) + 'U: Untracked'.ljust(15) ) 274 | scr.border(0) 275 | 276 | 277 | def draw_footer( scr, st ): 278 | h,w = scr.getmaxyx() 279 | scr.hline ( h-3, 1, curses.ACS_HLINE, w-2, curses.color_pair(5) ) 280 | scr.addstr( h-2, 1, ''.center(w), curses.color_pair(5) ) 281 | scr.addstr( h-2, 1, st, curses.color_pair(5) ) 282 | 283 | def draw_selection( scr ): 284 | scr.move( selected_rep_id+3, 0 ) 285 | curr_y, curr_x = scr.getyx() 286 | scr.chgat( curr_y, curr_x+LGD[0][1]+2, LGD[1][1]-1, curses.color_pair(2) | curses.A_BOLD ) 287 | scr.move( 3+selected_rep_id, curr_x + LGD[0][1] + LGD[1][1] +1 ) 288 | 289 | def draw_main( scr ): 290 | scr.erase() 291 | h,w = scr.getmaxyx() 292 | 293 | y = 0 294 | x = 0 295 | 296 | # draw the legend 297 | n_legs = len( LGD ) 298 | li = 1 299 | for leg in LGD: 300 | scr.addstr( y, x, ''.center(leg[1]), curses.A_STANDOUT ) 301 | if leg[2] == 'Left': 302 | scr.addstr( y+1, x, leg[0].ljust(leg[1]), curses.A_STANDOUT ) 303 | elif leg[2] == 'Center': 304 | scr.addstr( y+1, x, leg[0].center(leg[1]), curses.A_STANDOUT ) 305 | elif leg[2] == 'Right': 306 | scr.addstr( y+1, x, leg[0].rjust(leg[1]), curses.A_STANDOUT ) 307 | else: 308 | assert( 0 > 1) 309 | 310 | scr.addstr( y+2, x, ''.center(leg[1]), curses.A_STANDOUT ) 311 | x = x+leg[1]+1 312 | if li != n_legs: 313 | scr.vline( y, x-1, curses.ACS_VLINE, 3 , curses.A_STANDOUT ) 314 | scr.vline( y+3, x-1, curses.ACS_VLINE, NR ) 315 | li = li+1 316 | 317 | # display repo reports 318 | y = y + 3 319 | 320 | cnt = 0 321 | for g in GRepos: 322 | li = 0 323 | x = 0 324 | if g.repo_name == "": 325 | scr.hline( y+cnt, x, curses.ACS_HLINE, w-2 ) 326 | else: 327 | for leg in LGD: 328 | if li == 0: 329 | scr.addstr( y+cnt, x, (str(cnt)+' ').rjust(leg[1]) ) 330 | elif li == 1: 331 | scr.addstr( y+cnt, x, (g.repo_name+' ').rjust(leg[1]) ) 332 | elif li == 2: 333 | scr.addstr( y+cnt, x, repo_remote_state(g).center(leg[1]) ) 334 | elif li == 3: 335 | scr.addstr( y+cnt, x, num_list_elem(g.sfiles).center(leg[1]) ) 336 | elif li == 4: 337 | scr.addstr( y+cnt, x, num_list_elem(g.cfiles).center(leg[1]) ) 338 | elif li == 5: 339 | scr.addstr( y+cnt, x, num_list_elem(g.ufiles).center(leg[1]) ) 340 | elif li == 6: 341 | scr.addstr( y+cnt, x, g.branch.center(leg[1]) ) 342 | elif li == 7: 343 | scr.addstr( y+cnt, x, var_state(g.commit).center(leg[1]) ) 344 | x = x + leg[1]+1 345 | li = li+1 346 | cnt = cnt+1 347 | 348 | def cache_git_reports(): 349 | global GRepos 350 | for (cnt, g) in enumerate( GRepos ): 351 | if g.repo_name != "": 352 | GRepos[cnt].clear() 353 | GRepos[cnt].parse_status() 354 | GRepos[cnt].parse_remote() 355 | GRepos[cnt].parse_last_commit_history() 356 | 357 | def update_layouts(): 358 | global ML, S0, S1, L0, F0, display_mode 359 | global main_pad, supp_pad, msg_pad 360 | 361 | h,w = stdscr.getmaxyx() 362 | if w < 140: 363 | display_mode = 'single' 364 | else: 365 | display_mode = 'split' 366 | 367 | assert w>mw, 'Require a window bigger than width %r - Got only %r' % (mw, w) 368 | 369 | 370 | if display_mode == 'single': 371 | S0.update_screen_coords( 0, h-1, 0, mw ) 372 | S1.update_screen_coords( 0, h-1, mw, w-1 ) 373 | 374 | if active_screen == 0: 375 | main_pad = curses.newpad( pad_h, mw ) 376 | sw = (w-mw)/2 377 | sh = (h-(NR+3))/2 378 | ML.update_screen_coords( max(0,sh), min(h-1,sh+NR+3), max(0,sw), min(w-1,sw+mw) ) 379 | elif active_screen == 1: 380 | main_pad = curses.newpad( pad_h, w ) 381 | ML.update_screen_coords( 0, h-1, 0, w-1 ) 382 | elif active_screen == 3: 383 | main_pad = curses.newpad( pad_h, w ) 384 | ML.update_screen_coords( 0, h-1, 0, w-1 ) 385 | else: 386 | ML.update_screen_coords( 0, h-1, 0, w-1 ) 387 | S0.update_screen_coords( 0, h-1, 0, mw ) 388 | S1.update_screen_coords( 0, h-1, mw, w-1 ) 389 | 390 | if active_screen == 0 or active_screen == 1 or active_screen == 2: 391 | main_pad = curses.newpad( pad_h, mw ) 392 | supp_pad = curses.newpad( pad_h, w-mw ) 393 | elif active_screen == 3: 394 | main_pad = curses.newpad( pad_h, w-1 ) 395 | 396 | msg_pad = curses.newpad( min(h,msg_h), w ) 397 | F0.update_screen_coords( max(0,h-msg_h), h-1, 0, w-1 ) 398 | 399 | if h < lgh or w < lgw: 400 | leg_pad = curses.newpad( h, w ) 401 | sh = (h-1-lgh)/2 402 | sw = (w-1-lgw)/2 403 | L0.update_screen_coords( max(0,sh), min(h-1,sh+lgh), max(0,sw), min(w-1,sw+lgw) ) 404 | 405 | main_pad.keypad(True) 406 | 407 | ML.erase() 408 | S0.erase() 409 | S1.erase() 410 | F0.erase() 411 | 412 | def draw_content(): 413 | global main_pad, supp_pad 414 | global ML, S0, S1 415 | 416 | if display_mode == 'single': 417 | if active_screen == 0: 418 | draw_main( main_pad ) 419 | draw_selection( main_pad ) 420 | ML.refresh( main_pad ) 421 | elif active_screen == 1: 422 | draw_repo_details( main_pad, GRepos[selected_rep_id] ) 423 | ML.refresh( main_pad ) 424 | elif active_screen == 3: 425 | draw_repo_man_screen( main_pad ) 426 | draw_selection( main_pad ) 427 | ML.refresh( main_pad ) 428 | elif active_screen == 2: 429 | print 'cannot draw two pads in one small screen' 430 | print 'should not have entered this loop' 431 | assert( 0 > 1 ) 432 | 433 | elif display_mode == 'split': 434 | if active_screen == 2 or active_screen == 0 or active_screen == 1: 435 | draw_main( main_pad ) 436 | draw_selection( main_pad ) 437 | draw_repo_details( supp_pad, GRepos[selected_rep_id] ) 438 | S0.refresh( main_pad ) 439 | S1.refresh( supp_pad ) 440 | elif active_screen == 3: 441 | draw_repo_man_screen( main_pad ) 442 | draw_selection( main_pad ) 443 | ML.refresh( main_pad ) 444 | 445 | def config_file_add_new_repo( repo_name, repo_path ): 446 | f = open( CONFIG_FILE, 'a' ) 447 | if repo_path == None and repo_name == '----': 448 | f.write( 'separator\n' ) 449 | else: 450 | f.write( 'repo['+repo_name+']['+repo_path+']\n' ) 451 | f.close() 452 | 453 | def reload_repos(): 454 | global GRepos, remotes, remote_symbols, NR 455 | GRepos, remotes, remote_symbols = load_config_file() 456 | cache_git_reports() 457 | 458 | def load_config_file(): 459 | global NR 460 | if is_valid_file(CONFIG_FILE) is False: 461 | print 'could not find ['+CONFIG_FILE+'] - generating dummy prototype ' 462 | print 'there is an example .gmconfig file named gmconfig-example in the git_manager directory' 463 | f = open( CONFIG_FILE, 'w' ) 464 | f.write('repo['+'repo_name'+'][repo_full_path]' '\n') 465 | f.write('remote['+'HEAD'+'][H]' '\n') 466 | f.write('remote['+'local'+'][L]' '\n') 467 | f.close() 468 | sys.exit(1) 469 | return None 470 | 471 | f = open( CONFIG_FILE, 'r+' ) 472 | 473 | G = [] 474 | remotes = [] 475 | remote_symbols = [] 476 | 477 | for r in f: 478 | r = r.strip() 479 | if r == "": 480 | continue 481 | if r[0] == '#': 482 | print 'Skipping Comment: ['+r+']' 483 | continue 484 | elif r == 'separator': 485 | repo = GitReport() 486 | repo.repo_name = "" 487 | repo.path = "" 488 | G.append(repo) 489 | else : 490 | regps = re.match( r'(.*)\[(.*)\]\[(.*)\]', r, re.M) 491 | if regps: 492 | if regps.group(1) == 'repo': 493 | if is_valid_dir(regps.group(3)) is False: 494 | print 'Not a valid repo: ['+regps.group(2)+']['+regps.group(3)+'] - Skipping ' 495 | continue 496 | repo = GitReport() 497 | repo.repo_name = regps.group(2) 498 | repo.path = regps.group(3) 499 | G.append( repo ) 500 | if regps.group(1) == 'remote': 501 | remotes.append( regps.group(2) ) 502 | remote_symbols.append( regps.group(3) ) 503 | if regps.group(1) == 'repo_dir': 504 | folder = regps.group(2) 505 | rnames = get_folders( folder ) 506 | for x in rnames: 507 | repo = GitReport() 508 | repo.repo_name = x 509 | repo.path = folder + '/' + x 510 | G.append( repo ) 511 | if regps.group(1) == 'repo_d': 512 | folder = get_home()+'/'+regps.group(3) 513 | repo = GitReport() 514 | repo.repo_name = regps.group(2) 515 | repo.path = folder + '/' + regps.group(2) 516 | G.append( repo ) 517 | 518 | NR = len( G ) 519 | f.close() 520 | return G, remotes, remote_symbols 521 | 522 | 523 | # 524 | # initialize main window legend 525 | # 526 | NR = 0 527 | GRepos, remotes, remote_symbols = load_config_file() 528 | 529 | cache_git_reports() 530 | 531 | 532 | remote_header = '' 533 | for rsym in remote_symbols: 534 | remote_header += rsym 535 | LGD = ( ('ID ' , 4, 'Right'), 536 | ('Repositories ' , 25, 'Right'), 537 | (remote_header , 10, 'Center'), 538 | ('S ' , 3, 'Right'), 539 | ('C ' , 3, 'Right'), 540 | ('U ' , 3, 'Right'), 541 | ('Branch' , 10, 'Center'), 542 | ('Dirty' , 10, 'Center') ) 543 | 544 | mw = len( LGD )-1 545 | for (i, leg) in enumerate( LGD ): 546 | mw += leg[1] 547 | 548 | 549 | if NR == 0: 550 | print ' No repo configured' 551 | print 552 | print ' Create the '+CONFIG_FILE+' file and add entries like:' 553 | print 554 | print ' repo[repo_name][repo_full_path]' 555 | print ' remote[remote_name][remote_symbol]' 556 | print 557 | print ' For example:' 558 | print 559 | print ' repo[kortex][/home/tola/src/cpp/lib/kortex]' 560 | print ' repo[KX-OCV][/home/tola/src/cpp/lib/kortex-ext-opencv]' 561 | print ' remote[HEAD][H]' 562 | print ' remote[borg][B]' 563 | print ' remote[local][L]' 564 | print 565 | print 566 | sys.exit(1) 567 | 568 | msg_h = 2 569 | msg_w = 80 570 | 571 | lgh = 20 572 | lgw = 80 573 | 574 | selected_rep_id = 0 575 | 576 | 577 | try: 578 | 579 | stdscr = curses.initscr() 580 | stdscr.erase() 581 | curses.cbreak() 582 | curses.noecho() 583 | 584 | curses.start_color() 585 | curses.use_default_colors() 586 | for i in range(0, curses.COLORS): 587 | curses.init_pair(i + 1, i, -1) 588 | 589 | h,w = stdscr.getmaxyx() 590 | 591 | pad_h = 200 592 | 593 | main_pad = curses.newpad( pad_h, mw ) 594 | main_pad.keypad(True) 595 | main_pad.border(0) 596 | main_pad.addstr(0, 0, 'main' ) 597 | 598 | supp_pad = curses.newpad( pad_h, 100 ) 599 | supp_pad.keypad(True) 600 | supp_pad.border(0) 601 | supp_pad.addstr(0, 0, 'support' ) 602 | 603 | leg_pad = curses.newpad(lgh,lgw) 604 | leg_pad.border(0) 605 | 606 | msg_pad = curses.newpad( msg_h, w ) 607 | 608 | ML = Layout() 609 | S0 = Layout() 610 | S1 = Layout() 611 | L0 = Layout() 612 | F0 = Layout() 613 | 614 | active_screen = 0 615 | display_mode = 'single' 616 | 617 | update_layouts() 618 | stdscr.refresh() 619 | 620 | update_main = False 621 | 622 | dl = 0 623 | k = 0 624 | while k!= ord('q'): 625 | 626 | update_support = False 627 | update_main = False 628 | 629 | if k == curses.KEY_RESIZE: 630 | update_layouts() 631 | stdscr.refresh() # makes sure during get_input screen is not empty 632 | 633 | elif k == curses.KEY_DOWN: 634 | selected_rep_id = selected_rep_id + 1 635 | if selected_rep_id >= NR-1: 636 | selected_rep_id=NR-1 637 | if GRepos[selected_rep_id].repo_name == "": 638 | selected_rep_id=selected_rep_id+1 639 | if selected_rep_id >= NR-1: 640 | selected_rep_id=NR-1 641 | 642 | elif k == curses.KEY_UP: 643 | selected_rep_id = selected_rep_id - 1 644 | if selected_rep_id < 0: 645 | selected_rep_id=0 646 | if GRepos[selected_rep_id].repo_name == "": 647 | selected_rep_id=selected_rep_id-1 648 | if selected_rep_id < 0: 649 | selected_rep_id=0 650 | 651 | elif k == ord('d'): 652 | if display_mode == 'single': 653 | if active_screen == 1: 654 | active_screen = 0 655 | elif active_screen == 0: 656 | active_screen = 1 657 | if active_screen == 3: 658 | active_screen = 1 659 | update_layouts() 660 | 661 | elif k == ord('r'): 662 | active_screen = 3 663 | update_layouts() 664 | 665 | elif k == ord('m'): 666 | active_screen = 0 667 | update_layouts() 668 | 669 | elif k == ord('u'): 670 | show_message( main_pad, 'Updating Repo Info') 671 | cache_git_reports() 672 | 673 | elif k == ord('l') or k == ord('h'): 674 | dl = (dl+1)%2 675 | if dl==0: 676 | L0.erase() 677 | 678 | elif k == ord('c'): 679 | get_input( stdscr ) 680 | 681 | elif active_screen == 3 and k == ord('a'): 682 | add_new_repo( stdscr ) 683 | 684 | draw_content() 685 | 686 | if dl == 1: 687 | draw_legend( leg_pad ) 688 | L0.refresh( leg_pad ) 689 | 690 | h,w = stdscr.getmaxyx() 691 | msg_pad.hline( 0, 0, curses.ACS_HLINE, w, curses.color_pair(5) ) 692 | msg_pad.addstr( 0, 0, str(NR) ) 693 | 694 | msg_pad.addstr( 1, w-4, str(w) ) 695 | F0.refresh( msg_pad ) 696 | # curses.napms(100) 697 | 698 | k = main_pad.getch() 699 | msg_pad.erase() 700 | # F0.erase() 701 | 702 | finally: 703 | curses.nocbreak() 704 | stdscr.keypad(False) 705 | curses.echo() 706 | curses.endwin() 707 | --------------------------------------------------------------------------------