├── pygit ├── __version__.py ├── __main__.py ├── __init__.py ├── test.py └── pygit.py ├── MANIFEST.in ├── .gitignore ├── Pipfile ├── LICENSE.txt ├── setup.py ├── README.md └── Pipfile.lock /pygit/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2019.01.31" -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include license.txt -------------------------------------------------------------------------------- /pygit/__main__.py: -------------------------------------------------------------------------------- 1 | from .pygit import initialize 2 | 3 | if __name__ == "__main__": 4 | initialize() 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | .vscode/ 3 | *.pyc 4 | status/ 5 | pygit_logger.log 6 | dist/ 7 | build/ 8 | python_git.egg-info/ 9 | PYGIT_SHELF/ 10 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | coverage = "==4.5.1" 8 | "send2trash" = "==1.5.0" 9 | pytest = "*" 10 | 11 | [dev-packages] 12 | twine = "*" 13 | pylint = "*" 14 | 15 | [requires] 16 | python_version = "3.6" 17 | -------------------------------------------------------------------------------- /pygit/__init__.py: -------------------------------------------------------------------------------- 1 | from .__version__ import __version__ 2 | from .pygit import ( 3 | cleanup, check_git_support, is_git_repo, initialize, update, 4 | Commands, repos, load, load_multiple, pull, push, all_status 5 | ) 6 | 7 | __all__ = ['all_status', 'cleanup', 'check_git_support', 'is_git_repo', 'initialize', 'update', 8 | 'Commands', 'repos', 'load', 'load_multiple', 'pull', 'push'] 9 | -------------------------------------------------------------------------------- /pygit/test.py: -------------------------------------------------------------------------------- 1 | """Test API""" 2 | 3 | import json 4 | import os 5 | import random 6 | import unittest 7 | 8 | # import pytest 9 | from . import pygit 10 | 11 | from pathlib import Path 12 | from glob import glob 13 | from random import choice 14 | from send2trash import send2trash 15 | # import inspect 16 | 17 | # from .main import ( 18 | # USERHOME, DESKTOP, STATUS_DIR, BASE_DIR, SHELF_DIR, TEST_DIR, TIME_STAMP, 19 | # cleanup, check_git_support, is_git_repo, need_attention, initialize, 20 | # Commands, repos, load, load_multiple, pull, push, all_status 21 | # ) 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Chidi Orji 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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup""" 2 | 3 | import os 4 | import sys 5 | import site 6 | 7 | from pathlib import Path 8 | from subprocess import call 9 | from setuptools import setup 10 | 11 | def readme(): 12 | """Readme""" 13 | with open("README.md") as rhand: 14 | return rhand.read() 15 | 16 | 17 | setup(name='python-git', 18 | version='2018.01.08', 19 | description='Automate boring git tasks', 20 | long_description=readme(), 21 | classifiers=[ 22 | 'Development Status :: 5 - Production/Stable', 23 | 'Programming Language :: Python :: 3.6', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Intended Audience :: Developers', 26 | 'Natural Language :: English', 27 | 'Operating System :: POSIX :: Linux', 28 | 'Operating System :: Microsoft :: Windows :: Windows 10', 29 | 'Topic :: Software Development :: Version Control :: Git', 30 | 'Topic :: Utilities', 31 | ], 32 | keywords='automate boring git and github tasks', 33 | url='https://github.com/chidimo/python-git', 34 | download_url='https://github.com/chidimo/python-git/archive/master.zip', 35 | author='Chidi Orji', 36 | author_email='orjichidi95@gmail.com', 37 | license='MIT', 38 | packages=['pygit'], 39 | install_requires=[ 40 | 'send2trash' 41 | ], 42 | zip_safe=False,) 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python-Git 2 | 3 | Automate the boring git stuff with python 4 | 5 | ## Motivation 6 | 7 | Whenever I wanted to see the status of all my git repos I have to fire up the 8 | `git-cmd.exe` shell on windows, navigate to each folder and then do a `git status`. 9 | I have to do this both at home and at work. 10 | 11 | But I got quickly tired of it. So I decided to make this tool to give me a quick 12 | report so I can see what is ahead and what's behind and what's ahead at a glance. 13 | In short, what needs attention so as to avoid those troubling merge conflicts. 14 | 15 | ## Requirements 16 | 17 | Other thing you need is a computer with `git` either accessible from the command line (which means its in your system path) or as a standalone file somewhere in your system. 18 | If you're working without installation rights, you can use a portable `git` and `python-git` will work just fine. 19 | 20 | You can get a portable git version from [here](https://git-scm.com/download/win) 21 | 22 | Just unzip it and place it somewhere on your disk. Later (during initialization), you'll need to tell `python-git` where this file is located. 23 | 24 | ## Installation 25 | 26 | pip install python-git 27 | 28 | ## Setup 29 | 30 | After installation, an initial setup is required to tell `pygit` the folders it needs to work with. Open a terminal and `python -m pygit` the below line with appropriate command line arguments. 31 | 32 | The output of `python -m pygit --help` is shown below. 33 | 34 | ```cmd 35 | usage: Pygit. Initialize working directories for python-git 36 | [-h] [-v {0,1}] [-r RULES [RULES ...]] [-g GITPATH] 37 | [-m MASTERDIRECTORY] [-s SIMPLEDIRECTORY [SIMPLEDIRECTORY ...]] 38 | 39 | optional arguments: 40 | -h, --help show this help message and exit 41 | -v {0,1}, --verbosity {0,1} 42 | turn verbosity ON/OFF 43 | -r RULES [RULES ...], --rules RULES [RULES ...] 44 | Set a list of string patterns for folders to skip 45 | during setup 46 | -g GITPATH, --gitPath GITPATH 47 | Full pathname to git executable. cmd or bash. 48 | -m MASTERDIRECTORY, --masterDirectory MASTERDIRECTORY 49 | Full pathname to directory holding any number of git 50 | repos. 51 | -s SIMPLEDIRECTORY [SIMPLEDIRECTORY ...], --simpleDirectory SIMPLEDIRECTORY [SIMPLEDIRECTORY ...] 52 | A list of full pathnames to any number of individual 53 | git repos. 54 | ``` 55 | 56 | As an example you I have a folder in my `D:` drive that holds all my git repos, so I will setup `pygit` with the following command 57 | 58 | python -m pygit --m D:\git -v 1 59 | 60 | If it happens that you clone more repos into your master directory, you may update the index by issuing the `update()`command inside a `python` shell. 61 | 62 | pygit.update() 63 | 64 | ## Usage 65 | 66 | Activate python environment on command line. 67 | 68 | import pygit 69 | 70 | In case things change (perhaps you moved folders around or you add a new git repo) and you want to reset your folders just redo the initialization step 71 | 72 | pygit.repos() 73 | 74 | show all git repos in the format shown immediately below 75 | 76 | pygit.load(repo_id_or_name) # load a repo 77 | 78 | where `repo_id` is a string-valued id assigned to that particular repo. The first value in the `repos` command's output. 79 | 80 | The `load(input_string)` command returns a `Commands` object for that repo, which provides a gateway for issuing git commands on the repository 81 | 82 | Operations that can be performed on `Commands` object are shown below. 83 | 84 | ```python 85 | r = pygit.load_repo(repo_id_or_name) 86 | r.fetch() # perform fetch 87 | r.status() # see status 88 | r.add_all() # stage all changes for commit 89 | r.commit(message='chore: minor changes') # commit changes. Press enter to accept default message 90 | r.push() # perform push action 91 | r.pull() # perform pull request 92 | r.add_commit() # add and commit at once 93 | ``` 94 | 95 | ### Batch Operations 96 | 97 | The following batch operations on indexed repos are available. 98 | 99 | pygit.load_multiple(*args) # load a set of repos 100 | pygit.load_multiple("2", "5") # load only repo 2 and 5 101 | 102 | returns a `generator` of `Commands` objects for repositories 2 and 5. Afterwards you can iterate over the repos like below 103 | 104 | ```python 105 | for each in pygit.load_multiple("2", "5"): 106 | each.add_commit() 107 | ``` 108 | 109 | pygit.all_status() 110 | 111 | performs a `status` command on all indexed repos. The result is written to a markdown file. 112 | Carries a timestamp of the time the command was issued. Call it a snapshot of your repo status if you will. Items which are out of sync with their remote counterpart are also highlighted as needing attention. 113 | 114 | pygit.pull_all() 115 | 116 | perform a `pull` request on all indexed repos at once. It returns `None`. 117 | 118 | pygit.push_all() 119 | 120 | performs a `push` action on all indexed repos at once. It returns `None`. 121 | 122 | pygit.load_all() 123 | 124 | returns a `generator` of `Commands` object for each indexed repo. 125 | 126 | ## To do 127 | 128 | 1. Add `git-bash.exe` 129 | 1. Implement `Commands.branch()` 130 | 1. Refactor tests 131 | 1. Auto-run test after importation to make sure every other thing works fine. 132 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "c9930b04aa3a62c243f997d7b2b03c03d04913e82a4428833e23394f93f2bdd4" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "atomicwrites": { 20 | "hashes": [ 21 | "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", 22 | "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" 23 | ], 24 | "version": "==1.2.1" 25 | }, 26 | "attrs": { 27 | "hashes": [ 28 | "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", 29 | "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" 30 | ], 31 | "version": "==18.2.0" 32 | }, 33 | "colorama": { 34 | "hashes": [ 35 | "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", 36 | "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" 37 | ], 38 | "markers": "sys_platform == 'win32'", 39 | "version": "==0.4.1" 40 | }, 41 | "coverage": { 42 | "hashes": [ 43 | "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", 44 | "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", 45 | "sha256:0bf8cbbd71adfff0ef1f3a1531e6402d13b7b01ac50a79c97ca15f030dba6306", 46 | "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95", 47 | "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", 48 | "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd", 49 | "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", 50 | "sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1", 51 | "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", 52 | "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", 53 | "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", 54 | "sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", 55 | "sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", 56 | "sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", 57 | "sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", 58 | "sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", 59 | "sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", 60 | "sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", 61 | "sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", 62 | "sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", 63 | "sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", 64 | "sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", 65 | "sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", 66 | "sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", 67 | "sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", 68 | "sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", 69 | "sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", 70 | "sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", 71 | "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", 72 | "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", 73 | "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", 74 | "sha256:f05a636b4564104120111800021a92e43397bc12a5c72fed7036be8556e0029e", 75 | "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80" 76 | ], 77 | "index": "pypi", 78 | "version": "==4.5.1" 79 | }, 80 | "more-itertools": { 81 | "hashes": [ 82 | "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", 83 | "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", 84 | "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9" 85 | ], 86 | "version": "==5.0.0" 87 | }, 88 | "pluggy": { 89 | "hashes": [ 90 | "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", 91 | "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" 92 | ], 93 | "version": "==0.8.0" 94 | }, 95 | "py": { 96 | "hashes": [ 97 | "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", 98 | "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" 99 | ], 100 | "version": "==1.7.0" 101 | }, 102 | "pytest": { 103 | "hashes": [ 104 | "sha256:f689bf2fc18c4585403348dd56f47d87780bf217c53ed9ae7a3e2d7faa45f8e9", 105 | "sha256:f812ea39a0153566be53d88f8de94839db1e8a05352ed8a49525d7d7f37861e9" 106 | ], 107 | "index": "pypi", 108 | "version": "==4.0.2" 109 | }, 110 | "send2trash": { 111 | "hashes": [ 112 | "sha256:60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2", 113 | "sha256:f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b" 114 | ], 115 | "index": "pypi", 116 | "version": "==1.5.0" 117 | }, 118 | "six": { 119 | "hashes": [ 120 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 121 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 122 | ], 123 | "version": "==1.12.0" 124 | } 125 | }, 126 | "develop": { 127 | "astroid": { 128 | "hashes": [ 129 | "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", 130 | "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" 131 | ], 132 | "version": "==2.1.0" 133 | }, 134 | "bleach": { 135 | "hashes": [ 136 | "sha256:48d39675b80a75f6d1c3bdbffec791cf0bbbab665cf01e20da701c77de278718", 137 | "sha256:73d26f018af5d5adcdabf5c1c974add4361a9c76af215fe32fdec8a6fc5fb9b9" 138 | ], 139 | "version": "==3.0.2" 140 | }, 141 | "certifi": { 142 | "hashes": [ 143 | "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", 144 | "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" 145 | ], 146 | "version": "==2018.11.29" 147 | }, 148 | "chardet": { 149 | "hashes": [ 150 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 151 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 152 | ], 153 | "version": "==3.0.4" 154 | }, 155 | "colorama": { 156 | "hashes": [ 157 | "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", 158 | "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" 159 | ], 160 | "markers": "sys_platform == 'win32'", 161 | "version": "==0.4.1" 162 | }, 163 | "docutils": { 164 | "hashes": [ 165 | "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", 166 | "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", 167 | "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" 168 | ], 169 | "version": "==0.14" 170 | }, 171 | "idna": { 172 | "hashes": [ 173 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 174 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 175 | ], 176 | "version": "==2.8" 177 | }, 178 | "isort": { 179 | "hashes": [ 180 | "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", 181 | "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", 182 | "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" 183 | ], 184 | "version": "==4.3.4" 185 | }, 186 | "lazy-object-proxy": { 187 | "hashes": [ 188 | "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", 189 | "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", 190 | "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", 191 | "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", 192 | "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", 193 | "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", 194 | "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", 195 | "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", 196 | "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", 197 | "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", 198 | "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", 199 | "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", 200 | "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", 201 | "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", 202 | "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", 203 | "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", 204 | "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", 205 | "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", 206 | "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", 207 | "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", 208 | "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", 209 | "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", 210 | "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", 211 | "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", 212 | "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", 213 | "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", 214 | "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", 215 | "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", 216 | "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" 217 | ], 218 | "version": "==1.3.1" 219 | }, 220 | "mccabe": { 221 | "hashes": [ 222 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 223 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 224 | ], 225 | "version": "==0.6.1" 226 | }, 227 | "pkginfo": { 228 | "hashes": [ 229 | "sha256:5878d542a4b3f237e359926384f1dde4e099c9f5525d236b1840cf704fa8d474", 230 | "sha256:a39076cb3eb34c333a0dd390b568e9e1e881c7bf2cc0aee12120636816f55aee" 231 | ], 232 | "version": "==1.4.2" 233 | }, 234 | "pygments": { 235 | "hashes": [ 236 | "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", 237 | "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" 238 | ], 239 | "version": "==2.3.1" 240 | }, 241 | "pylint": { 242 | "hashes": [ 243 | "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", 244 | "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" 245 | ], 246 | "index": "pypi", 247 | "version": "==2.2.2" 248 | }, 249 | "readme-renderer": { 250 | "hashes": [ 251 | "sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f", 252 | "sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d" 253 | ], 254 | "version": "==24.0" 255 | }, 256 | "requests": { 257 | "hashes": [ 258 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 259 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 260 | ], 261 | "version": "==2.21.0" 262 | }, 263 | "requests-toolbelt": { 264 | "hashes": [ 265 | "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237", 266 | "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5" 267 | ], 268 | "version": "==0.8.0" 269 | }, 270 | "six": { 271 | "hashes": [ 272 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 273 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 274 | ], 275 | "version": "==1.12.0" 276 | }, 277 | "tqdm": { 278 | "hashes": [ 279 | "sha256:3c4d4a5a41ef162dd61f1edb86b0e1c7859054ab656b2e7c7b77e7fbf6d9f392", 280 | "sha256:5b4d5549984503050883bc126280b386f5f4ca87e6c023c5d015655ad75bdebb" 281 | ], 282 | "version": "==4.28.1" 283 | }, 284 | "twine": { 285 | "hashes": [ 286 | "sha256:7d89bc6acafb31d124e6e5b295ef26ac77030bf098960c2a4c4e058335827c5c", 287 | "sha256:fad6f1251195f7ddd1460cb76d6ea106c93adb4e56c41e0da79658e56e547d2c" 288 | ], 289 | "index": "pypi", 290 | "version": "==1.12.1" 291 | }, 292 | "typed-ast": { 293 | "hashes": [ 294 | "sha256:0555eca1671ebe09eb5f2176723826f6f44cca5060502fea259de9b0e893ab53", 295 | "sha256:0ca96128ea66163aea13911c9b4b661cb345eb729a20be15c034271360fc7474", 296 | "sha256:16ccd06d614cf81b96de42a37679af12526ea25a208bce3da2d9226f44563868", 297 | "sha256:1e21ae7b49a3f744958ffad1737dfbdb43e1137503ccc59f4e32c4ac33b0bd1c", 298 | "sha256:37670c6fd857b5eb68aa5d193e14098354783b5138de482afa401cc2644f5a7f", 299 | "sha256:46d84c8e3806619ece595aaf4f37743083f9454c9ea68a517f1daa05126daf1d", 300 | "sha256:5b972bbb3819ece283a67358103cc6671da3646397b06e7acea558444daf54b2", 301 | "sha256:6306ffa64922a7b58ee2e8d6f207813460ca5a90213b4a400c2e730375049246", 302 | "sha256:6cb25dc95078931ecbd6cbcc4178d1b8ae8f2b513ae9c3bd0b7f81c2191db4c6", 303 | "sha256:7e19d439fee23620dea6468d85bfe529b873dace39b7e5b0c82c7099681f8a22", 304 | "sha256:7f5cd83af6b3ca9757e1127d852f497d11c7b09b4716c355acfbebf783d028da", 305 | "sha256:81e885a713e06faeef37223a5b1167615db87f947ecc73f815b9d1bbd6b585be", 306 | "sha256:94af325c9fe354019a29f9016277c547ad5d8a2d98a02806f27a7436b2da6735", 307 | "sha256:b1e5445c6075f509d5764b84ce641a1535748801253b97f3b7ea9d948a22853a", 308 | "sha256:cb061a959fec9a514d243831c514b51ccb940b58a5ce572a4e209810f2507dcf", 309 | "sha256:cc8d0b703d573cbabe0d51c9d68ab68df42a81409e4ed6af45a04a95484b96a5", 310 | "sha256:da0afa955865920edb146926455ec49da20965389982f91e926389666f5cf86a", 311 | "sha256:dc76738331d61818ce0b90647aedde17bbba3d3f9e969d83c1d9087b4f978862", 312 | "sha256:e7ec9a1445d27dbd0446568035f7106fa899a36f55e52ade28020f7b3845180d", 313 | "sha256:f741ba03feb480061ab91a465d1a3ed2d40b52822ada5b4017770dfcb88f839f", 314 | "sha256:fe800a58547dd424cd286b7270b967b5b3316b993d86453ede184a17b5a6b17d" 315 | ], 316 | "markers": "python_version < '3.7' and implementation_name == 'cpython'", 317 | "version": "==1.1.1" 318 | }, 319 | "urllib3": { 320 | "hashes": [ 321 | "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", 322 | "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" 323 | ], 324 | "version": "==1.24.1" 325 | }, 326 | "webencodings": { 327 | "hashes": [ 328 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 329 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 330 | ], 331 | "version": "==0.5.1" 332 | }, 333 | "wrapt": { 334 | "hashes": [ 335 | "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" 336 | ], 337 | "version": "==1.10.11" 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /pygit/pygit.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3.6 2 | 3 | import os 4 | import sys 5 | import shutil 6 | import shelve 7 | import argparse 8 | import logging 9 | 10 | from datetime import datetime 11 | from subprocess import Popen, PIPE, STDOUT 12 | from pathlib import Path, PurePath, PureWindowsPath 13 | 14 | from send2trash import send2trash 15 | 16 | BASE_DIR = Path.home() 17 | DESKTOP = BASE_DIR / 'Desktop' 18 | SHELF_DIR = BASE_DIR / 'python-git-shelf' 19 | STATUS_DIR = BASE_DIR / 'python-git-status' 20 | TEST_DIR = BASE_DIR / "TEST_FOLDER" 21 | 22 | # NAME_SHELF = shelve.open(str(PurePath(SHELF_DIR / "NAME_SHELF"))) # Use the string representation to open path to avoid errors 23 | # INDEX_SHELF = shelve.open(str(PurePath(SHELF_DIR / "INDEX_SHELF"))) 24 | # MASTER_SHELF = shelve.open(str(PurePath(SHELF_DIR / "MASTER_SHELF"))) 25 | 26 | def logging_def(log_file_name): 27 | FORMATTER = logging.Formatter("%(asctime)s:%(funcName)s:%(levelname)s\n%(message)s") 28 | # console_logger = logging.StreamHandler(sys.stdout) 29 | file_logger = logging.FileHandler(log_file_name) 30 | file_logger.setFormatter(FORMATTER) 31 | 32 | logger = logging.getLogger(log_file_name) 33 | logger.setLevel(logging.DEBUG) 34 | logger.addHandler(file_logger) 35 | logger.propagate = False 36 | return logger 37 | 38 | 39 | pygit_logger = logging_def('pygit_logger.log') 40 | # logging.disable(logging.CRITICAL) 41 | 42 | 43 | def show_verbose_output(verbosity, *args): 44 | """Logs output""" 45 | if verbosity: 46 | for arg in args: 47 | pygit_logger.debug(arg) 48 | 49 | 50 | def cleanup(): 51 | """Cleanup files""" 52 | send2trash(SHELF_DIR) 53 | return 54 | 55 | # keep for later 56 | def kill_process(process): 57 | if process.poll() is None: # don't send the signal unless it seems it is necessary 58 | try: 59 | process.kill() 60 | except PermissionError: # ignore 61 | print("Os error. cannot kill kill_process") 62 | pass 63 | return 64 | 65 | 66 | def need_attention(status_msg): 67 | """Return True if a repo status is not exactly same as that of remote""" 68 | msg = ["not staged", "behind", "ahead", "Untracked"] 69 | if any([each in status_msg for each in msg]): 70 | return True 71 | return False 72 | 73 | 74 | def is_git_repo(directory): 75 | """ 76 | Determine if a folder is a git repo 77 | Checks the 'git status' message for error 78 | """ 79 | files = os.listdir(directory) 80 | if '.git' in files: 81 | return True 82 | return False 83 | 84 | 85 | def check_git_support(): 86 | """ 87 | Return True if git is available via command line. 88 | If not, check if its available as an executable in installation folder. 89 | """ 90 | proc = Popen(['git', '--version'], shell=True, stdout=PIPE,) 91 | msg, _ = proc.communicate() 92 | msg = msg.decode('utf-8') 93 | if "git version" in msg: 94 | return True 95 | return False 96 | 97 | 98 | def get_command_line_arguments(): 99 | """Get arguments from command line""" 100 | 101 | parser = argparse.ArgumentParser(prog="Pygit. Initialize working directories for python-git") 102 | parser.add_argument("-v", "--verbosity", type=int, help="turn verbosity ON/OFF", choices=[0,1]) 103 | parser.add_argument("-r", "--rules", help="Set a list of string patterns for folders to skip during setup", nargs='+') 104 | parser.add_argument('-g', '--gitPath', help="Full pathname to git executable. cmd or bash.") 105 | parser.add_argument('-m', '--masterDirectory', help="Full pathname to directory holding any number of git repos.") 106 | parser.add_argument('-s', '--simpleDirectory', help="A list of full pathnames to any number of individual git repos.", nargs='+') 107 | return parser.parse_args() 108 | 109 | 110 | def shelve_git_path(git_path, verbosity): 111 | """Find and store the location of git executable""" 112 | 113 | if check_git_support(): 114 | print("Your system is configured to work with git.\n") 115 | elif "git" in os.environ['PATH']: 116 | user_paths = os.environ['PATH'].split(os.pathsep) 117 | for path in user_paths: 118 | if "git-cmd.exe" in path: 119 | NAME_SHELF['GIT_WINDOWS'] = path 120 | return 121 | if "git-bash.exe" in path: 122 | NAME_SHELF['GIT_BASH'] = path 123 | return 124 | else: 125 | print("Git was not found in your system path.\nYou may need to set the location manually using the -g flag.\n") 126 | 127 | if git_path: 128 | for _, __, files in os.walk(git_path): 129 | if "git-cmd.exe" in files: 130 | NAME_SHELF['GIT_WINDOWS'] = git_path 131 | elif "git-bash.exe" in files: 132 | NAME_SHELF['GIT_BASH'] = git_path 133 | else: 134 | print("A valid git executable was not found in the directory.\n") 135 | return 136 | 137 | 138 | def enforce_exclusion(folder_name, verbosity): 139 | """Return True if a folder starts with any character in exclusion_folder_start""" 140 | exclusion_folder_start = [".", "_"] # skip folders that start with any of these characters 141 | if any([str(PurePath(folder_name)).startswith(each) for each in exclusion_folder_start]): 142 | if verbosity: 143 | show_verbose_output(verbosity, folder_name, " starts with one of ", exclusion_folder_start, " skipping\n") 144 | return True 145 | return False 146 | 147 | 148 | def match_rule(rules, path, verbosity): 149 | """Return True if a folder matches a rule in rules""" 150 | if rules: 151 | if any([rule in path for rule in rules]): 152 | show_verbose_output(verbosity, path, " matches an exclusion rule. Skipping\n") 153 | return True 154 | return False 155 | 156 | 157 | def save_master(master_directory): 158 | """Saves the location of the master directory""" 159 | global MASTER_SHELF 160 | MASTER_SHELF = shelve.open(str(PurePath(SHELF_DIR / "MASTER_SHELF"))) 161 | MASTER_SHELF["master"] = master_directory 162 | MASTER_SHELF.close() 163 | 164 | 165 | def shelve_master_directory(master_directory, verbosity, rules): 166 | """Find and store the locations of git repos""" 167 | 168 | if master_directory: 169 | save_master(master_directory) 170 | show_verbose_output(verbosity, "Master directory set to ", master_directory, "Now Shelving") 171 | 172 | i = len(list(INDEX_SHELF.keys())) + 1 173 | folder_paths = [x for x in Path(master_directory).iterdir() if x.is_dir()] 174 | 175 | for f in folder_paths: # log folders 176 | show_verbose_output(verbosity, f) 177 | 178 | for folder_name in folder_paths: 179 | path = Path(master_directory) / folder_name 180 | if enforce_exclusion(folder_name, verbosity): 181 | continue 182 | if match_rule(rules, path, verbosity): 183 | continue 184 | 185 | directory_absolute_path = Path(path).resolve() 186 | if is_git_repo(directory_absolute_path): 187 | if sys.platform == 'win32': 188 | name = PureWindowsPath(directory_absolute_path).parts[-1] 189 | if sys.platform == 'linux': 190 | name = PurePath(directory_absolute_path).parts[-1] 191 | 192 | show_verbose_output(verbosity, directory_absolute_path, " is a git repository *** shelving\n") 193 | 194 | NAME_SHELF[name] = directory_absolute_path 195 | INDEX_SHELF[str(i)] = name 196 | i += 1 197 | # NAME_SHELF.close() 198 | # INDEX_SHELF.close() 199 | 200 | 201 | def shelve_simple_directory(simple_directory, verbosity): 202 | if simple_directory: 203 | 204 | i = len(list(INDEX_SHELF.keys())) + 1 205 | for directory in simple_directory: 206 | 207 | if is_git_repo(directory): 208 | show_verbose_output(verbosity, " is a git repository *** shelving\n") 209 | if sys.platform == 'win32': 210 | name = directory.split("\\")[-1] 211 | if sys.platform == 'linux': 212 | name = directory.split("/")[-1] 213 | NAME_SHELF[name] = directory 214 | INDEX_SHELF[str(i)] = name 215 | else: 216 | show_verbose_output(verbosity, " is not a valid git repo.\nContinuing...\n") 217 | continue 218 | i += 1 219 | 220 | 221 | def initialize(): 222 | """Initialize the data necessary for pygit to operate""" 223 | print("Initializing ...") 224 | 225 | global NAME_SHELF, INDEX_SHELF 226 | try: 227 | Path.mkdir(SHELF_DIR) 228 | except FileExistsError: 229 | shutil.rmtree(SHELF_DIR) 230 | Path.mkdir(SHELF_DIR) 231 | 232 | try: 233 | Path.mkdir(STATUS_DIR) 234 | except FileExistsError: 235 | pass 236 | 237 | NAME_SHELF = shelve.open(str(PurePath(SHELF_DIR / "NAME_SHELF"))) # Use the string representation to open path to avoid errors 238 | INDEX_SHELF = shelve.open(str(PurePath(SHELF_DIR / "INDEX_SHELF"))) 239 | 240 | args = get_command_line_arguments() 241 | verbosity = args.verbosity 242 | rules = args.rules 243 | shelve_git_path(args.gitPath, verbosity) 244 | shelve_master_directory(args.masterDirectory, verbosity, rules) 245 | shelve_simple_directory(args.simpleDirectory, verbosity) 246 | 247 | INDEX_SHELF.close() 248 | NAME_SHELF.close() 249 | 250 | if verbosity: 251 | print("\nIndexed git repos.\n") 252 | NAME_SHELF = shelve.open(str(PurePath(SHELF_DIR / "NAME_SHELF"))) 253 | INDEX_SHELF = shelve.open(str(PurePath(SHELF_DIR / "INDEX_SHELF"))) 254 | 255 | print("Status saved in {}".format(STATUS_DIR)) 256 | print("{:<4} {:<20} {:<}".format("Key", "| Name", "| Path")) 257 | print("*********************************") 258 | for key in INDEX_SHELF.keys(): 259 | name = INDEX_SHELF[key] 260 | print("{:<4} {:<20} {:<}".format(key, name, str(NAME_SHELF[name]))) 261 | else: 262 | print("Indexing done") 263 | return 264 | 265 | 266 | def update(): 267 | """Update INDEX_SHELF""" 268 | MASTER_SHELF = shelve.open(str(PurePath(SHELF_DIR / "MASTER_SHELF"))) 269 | INDEX_SHELF = shelve.open(str(PurePath(SHELF_DIR / "INDEX_SHELF"))) 270 | NAME_SHELF = shelve.open(str(PurePath(SHELF_DIR / "NAME_SHELF"))) 271 | 272 | master = MASTER_SHELF["master"] 273 | print("Master ", master) 274 | # shelve_master_directory(master, 0, "") 275 | save_master(master) 276 | 277 | i = len(list(INDEX_SHELF.keys())) + 1 278 | folder_paths = [x for x in Path(master).iterdir() if x.is_dir()] 279 | 280 | for folder_name in folder_paths: 281 | path = Path(master) / folder_name 282 | 283 | directory_absolute_path = Path(path).resolve() 284 | if is_git_repo(directory_absolute_path): 285 | if sys.platform == 'win32': 286 | name = PureWindowsPath(directory_absolute_path).parts[-1] 287 | if sys.platform == 'linux': 288 | name = PurePath(directory_absolute_path).parts[-1] 289 | 290 | NAME_SHELF[name] = directory_absolute_path 291 | INDEX_SHELF[str(i)] = name 292 | i += 1 293 | 294 | print("Update completed successfully") 295 | return 296 | 297 | 298 | class Commands: 299 | """Commands class 300 | 301 | Parameters 302 | ----------- 303 | repo_name : str 304 | The repository name. See list of repositories by running 305 | master_directory : str 306 | The absolute path to the directory 307 | git_exec : str 308 | The path to the git executable on the system 309 | message : str 310 | Commit message 311 | 312 | Returns 313 | -------- 314 | : Commands object 315 | """ 316 | 317 | def __str__(self): 318 | return "Commands: {}: {}".format(self.name, self.dir) 319 | 320 | def __eq__(self, other): 321 | if isinstance(other, self.__class__): 322 | return self.__dict__ == other.__dict__ 323 | else: 324 | return False 325 | 326 | def __init__(self, repo_name, master_directory, git_exec=None, message="minor changes"): 327 | self.name = repo_name 328 | self.dir = master_directory 329 | self.git_exec = git_exec 330 | self.message = message 331 | 332 | try: 333 | os.chdir(self.dir) 334 | except (FileNotFoundError, TypeError): 335 | print("{} may have been moved.\n Run initialize() to update paths".format(self.name)) 336 | self.dir = os.getcwd() 337 | 338 | 339 | def need_attention(self): 340 | """Return True if a repo status is not exactly same as that of remote""" 341 | msg = ["not staged", "behind", "ahead", "Untracked"] 342 | status_msg = self.status() 343 | if any([each in status_msg for each in msg]): 344 | return True 345 | return False 346 | 347 | def fetch(self): 348 | """git fetch""" 349 | if self.git_exec: 350 | process = Popen([self.git_exec, "git fetch"], stdin=PIPE, stdout=PIPE, stderr=STDOUT) 351 | else: 352 | process = Popen("git fetch", shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 353 | # output, error = process.communicate() 354 | process.communicate() 355 | 356 | def status(self): 357 | """git status""" 358 | self.fetch() # always do a fetch before reporting status 359 | if self.git_exec: 360 | process = Popen([self.git_exec, " git status"], stdin=PIPE, stdout=PIPE, stderr=STDOUT) 361 | else: 362 | process = Popen("git status", shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 363 | output, _ = process.communicate() 364 | return str(output.decode("utf-8")) 365 | 366 | def stage_file(self, file_name): 367 | """git add file""" 368 | stage_file = 'git add {}'.format(file_name) 369 | if self.git_exec: 370 | process = Popen([self.git_exec, stage_file], stdin=PIPE, stdout=PIPE, stderr=STDOUT,) 371 | else: 372 | process = Popen(stage_file, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT,) 373 | output, _ = process.communicate() 374 | return str(output.decode("utf-8")) 375 | 376 | def stage_all(self, files="."): 377 | """git add all""" 378 | files = "` ".join(files.split()) 379 | stage_file = 'git add {}'.format(files) 380 | if self.git_exec: 381 | process = Popen([self.git_exec, stage_file], stdin=PIPE, stdout=PIPE, stderr=STDOUT,) 382 | else: 383 | process = Popen(stage_file, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT,) 384 | output, _ = process.communicate() 385 | return str(output.decode("utf-8")) 386 | 387 | def commit(self): 388 | """git commit""" 389 | enter = input("Commit message.\nPress enter to use 'minor changes'") 390 | if enter == "": 391 | message = self.message 392 | else: 393 | message = enter 394 | # message = "` ".join(message.split()) 395 | if self.git_exec: 396 | process = Popen([self.git_exec, 'git', ' commit ', '-m ', message], stdin=PIPE, stdout=PIPE, stderr=PIPE,) 397 | else: 398 | process = Popen(['git', ' commit', ' -m ', message], shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE,) 399 | output, _ = process.communicate() 400 | return str(output.decode("utf-8")) 401 | 402 | def stage_and_commit(self): 403 | """git add followed by commit""" 404 | self.stage_all() 405 | self.commit() 406 | 407 | def push(self): 408 | """git push""" 409 | if self.git_exec: 410 | process = Popen([self.git_exec, ' git push'], stdin=PIPE, stdout=PIPE, stderr=STDOUT,) 411 | else: 412 | process = Popen(['git push'], shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE,) 413 | output, _ = process.communicate() 414 | return str("Push completed.{}".format(str(output.decode("utf-8")))) 415 | 416 | def pull(self): 417 | """git pull""" 418 | if self.git_exec: 419 | process = Popen([self.git_exec, ' git pull'], stdin=PIPE, stdout=PIPE, stderr=STDOUT,) 420 | else: 421 | process = Popen(['git pull'], shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE,) 422 | output, _ = process.communicate() 423 | return str("Pull completed.\n{}".format(str(output.decode("utf-8")))) 424 | 425 | def reset(self, number='1'): 426 | """git reset""" 427 | if self.git_exec: 428 | process = Popen([self.git_exec, ' git reset HEAD~', number], stdin=PIPE, stdout=PIPE, stderr=STDOUT,) 429 | else: 430 | process = Popen(['git reset HEAD~', number], stdin=PIPE, stdout=PIPE, stderr=STDOUT,) 431 | output, _ = process.communicate() 432 | return str(output.decode("utf-8")) 433 | 434 | # def branch(self): 435 | # """Return the branch being tracked by local""" 436 | # process = Popen([self.git_exec, 'git branch -vv'], shell=True, 437 | # stdin=PIPE, stdout=PIPE, stderr=STDOUT,) 438 | # output, _ = process.communicate() 439 | # out_text = str(output.decode("utf-8")) 440 | # try: 441 | # line = [each for each in out_text.split("\n") if each.startswith("*")][0] 442 | # except IndexError: # no lines start with * 443 | # return 444 | # branch_name = re.search(r"\[origin\/(.*)\]", line) 445 | # return branch_name.group(1) 446 | 447 | def repos(): 448 | """Show all available repositories, path, and unique ID""" 449 | print("\nThe following repos are available.\n") 450 | NAME_SHELF = shelve.open(str(PurePath(SHELF_DIR / "NAME_SHELF"))) 451 | INDEX_SHELF = shelve.open(str(PurePath(SHELF_DIR / "INDEX_SHELF"))) 452 | 453 | print("{:<4} {:<20} {:<}".format("Key", "| Name", "| Path")) 454 | print("******************************************") 455 | for key in INDEX_SHELF.keys(): 456 | name = INDEX_SHELF[key] 457 | print("{:<4} {:<20} {:<}".format(key, name, str(NAME_SHELF[name]))) 458 | INDEX_SHELF.close() 459 | NAME_SHELF.close() 460 | 461 | 462 | def load(input_string): # id is string 463 | """Load a repository with specified id""" 464 | NAME_SHELF = shelve.open(str(PurePath(SHELF_DIR / "NAME_SHELF"))) 465 | INDEX_SHELF = shelve.open(str(PurePath(SHELF_DIR / "INDEX_SHELF"))) 466 | input_string = str(input_string) 467 | 468 | try: 469 | int(input_string) # if not coercible into an integer, then its probably a repo name rather than ID 470 | try: 471 | name = INDEX_SHELF[input_string] 472 | return Commands(name, str(NAME_SHELF[name])) 473 | except KeyError: 474 | raise Exception("That index does not exist.") 475 | except ValueError: 476 | try: 477 | return Commands(input_string, NAME_SHELF[input_string]) 478 | except KeyError: 479 | raise Exception("That repository name does not exist or is not indexed") 480 | INDEX_SHELF.close() 481 | NAME_SHELF.close() 482 | 483 | 484 | def load_multiple(*args, _all=False): 485 | """Create `commands` object for a set of repositories 486 | 487 | Parameters 488 | ------------ 489 | args : int 490 | comma-separated string values 491 | 492 | Yields 493 | --------- 494 | A list of commands objects. One for each of the entered string 495 | """ 496 | 497 | if _all: 498 | NAME_SHELF = shelve.open(str(PurePath(SHELF_DIR / "NAME_SHELF"))) 499 | for key in NAME_SHELF.keys(): 500 | yield load(key) 501 | else: 502 | for arg in args: 503 | yield load(arg) 504 | 505 | 506 | def pull(*args, _all=False): 507 | for each in load_multiple(*args, _all=_all): 508 | s = "*** {} ***\n{}".format(each.name, each.pull()) 509 | print(s) 510 | 511 | 512 | def push(*args, _all=False): 513 | for each in load_multiple(*args, _all=_all): 514 | s = "*** {} ***\n{}".format(each.name, each.push()) 515 | print(s) 516 | 517 | 518 | def all_status(): 519 | """Write status of all repositories to file in markdown format""" 520 | print("Getting repo status.\n\nYou may be prompted for credentials...") 521 | 522 | os.chdir(STATUS_DIR) 523 | attention = "" 524 | messages = [] 525 | TIME_STAMP = datetime.now().strftime("%a_%d_%b_%Y_%H_%M_%S_%p") 526 | 527 | fname = "REPO_STATUS_@_{}.md".format(TIME_STAMP) 528 | with open(fname, 'w+') as f: 529 | f.write("# Repository status as at {}\n\n".format(TIME_STAMP)) 530 | 531 | for each in load_multiple(_all=True): 532 | name = each.name 533 | status = each.status() 534 | 535 | messages.append("## {}\n\n```cmd\n{}```\n".format(name, status)) 536 | 537 | if need_attention(status): 538 | attention += "1. {}\n".format(name) 539 | 540 | f.write("## REPOS NEEDING ATTENTION\n\n") 541 | f.write(attention) 542 | f.write("\n-------\n\n") 543 | f.write("## STATUS MESSAGES\n\n") 544 | f.write("\n".join(messages)) 545 | 546 | print("\n\nDone. Status file saved in ", STATUS_DIR) 547 | os.chdir(BASE_DIR) 548 | return 549 | 550 | if __name__ == "__main__": 551 | initialize() 552 | --------------------------------------------------------------------------------